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 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:

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

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:

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(...)). 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: variable or field: _; 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#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.