Skip to content

Modules and packages

Each .mag file is a module. It may declare one model, named after the file: motor.mag declares Motor; speed_loop.mag declares SpeedLoop. A module containing only functions, constants, or types needs no model.

Sources live beside Magnet.toml or under src/. Each blocks/<name>/ directory is a separate package named <name>, with no manifest entry needed. Directories under src/ form the module path. For example, src/control/speed_loop.mag is imported as:

use crate::control::speed_loop::SpeedLoop;
  • crate refers to the current package; a dependency name refers to that package.
  • Imports bind individual items. Group imports use use crate::values::{GAIN, LIMIT};. An item is one of:
    • a model
    • a function
    • a constant
    • a composite
    • an enum
  • Each file declares its own imports. Calls use the imported name, such as clamp_value(x), rather than a qualified path.
  • Import cycles are allowed. These recursions are rejected:
    • model containment
    • function calls
    • composite types
    • constant definitions
  • main.mag and lib.mag are reserved. The package root declares no items, except in a single-file dependency, whose items import as use util::clamp_value;.

Declare a child as a model field and call it with self.name(inputs). Each field has independent state. This auto.mag uses the overview’s Motor:

use crate::motor::Motor;
model Auto {
left: Motor,
right: Motor,
}
step Auto(a: f64, b: f64) -> (left_speed: f64, right_speed: f64) {
left_speed = self.left(a);
right_speed = self.right(b);
}
params Auto(gain: f64 = 2.0) {
Self { left: Motor(gain), right: Motor(gain) }
}

Calls pass inputs in declaration order. A single output is a value; multiple outputs are bound with let (a, b) = self.child(x);. Use let () = self.child(); for a call with no outputs.

An instance may be stepped at most once on each execution path through a tick. A child call must be the whole right-hand side of a binding or assignment; bind its result before using it in arithmetic.

model fields declare storage; params declares the constructor’s arguments and initializes that storage. Argument names need not match field names.

// Alternative constructor for Motor.
params Motor(scale: f64 = 1.0, tau: f64 = 0.2) {
let k = 2.0 * scale;
Self { k, tau }
}
  • Arguments require types. = value supplies a default; an argument without a default is required.
  • Defaults may reference constants, but not other arguments. Body let bindings can read arguments and earlier bindings.
  • The body ends with Self { ... }, initializing every model field exactly once. Self { k } is shorthand for Self { k: k }.
  • Values follow the const expression rules. Child calls such as Motor(2.0) pass positional constructor arguments; omitted trailing arguments require defaults.
  • Without a params item, the model has an implicit constructor over its fields: data fields first, then child fields, in declaration order within each group. Data fields are required; a child field defaults to the child’s empty constructor call if all of that child’s arguments have defaults.

At simulation entry, --param gain=3.0 supplies or overrides the selected model’s constructor argument. Child arguments are set by the parent constructor: --param left.k=3.0 is rejected. Missing required arguments prevent the run.

The former params Name { ... } syntax and pub field modifier are not accepted.

Inside a constructor, a model literal sets stored fields directly:

let motor = Motor { k: 2.0, tau: 0.1 };
let faster = Motor { tau: 0.05, ..motor };

Motor(...) takes constructor arguments; Motor { ... } takes model field names. A literal must supply every field, or end with ..base to copy omitted fields from a value of the same model type. ..Motor() uses the constructor’s defaults. Bare .. is invalid, and the final Self { ... } cannot use a base.

A package groups .mag modules under a Magnet.toml manifest. A library uses the same layout as an application; no separate library declaration is required.

control-lib/
├── Magnet.toml
└── src/
├── gain.mag
└── math.mag

Declare dependencies in the consuming package’s manifest:

[package]
name = "plant"
[dependencies]
control-lib = { path = "../control-lib" }

Import the package, module, and item explicitly:

use control_lib::gain::Gain;
use control_lib::math::{deadband, saturate};
  • The dependency key supplies the import name. Hyphens become underscores.
  • A path may point to a package directory or a single .mag file. Directory dependencies load modules beside the manifest and recursively under src/.
  • Git dependencies use git with at most one of tag, rev, or branch. See manifest settings for syntax.
  • The root manifest must declare the required DSL dependencies; dependency manifests do not add transitive DSL dependencies.
  • core is the embedded standard library and needs no manifest entry, for example use core::integrator::Integrator;.

Imported models use the same constructor and call rules as local models. Imported types retain their identity: declaring another composite with identical fields does not create the same type.

Use magnet add, remove, and update to manage dependencies. Rust implementations use the external declaration boundary.