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 a namespace that is distinct from the type namespace — a single lexeme can name both a type and a constructor without ambiguity.
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, so the payload field set is unambiguous:
For types with multiple constructors, 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(...)). The parens-with-named-args form is the
canonical syntax:
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- Each field binding must be explicit:
field: variableorfield: _ - 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.