Skip to content

Magscript

Magscript is Magnetite’s synchronous dataflow language. Block diagrams use the same source format. A .mag file describes a model’s:

  • parameters
  • inputs
  • outputs
  • equations

Save this as motor.mag:

model Motor {
k: f64,
tau: f64,
}
step Motor(throttle: f64) -> (omega: f64) {
let omega_dot = (self.k * throttle - omega) / self.tau;
omega = 0.0 fby omega + omega_dot * dt;
}
params Motor(k: f64 = 1.0, tau: f64 = 0.2) {
Self { k, tau }
}
  • model declares stored parameters and child model instances.
  • params constructs those fields from typed arguments and defaults.
  • step declares named ports and the equations evaluated on each tick.
  • self.k reads a parameter, and self.child.k a child’s; dt is the step duration in seconds.
  • initial fby next supplies initial on the first tick, then the previous tick’s next value.

Equations describe dependencies, not execution order. Each output is assigned once per tick; the compiler schedules equations and rejects feedback without a delay.

Magscript uses:

  • commas between fields and arguments
  • braces for blocks
  • semicolons after bindings and assignments

Whitespace does not affect meaning; // starts a comment that continues to the end of the line.

controller.mag
enum Mode { Idle, Running }
model Controller { gain: f64, mode: Mode }
step Controller(u: f64) -> (y: f64) {
let scaled: f64 = self.gain * u;
y = scaled;
}
fn twice(x: f64) -> f64 { x * 2.0 }
params Controller(gain: f64 = 1.0) {
Self { gain, mode: Mode::Idle }
}
  • let x = expression; binds a local whose type is its value’s. let x: T = expression; states the type, and the value must have it. Equations may be written in any order, annotated or not: a local may be read before the line that binds it.
  • Locals that read one another in a loop take their type from where the loop is entered: an input, or a typed local. A loop nothing typed enters is an error that lists every local in it; annotate one of them.
  • y = expression; assigns a declared output. Each output must be assigned once; assignment does not mutate an earlier binding.
  • let (a, b) = self.child(x); binds multiple child outputs. (a, b) = ...; assigns existing output targets.
  • if condition { a } else { b } is a value expression. The condition must be bool; both branches must return the same type. else is required.
  • Expression blocks contain local let bindings and a final value without a semicolon. Step bodies contain equations and have no return expression.

Identifiers are case-sensitive letters, digits, and underscores, beginning with a letter or underscore. Numbers use decimal notation, optionally with a fraction or exponent (1.0, 1e-3). Radix prefixes, digit separators, and type suffixes are unsupported; specify the type on a declaration.

Binary operators at the same level associate left to right, except fby, which associates right to left. Prefix operators apply right to left.

Precedence (high to low) Operators
1 postfix call, field access, indexing
2 unary -
3 *, /, .*, ./, %
4 +, -
5 >, <, >=, <=, ==, !=
6 unary not
7 and, xor, nand, nor (one level)
8 or
9 fby (right associative)

Parentheses override precedence. Array literals use [a, b]; type arrays use [T; N]. See types and state.

The rest of the reference: