State, functions and decorators
A step evaluates once per active tick. Its equations produce current values;
fby stores a value for the next tick.
Delay: fby
Section titled “Delay: fby”model Counter {}
step Counter() -> (count: i32) { count = 0 fby count + 1;}This produces 0, 1, 2, .... In initial fby next:
- The first active tick returns
initial; each later tick returns the previous active tick’snext. - Both operands must have the same type. These can carry state:
- scalars
- arrays
- composites
- enums
- The initial value must be available at initialization. It cannot depend on
an input, a local or
dt. It may be any of:- a literal
- a named
const - a builtin constant
- an enum variant
- a parameter read
- arithmetic, arrays and records built from those
- Each
fbyexpression owns independent state. Reinitializing the model resets it.
dt is the current step duration in seconds. An unrated child inherits its
caller’s duration; @every
sets an explicit period.
Feedback and scheduling
Section titled “Feedback and scheduling”Equations may refer to values declared later in the step. The compiler orders
them by dependency. Every feedback cycle must cross a fby; a cycle of only
current-tick values is an algebraic loop and is rejected.
A child model call is an atomic scheduling boundary. A delay inside the child
does not break a loop around the call. Put a fby on the parent’s feedback path,
or declare the child field inline child: Child to expose its equations to the
parent scheduler. External and rated children cannot be inlined.
Conditional state: match
Section titled “Conditional state: match”match selects one enum arm and declares the values that each arm must produce:
enum Mode { Idle, Running }
model Supervisor {}
step Supervisor(enable: bool) -> (ticks: i32) { let mode: Mode = Mode::Idle fby next; match mode -> (next: Mode, elapsed: i32) { Mode::Idle => { next = if enable { Mode::Running } else { Mode::Idle }; elapsed = 0; } Mode::Running => { let n: i32 = 0 fby n + 1; next = if enable { Mode::Running } else { Mode::Idle }; elapsed = n; } } ticks = elapsed;}- Every variant requires exactly one arm. Each arm assigns every target once.
- Arm locals are visible only in that arm. Assign step outputs after the match, using its targets.
- Delays and child calls inside an arm advance only when that arm runs. Their state is retained while inactive; entering an arm does not reset it.
- A child can be called once in each alternative arm, but cannot also be called
outside the match. An
inlinechild cannot be shared across arms. - Nested matches, wildcard arms, and rated child calls inside arms are unsupported.
Use if condition { a } else { b } to select a value. Use match when an enum
must control which state advances.
Pure functions
Section titled “Pure functions”fn declares a stateless function. It takes typed values and returns one typed
value. Its body is a block whose final expression has the declared return type.
fn clamp(x: f64, lo: f64, hi: f64) -> f64 { if x < lo { lo } else if x > hi { hi } else { x }}Functions may call other functions and builtins. Calls can appear before or
after the declaration. Recursive call cycles are rejected. extern fn name(...) -> T; declares a Rust implemented function with the same signature.
Pure functions cannot read tick state. These are errors in a function:
fbydtselfmodel reads or steps
These are allowed:
- constants
ZERO- arguments
- local
letbindings - conditionals
- arrays
- indexing
- composite values
Arguments must have the declared types and arity; there is no implicit numeric conversion.
Builtins
Section titled “Builtins”Builtins are reserved names and resolve before user functions. Numeric builtins
lift element-wise over arrays when the first argument is an array. For lifted
calls, later scalar arguments broadcast and later array arguments must have the
same shape. dot, amax and norm are array reductions, and cross is the
3-vector product.
| Signature | Builtins |
|---|---|
T -> T |
abs, sin, cos, tan, asin, acos, atan, sqrt, exp, ln |
T, T -> T |
min, max, atan2, powf |
T, T, T -> T |
limit(value, lo, hi) |
[T; N] -> T |
amax, norm |
[T; N], [T; N] -> T |
dot |
[T; 3], [T; 3] -> [T; 3] |
cross |
T is one numeric type. min, max, abs, limit, dot, amax, and
cross accept integers or floats; the remaining functions require floats.
limit(value, lo, hi)returnsloifvalue < lo,hiifvalue > hi, otherwisevalue.atan2(y, x)returns the angle in radians;powf(x, p)raisesxtop.dot(a, b)returns the inner product;amax(v)returns the largest absolute element;norm(v)returns the Euclidean length. All three return the array’s element type.cross(a, b)returns the cross producta × bof two 3-element arrays. Any other length is a compile error.
Builtins require no import. They also accept the qualified form
core::min(a, b). Other cross-module function calls require a use import.
Decorators
Section titled “Decorators”Decorators precede the step declaration they annotate. The supported
decorators are @probe and @every.
model Filter {}
@probe(u, previous)@every(10ms)step Filter(u: f64) -> (y: f64) { let previous: f64 = 0.0 fby u; y = (u + previous) / 2.0;}Probes
Section titled “Probes”@probe(name, ...) records named inputs, outputs, or locals in simulation
telemetry. Names must exist in the annotated step; @probe() selects none.
Arrays and composites produce one column per scalar leaf. Child telemetry
is qualified by its instance path, such as probe.filter.previous.
Probes do not change the model’s outputs or state. Production builds omit
telemetry. On an extern step, probes may record only inputs and outputs.
Execution period
Section titled “Execution period”@every(duration) declares a positive period. A duration is an integer followed
by s, ms, us, or ns: write 1500us for 1.5 milliseconds. Frequencies,
fractional literals, and named constants are not accepted here.
- An unrated model inherits its caller’s period.
- A rated child’s period must be an integer multiple of its parent’s period. It cannot run faster than the parent.
- Rated children fire from tick 0, sample inputs when they fire, and hold outputs and probes between firings. Their internal state advances only on firing ticks.
- Inside a rated step,
dtis its declared period in seconds. - A rated simulation entry fixes the run’s sample time. An unrated entry requires
--dt, which must divide the periods of rated descendants on that base clock.
Rated instances cannot be inline or called inside a match arm.