Skip to content

Packaging and libraries

Reusable magscript goes in its own package, wired up through the manifest. This guide builds a small library, consumes it, and shares it by repository. It assumes a project from the quickstart.

A dependency comes from a directory on disk (path) or from a repository (git). A path may point at a package directory or at a single .mag file:

[package]
name = "rig"
[dependencies]
control_lib = { path = "../control_lib" }

fn declares a pure function: stateless and never recursive. A library of them in control_lib/util.mag:

fn saturate(val: f64, lo: f64, hi: f64) -> f64 {
if val < lo { lo } else if val > hi { hi } else { val }
}
fn deadband(val: f64, width: f64) -> f64 {
if abs(val) < width { 0.0 } else { val }
}

Every call needs a use, because there are no inline qualified calls:

use core::pid::Pid;
use control_lib::util::{saturate, deadband};
model Rig {
pid: Pid,
}
step Rig(e: f64) -> (u: f64) {
let cleaned = deadband(e, 0.05);
let raw = self.pid(cleaned);
u = saturate(raw, -10.0, 10.0);
}
Terminal window
$ magnet check
Checking rig
ok 1 model (Rig), 0 functions

check counts what this package declares. Pid, saturate and deadband are compiled in but belong to other packages. Only what the entry step reaches is compiled, so a large library costs nothing.

A use binds in the module that writes it, so every file states what it reaches for. A sibling module of the same package is reached through crate, as in use crate::pid::Pid;.

Each module carries its own params, so a library module runs on its own. magnet simulate src/pid.mag runs Pid alone, and magnet simulate src/rig.mag runs Rig and builds its Pid from the call params Rig writes, pid: Pid(2.0): the arguments passed there, and Pid’s own defaults for the ones the call stops before.

A const is an item, so use reaches it like any other. A module of constants suits values a package states more than once, or values a script computes:

// src/values.mag, written by tools/filter.py
const CUTOFF_HZ: f64 = 12.5;
const SOC_MIN: f64 = 0.0;
const SOC_MAX: f64 = 1.0;

A constant’s value is an expression with arithmetic between any of these:

  • numbers
  • bools
  • math constants
  • other constants
  • arrays
  • composite literals

For example:

const RADIUS_M: f64 = 0.15;
const SPEED_MPS: f64 = 3.6;
const OMEGA_RADS: f64 = SPEED_MPS / RADIUS_M;

What a constant’s expression may contain, and how it is typed, is in the reference.

A constructor takes expressions too, so a call’s argument can be a derivation. The bounds every cell shares are stated once, as constants, and a value the command line should reach is an argument of the constructor instead, like ic here:

use crate::values::{CUTOFF_HZ, SOC_MAX, SOC_MIN};
params Rig(ic: f64 = 0.5) {
Self {
filter: LowPass(CUTOFF_HZ / 2.0),
soc: Integrator(0.6, SOC_MIN, SOC_MAX),
soc_estimate: Integrator(ic, SOC_MIN, SOC_MAX),
}
}

To share a library across a team or projects, point the manifest at its repository. The two functions above are published as a public package:

[dependencies]
magnet-tutorial = { git = "https://codeberg.org/AstraLabs/magnet-tutorial.git", tag = "v1.1" }

The key is the import name, with hyphens folded to underscores:

use magnet_tutorial::util::{saturate, deadband};

magnet checks the repository out under .magnet/deps/ on first use and works from there. Only the first run fetches, and later builds work offline.

Terminal window
$ magnet check
Fetching magnet-tutorial (https://codeberg.org/AstraLabs/magnet-tutorial.git)
Checking rig
ok 1 model (Rig), 0 functions

magnet add writes the same manifest line, and magnet remove takes it and the checkout away. The fetch runs before the manifest is written, so an unreachable repository leaves it unchanged.

A dependency takes at most one of tag, rev or branch:

Selector Behaviour
tag = "v1.0" Reproducible. Prefer this.
rev = "a1b2c3d" Reproducible and exact.
branch = "main" Moving. Frozen at its first resolved commit.
none The default branch. Also moving and frozen.

A tag or rev is checked against the checkout on every build, so editing the manifest is how you change versions. A branch resolves once and holds, and every build says so:

Terminal window
$ magnet check
warning: dependency `magnet-tutorial` tracks branch `main` and is frozen at 9fddb00 — run `magnet update magnet-tutorial` to move it, or pin a `tag` or `rev` for reproducible builds

Move it on request:

Terminal window
$ magnet update
Fetching magnet-tutorial (https://codeberg.org/AstraLabs/magnet-tutorial.git)
Updated magnet-tutorial 9fddb00 -> 97c7705

core is the standard library, written in magscript and shipped inside the compiler. Its builtins and constants are in the reference; in short, it provides four kinds of names:

  1. Stateful blocks such as UnitDelay, Integrator and Pid, declared as instance fields and stepped like your own models. A stateless block such as a gain is just its expression.
  2. Builtins, always in scope: min, max, abs, limit, the transcendentals from sin to powf, the array reductions dot, amax and norm, and the 3-vector product cross.
  3. Constants, always in scope and spelled as Rust spells them: PI, TAU, E, SQRT_2 and the rest of core::f64::consts.
  4. Element-wise lifting. Given an array, a builtin applies per element, broadcasting scalar arguments, as in limit(v, -1.0, 1.0).