Algebraic Data Types¶
Every body-bearing type declaration in graphcal defines one nominal
algebraic data type with one or more constructors. Constructor count does
not create separate type categories: one-constructor record-shaped data and
multiple-constructor alternatives use the same declaration and type semantics.
Constructors¶
A type lists its constructors inside the braced body. Each constructor has an optional parenthesized payload or is a bare unit constructor. Braces delimit the type body, not an alternate payload form:
type ManeuverKind {
Impulsive(delta_v: Velocity),
LowThrust(thrust: Force, duration: Time),
Coast,
}
The outer braces are the declaration's member body, so type T { ... } uses neither = nor a trailing semicolon. This differs intentionally from named RHS definitions such as index I = { ... };, which use = rhs; even when the RHS is braced.
Constructors live in the Term namespace, which is distinct from the type
namespace — a single lexeme can name both a type and a constructor without
ambiguity. A constructor may not reuse the spelling of a built-in Term, such
as the constants PI, E, and TAU, built-in functions such as sum, or
contextual callables such as scan; the built-in owns that spelling regardless
of whether the constructor has payload fields. Time scales such as UTC are
Static atoms rather than Terms, so they may be reused as constructor spellings.
Payload field names must be unique within one constructor. The same field name may still appear in different constructors because each payload schema is checked independently.
Values retain both their canonical nominal type and their constructor member.
Equality and match use these identities, not display names: changing an import
alias does not change identity, and identically spelled constructors from
different nominal types remain distinct.
Record-Shaped One-Constructor Types¶
A type is record-shaped when it has exactly one constructor and that constructor has the same name as the type:
type TransferResult {
TransferResult(dv1: Velocity, dv2: Velocity, total_dv: Velocity, tof: Time),
}
Construction¶
Construction is always a constructor call — parens with named args:
node result: TransferResult = TransferResult(
dv1: 100.0 m/s,
dv2: 200.0 m/s,
total_dv: 300.0 m/s,
tof: 3600.0 s,
);
Constructor fields must always specify the field name and value. Graph
nodes are referenced explicitly with @:
node dv1: Velocity = 100.0 m/s;
node result: TransferResult =
TransferResult(dv1: @dv1, dv2: 200.0 m/s, total_dv: 300.0 m/s, tof: 3600.0 s);
Field Access¶
Field access works on a record-shaped value because there is exactly one constructor and its name matches the type, so the payload field set is unambiguous:
For types with multiple constructors, or one constructor whose name differs
from the type name, field access is rejected — destructure through match
instead.
Unit Markers¶
A unit marker is a one-constructor type whose constructor takes no payload:
Unit markers are useful as phantom type parameters (e.g., reference frames).
Note:
type T;(semicolon, no body) is not a unit marker — it declares a required type that importers must bind. See Multi-File Projects → Visibility, Bindability, and Input Ports.
Constructing Algebraic Values¶
Construct a variant by its constructor name. If another module exports a
same-named constructor, qualify the constructor with the module alias (for
example, rocket::LowThrust(...)). Payload constructors use parentheses with
named arguments. Unit constructors are fieldless and must omit parentheses;
Coast() is a compile error (S010):
node maneuver: ManeuverKind = LowThrust(thrust: 0.5 N, duration: 3600.0 s);
node coast: ManeuverKind = Coast;
Match Expressions¶
Use match to destructure algebraic values. match is reserved for
exhaustive case analysis over closed constructors; use if for ordinary
boolean predicates and comparisons.
node fuel_proxy: Force = match @maneuver {
Impulsive(delta_v: _) => 0.0 N,
LowThrust(thrust: thrust, duration: _) => thrust,
};
- Each arm uses a constructor pattern (bare or module-qualified) and binds its fields
_discards a field value- For constructors with payload fields, every field binding must be explicit:
field: variableorfield: _; omitting the binding list is not allowed - Unit constructors have no payload and must use the fieldless spelling, such as
Coast;Coast()is a compile error (S010) - Named-index labels can also be matched exhaustively with qualified, fieldless patterns such as
Maneuver#Departureormission::Maneuver#Departure
Exhaustiveness Checking¶
The compiler requires that all constructors are covered:
type Status {
Nominal,
Warning(code: Dimensionless),
}
// ERROR: non-exhaustive -- missing `Warning` arm
node code: Dimensionless = match @status {
Nominal => 0.0,
};
All Arms Must Agree¶
All match arms must produce the same type and dimension:
// ERROR: arms have different dimensions (Force vs Velocity)
node bad: Force = match @maneuver {
Impulsive(delta_v: delta_v) => delta_v, // Velocity
LowThrust(thrust: thrust, duration: _) => thrust, // Force
};
Generic Types¶
Types can have sort-aware generic parameters for dimensions, value types, indexes, and type-level natural numbers. Parameters may be used in payload field types or retained only for phantom distinctions:
Changing a Phantom Type Parameter¶
There is no phantom-type cast operator. To change a phantom type parameter (for example, to re-label a reference frame), construct a new instance and assign each field explicitly:
node pos_eci: Vec3<Length, Eci> = Vec3<Length, Eci>(x: 7000.0 km, y: 0.0 km, z: 0.0 km);
node pos_body: Vec3<Length, Body> = Vec3<Length, Body>(
x: @pos_eci.x,
y: @pos_eci.y,
z: @pos_eci.z,
);
The verbosity is intentional: a re-labeling is a deliberate, field-by-field act, visible at the call site — not a silent reinterpretation of opaque data.
Generic Defaults and Nat Arguments¶
Defaults are checked against the parameter's declared sort, must form a trailing suffix, and may refer only to earlier parameters. Type annotations and constructors share the same argument syntax:
type Unframed { Unframed }
type Vec3<D: Dim, F: Type = Unframed> {
Vec3(x: D, y: D, z: D),
}
// Equivalent to Vec3<Length, Unframed>
node pos: Vec3<Length> = Vec3<Length>(x: 1.0 m, y: 2.0 m, z: 3.0 m);
type Buffer<N: Nat = 3> {
Buffer(value: Dimensionless),
}
param buffer: Buffer<3> = Buffer<3>(value: 1.0);
A Nat argument may use literals, in-scope Nat parameters, +, and *.
Subtraction is deliberately unsupported; express the larger side additively,
such as Input<N + 1> and Output<N>. Nat and Index arguments are never
implicitly converted into one another.