Skip to content

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:

node total: Velocity = @result.total_dv;
node time_hours: Time = @result.tof -> h;

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:

type Eci { Eci }
type Body { Body }
type Coasting { Coasting }

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: variable or field: _
  • Named-index labels can also be matched exhaustively with qualified, fieldless patterns such as Maneuver.Departure or mission.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:

type Eci { Eci }
type Body { Body }

type Vec3<D: Dim, F: Type> {
    Vec3(x: D, y: D, z: D),
}

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.