# The SPEC is currently work-in-progress

# Types

The following types are built into Fpy, and the developer can directly refer to them by name:
* Numeric types: `U8, U16, U32, U64, I8, I16, I32, I64, F32, F64`
* Boolean type: `bool`
* Time type: `Fw.Time`

In addition, the developer can directly refer to any displayable type defined in FPP via its fully-qualified name. This includes user-defined structs, arrays and enums.

There are some types which exist in Fpy but cannot be directly referenced by name by the developer. These are the *Int*, and *LiteralString* types. See [literals](#literals).

## Structs
You can instantiate a new struct at runtime by calling its constructor. A struct's constructor is a function with the same name as the type, with arguments corresponding to the type and position of the struct's members. For example, a struct defined as:
```
module Fw {
    struct Example {
        intValue: U8
        boolValue: bool
    }
}
```
can be constructed in Fpy like:
```
Fw.Example(0, True)
```


## Fields
Fields refer to either a member of a struct, or an element of an array. Field access uses Python-like syntax: `expr.member` reads a struct (or `Fw.Time`) member and `expr[index]` reads an array element. These operations are only legal when the referenced type has a statically known layout. Because strings do not have a fixed size in memory, structs or arrays with string fields do not have a statically known layout.

Accessing `Fw.Time` produces synthetic members named `time_base`, `time_context`, `seconds`, and `useconds` with the types defined by F´.

Array indices are coerced to `I64` before use. If the index is a compile-time constant the compiler emits an error when it falls outside `[0, length)`. Otherwise the generated bytecode performs a runtime bounds check and terminates the sequence with `DirectiveErrorCode.ARRAY_OUT_OF_BOUNDS` if it fails.

# Literals
The following literals are supported by Fpy:
* Integer literals: `123`, `-456_879`
* Float literals: `0.123`, `1e-5`
* String literals: `"hello world"`, `'example string'`
* Boolean literals: `True` and `False`

## Integer literals
Integer literals are strings matching:
```
DEC_NUMBER:   "1".."9" ("_"?  "0".."9" )*
          |   "0"      ("_"?  "0"      )* /(?![1-9])/
```

The first rule of this syntax allows for integers without leading zeroes, separated by underscores. So this is okay:
```
123_456
```
but this is not:
```
0123_456
```

The second rule allows you to write any number of zeroes, separated by underscores:
```
00_000_0
```

Integer literals have type *Int*, which is not directly referenceable by the user. The *Int* type supports integers of arbitrary size.

## Float literals
Float literals are strings matching:
```
FLOAT_NUMBER: _SPECIAL_DEC _EXP | DECIMAL _EXP?
```
where `_SPECIAL_DEC`, `_EXP` and `DECIMAL` are defined as:

```
_SPECIAL_DEC: "0".."9" ("_"?  "0".."9")*
_EXP: ("e"|"E") ["+" | "-"] _SPECIAL_DEC
DECIMAL: "." _SPECIAL_DEC | _SPECIAL_DEC "." _SPECIAL_DEC
```

A `FLOAT_NUMBER` can be any string of digits suffixed with an exponent, like these:
```
1e-5
100_200e10
```

or it can be a `DECIMAL` optionally suffixed by an exponent, like these:
```
2.123
100.5e+10
```

Float literals have type `F64`.

## String literals
String literals are strings matching:
```
STRING: /("(?!"").*?(?<!\\)(\\\\)*?"|'(?!'').*?(?<!\\)(\\\\)*?')/i
```

They have type *LiteralString*, which is not directly referenceable by the user. The *LiteralString* type supports strings of arbitrary length.

# Functions

Every callable in Fpy uses the same syntax:
```
function_name(arg_0, arg_1, ..., arg_n)
```
Arguments are evaluated left-to-right exactly once. After evaluation, the compiler coerces each argument to the parameter type declared by the callable (except when invoking an explicit numeric cast, which bypasses the usual coercion rules). If any coercion fails, compilation fails. The value produced by the call has the callable’s declared return type and may later be coerced again by the surrounding context.

Fpy exposes several categories of callables:

## Commands
Every command instance defined in the FPP dictionary can be called. The callable name is the command’s fully qualified name, the signature matches the command’s FPP arguments, and the return type is always `Fw.CmdResponse`. Calling a command immediately serializes the opcode and arguments, sends them to the dispatcher, blocks the sequence until the command finishes, and then yields the dispatcher’s `Fw.CmdResponse`.

## Macros
Inline macros behave like functions whose bodies are pre-defined sequences of bytecode directives. They are defined in `src/fpy/macros.py`, evaluate their arguments, push those values onto the stack, and then emit the directives listed below.

Available macros:

* `exit(exit_code: U8)`: terminates the sequence immediately by emitting an `ExitDirective`.
* `log(operand: F64) -> F64`: computes the natural logarithm of the operand using `FloatLogDirective` and leaves the `F64` result on the stack.
* `sleep(seconds: U32 = 0, microseconds: U32 = 0)`: waits for the specified relative duration (the assembler emits `WaitRelDirective`).
* `sleep_until(wakeup_time: Fw.Time)`: waits until the supplied absolute time using `WaitAbsDirective`.
* `now() -> Fw.Time`: pushes the current time via `PushTimeDirective`.
* `time(timestamp: String, time_base: U16 = 0, time_context: U8 = 0) -> Fw.Time`: parses an ISO 8601 timestamp string (e.g., `"2025-12-19T14:30:00Z"` or `"2025-12-19T14:30:00.123456Z"`) at compile time and returns an `Fw.Time` with the specified `time_base` and `time_context`. The timestamp must be in UTC with a `Z` suffix.
* `iabs(value: I64) -> I64`: returns the absolute value of a signed 64-bit integer.
* `fabs(value: F64) -> F64`: returns the absolute value of a 64-bit float.

## Type constructors
Structs, arrays, and `Fw.Time` expose constructors whose callable name is the fully qualified type name. Their arguments correspond to the members in declaration order (struct fields by name, array elements as `e0`, `e1`, ..., and `Fw.Time` with `time_base`, `time_context`, `seconds`, `useconds`). A constructor call serializes the provided values into a new instance of that type.

## Numeric casts
Each concrete numeric type provides a callable whose name matches the type (for example `U16(value)` or `F64(value)`). Casts accept exactly one numeric argument. Unlike implicit coercion, casts always force the operand into the target type even when this requires narrowing; range checks are suppressed and the value is truncated or rounded if necessary. See [Casting](#casting) for details.

## User-defined functions
A function definition introduces a new callable into scope. Function definitions must appear at the top level of the sequence; a function definition nested inside another function, a loop body, or a conditional branch is a compile-time error. The syntax is:
```
def name(param_0: Type0, param_1: Type1 = default_value, ...) [-> ReturnType]:
    body
```

### Parameters
Each parameter declaration consists of a name followed by a colon and a type annotation. A parameter may optionally include a default value, written as `= expr` after the type annotation. Default value expressions must be constant expressions: literals, enum constants, or type constructors whose arguments are themselves constant expressions. Expressions referencing telemetry channels, variables, or function calls are not constant and produce a compile-time error when used as defaults.

Arguments may be passed by position or by name. Positional arguments are bound to parameters left-to-right. Named arguments use the syntax `name=expr` and bind the value of `expr` to the parameter with the matching name. All positional arguments must precede all named arguments. A parameter may not be bound more than once; supplying both a positional argument and a named argument for the same parameter is a compile-time error. If fewer arguments are supplied than parameters, the remaining parameters must have default values; those defaults are evaluated and bound. Supplying more positional arguments than parameters, or naming a parameter that does not exist, is a compile-time error.

### Return type
The return type annotation `-> Type` is optional. When present, every control-flow path through the function body must terminate with a `return expr` statement where `expr` has a type coercible to `Type`. When absent, the function does not produce a value; `return` statements in such functions must not include an expression, and the call expression has no usable result.

### Scope
A function body introduces a new scope. Within this scope the following names are visible:
1. Parameters declared in the function signature.
2. Local variables declared within the function body.
3. Top-level variables declared before the call site of the function (not the definition site).
4. All dictionary objects: commands, telemetry channels, parameters, enum constants, and types.
5. All user-defined functions, including functions defined after the current function (forward references are permitted).

Assignments to top-level variables within a function body modify the original variable. Assignments to parameters or local variables do not affect any outer scope.

# Type conversion

Type conversion is the process of converting an expression from one type to another. It can either be implicit, in which case it is called coercion, or explicit, in which case it is called casting.

## Coercion
Coercion happens when an expression of type *A* is used in a syntactic element which requires an expression of type *B*. For example, functions, operators and variable assignments all require specific input types, so type coercion happens in each of these.
In general, the rule of thumb is that coercion is allowed if the destination type can represent all possible values of the source type, with some exceptions. The following rules determine when type coercion can be performed:

1. If the source and destination types are identical, no coercion is performed.
2. *LiteralString* values may be coerced into any FPP string type. No other string expression can be coerced.
3. Otherwise both source and destination must be numeric (`NumericalValue`). Numeric coercions obey these constraints:
    * Floats never coerce to integers.
    * Integers may always coerce to floats.
    * Float-to-float coercions require a destination bit width greater than or equal to the source width.
    * Integer-to-integer coercions require matching signedness and a destination bit width greater than or equal to the source width.
    * Arbitrary-precision types (`Int`/`Float`) may coerce to any finite-width numeric type.
If no rule matches, the compiler raises an error.

Compile-time constant floats (including literals and constant-folded expressions) can only be narrowed into a smaller floating-point type when the value lies inside the destination’s representable range. When the value fits, the compiler rounds it to the nearest representable floating-point number; otherwise compilation fails with an out-of-range error.

## Casting
Each finite-bitwidth numeric type exposes an explicit cast with the same name as the type, e.g. `U32(value)` or `F64(value)`. Casts accept any numeric expression and bypass the implicit-coercion restrictions above: the operand is forced to the target type even when that entails narrowing, and compile-time range checks are suppressed. No casts exist for structs, arrays, enums, strings, or `Fw.Time`.


# Operators

Fpy supports the following operators:
* Basic arithmetic: `+, -, *, /`
* Modulo: `%`
* Exponentiation: `**`
* Floor division: `//`
* Boolean: `and, or, not`
* Comparison: `<, >, <=, >=, ==, !=`

Each time an operator is used, an intermediate type must be picked and both args must be converted to that type.

## Behavior of operators
All operators share the following rules:

1. The left operand is evaluated first, then the right operand. Boolean `and`/`or` short-circuit, so the right operand is skipped when the result is already known.
2. Each operand is coerced to the operator’s intermediate type (see [Intermediate Types](#intermediate-types)). If no valid intermediate type exists, compilation fails.

The subsections below describe behaviors that differ from the general rules.

### Numeric arithmetic (`+`, `-`, `*`)
These operators require numeric operands and produce a result in the chosen intermediate type. Addition, subtraction, and multiplication differ only in which arithmetic operation they perform. Integer overflow wraps according to the destination type when the result is ultimately stored, and floating-point operations follow IEEE-754 behavior.

### True division (`/`)
Both operands are promoted to `F64`, and the result is always an `F64`. This means you must explicitly cast the result to store it in an integer type.

### Floor division (`//`)
With integer operands, `//` performs truncating division using the signed or unsigned divide directive. If either operand is a float, the compiler divides in `F64`, converts the quotient to a signed 64-bit integer (which truncates toward zero), and converts back to `F64`, so floating-point floor division also truncates toward zero.

### Modulo (`%`)
Modulo works for numeric operands. Signed operands use the signed modulo directive, unsigned operands use the unsigned directive, and floats use floating-point modulo. For signed integers the remainder has the same sign as the dividend.

### Exponentiation (`**`)
Both operands are coerced to `F64`, the exponentiation happens in floating point, and the result type is `F64`.

### Boolean operators (`and`, `or`, `not`)
Operands must be `bool`. `not` negates a single operand. `and` evaluates the left operand first and only evaluates the right operand when the left operand is `True`. Conversely, `or` skips the right operand when the left operand is `True`. The result of every boolean operator is `bool`.

### Inequalities (`<`, `<=`, `>`, `>=`)
Inequalities require numeric operands. Each operand is coerced to the intermediate type, the comparison runs in that type, and the result is `bool`.

### Equality (`==`, `!=`)
If both operands are numeric, equality uses the same intermediate-type rules as arithmetic operators. Otherwise both operands must have the exact same concrete type (struct, array, enum, or `Fw.Time`). The compiler compares their serialized bytes. Strings cannot be compared.

## Intermediate types

Intermediate types are picked via the following rules:

1. The intermediate type of Boolean operators is always `bool`.
2. The intermediate type of `==` and `!=` may be any type, so long as the left and right hand sides are the same type. If both are numeric then continue.
3. If either argument is non-numeric, raise an error.
4. If the operator is `/` or `**`, the intermediate type is always `F64`.
5. If either argument is a float, the intermediate type is `F64`.
6. If either argument is an unsigned integer, the intermediate type is `U64`.
7. Otherwise, the intermediate type is `I64`.

If the expressions given to the operator are not of the intermediate type, type coercion rules are applied.

## Result type

The result type is the type of the value produced by the operator.
1. For numeric operators, the result type is the intermediate type.
2. For boolean and comparison operators, the result type is `bool`.

Normal type coercion rules apply to the result, of course. Once the operator has produced a value, it may be coerced into some other type depending on context.

# Loops

## Range expressions

The `lower .. upper` operator produces a `RangeValue`. Both bounds are coerced to `I64`. Range expressions are only meaningful as the right-hand side of a `for` loop, and both bounds are evaluated exactly once.

## For loops

```
for <var> in <lower> .. <upper>:
    <body>
```

* `<lower>` and `<upper>` are evaluated before the loop starts and stored in hidden variables.
* The loop variable is assigned `<lower>`, coerced to `I64`. If the name was not previously declared, the compiler declares it with type `I64`; otherwise the existing variable must already be of that type.
* The body runs while the loop variable is strictly less than `<upper>`. After each iteration the compiler inserts `var = var + 1`. Modifying `var` manually inside the body affects the next iteration in addition to this implicit increment.
* `break` and `continue` statements are only legal inside the loop body and behave as in C: `break` exits the loop, `continue` skips to the implicit increment/check sequence.

## While loops

```
while <condition>:
    <body>
```

The condition is coerced to `bool` and re-evaluated before every iteration. `break` and `continue` are legal only within the loop body.

