Skip to main content

graphcal_compiler/syntax/ast/
value.rs

1use std::marker::PhantomData;
2
3use crate::syntax::ast::common::{Ident, ModulePath};
4use crate::syntax::dimension::Rational;
5use crate::syntax::names::{
6    ConstructorName, DeclName, FieldName, IndexName, IndexVariantName, LocalName, NamePath,
7    ScopedName, UnitRef,
8};
9use crate::syntax::non_empty::NonEmpty;
10use crate::syntax::phase::{Phase, Raw};
11use crate::syntax::span::{Span, Spanned};
12
13/// Expression-level sugar — only legal in [`Raw`].
14///
15/// Each variant corresponds to a surface expression form that is rewritten
16/// into ordinary `ExprKind` variants by [`crate::desugar::convert`]. In
17/// `Desugared`, the `Sugar` slot is `Infallible` and these variants vanish.
18#[derive(Debug, Clone)]
19pub enum RawExprSugar {
20    /// Table literal: `table[Phase, 3] { ... }`.
21    ///
22    /// Desugars to [`ExprKind::MapLiteral`] — the `indexes` metadata is
23    /// dropped (entries already carry full `Index.Variant` keys), and the
24    /// `table` keyword is purely surface syntax preserved by the formatter
25    /// via the raw AST.
26    TableLiteral {
27        indexes: Vec<TableIndexSpec>,
28        entries: Vec<MapEntry<Raw>>,
29    },
30}
31
32// ---------------------------------------------------------------------------
33// Unresolved-ref variants (the only reference form in the syntax AST)
34// ---------------------------------------------------------------------------
35
36/// Unresolved reference, produced by the parser before HIR lowering.
37///
38/// Carried by `ExprKind::UnresolvedRef`. The parser emits these
39/// when the meaning of an identifier path cannot be determined from syntax
40/// alone; HIR expression lowering ([`crate::hir::lower_expr`]) classifies and
41/// resolves them in a single pass against the lexical scope and the
42/// module-aware resolver.
43///
44/// This indirection is necessary because the same token shape can mean
45/// different expression kinds depending on declarations and local scopes. For
46/// example, the dotted expression `Foo.Bar` is parsed as the unresolved path
47/// `Foo.Bar` in both of these programs:
48///
49/// ```graphcal
50/// index Foo = { Bar };
51/// node x: Dimensionless = Foo.Bar;
52/// ```
53///
54/// and:
55///
56/// ```graphcal
57/// node x: Dimensionless = Foo.Bar;
58/// ```
59///
60/// Only after collecting names from the file can resolution know whether
61/// `Foo` is an index. In the first program `Foo.Bar` becomes a HIR variant
62/// literal; in the second it becomes a qualified constant-like reference.
63///
64/// Bare identifiers have the same issue. `PI` parses as the unresolved path
65/// `PI` both when it denotes the built-in constant:
66///
67/// ```graphcal
68/// node x: Dimensionless = PI;
69/// ```
70///
71/// and when a local binding shadows that constant:
72///
73/// ```graphcal
74/// index I = { A };
75/// node x: Dimensionless[I] = for PI: I { PI };
76/// ```
77///
78/// HIR lowering turns the first `PI` into a built-in constant reference,
79/// but the loop body `PI` in the second program into a local reference.
80///
81/// The payload is a path rather than separate "bare" and "qualified" variants
82/// so the parser records the complete syntactic structure uniformly:
83/// `Foo`, `Foo.Bar`, and `Foo.Bar.Baz` are all identifier paths. Segment-count
84/// restrictions, such as index variants currently being two-segment paths, are
85/// semantic rules enforced by HIR lowering rather than parser artifacts.
86#[derive(Debug, Clone)]
87pub enum UnresolvedRef {
88    /// Unresolved identifier path: `Foo`, `Foo.Bar`, or `Foo.Bar.Baz`.
89    Path(IdentPath),
90}
91
92/// A non-empty dot-separated identifier path in expression position.
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub struct IdentPath {
95    pub segments: NonEmpty<Ident>,
96}
97
98impl IdentPath {
99    /// Construct a path from already-tokenized segments.
100    #[must_use]
101    pub const fn new(segments: NonEmpty<Ident>) -> Self {
102        Self { segments }
103    }
104
105    /// Construct a one-segment path from an identifier.
106    #[must_use]
107    pub fn bare(ident: Ident) -> Self {
108        Self::new(NonEmpty::singleton(ident))
109    }
110
111    /// Borrow all path segments in source order.
112    #[must_use]
113    pub fn segments(&self) -> &[Ident] {
114        self.segments.as_slice()
115    }
116
117    /// Consume and return the non-empty segment sequence.
118    #[must_use]
119    pub fn into_segments(self) -> NonEmpty<Ident> {
120        self.segments
121    }
122
123    /// Consume and return the segment vector.
124    #[must_use]
125    pub fn into_vec(self) -> Vec<Ident> {
126        self.segments.into_vec()
127    }
128
129    /// Number of path segments. Always at least 1.
130    #[must_use]
131    pub const fn len(&self) -> usize {
132        self.segments.len()
133    }
134
135    /// Returns `false`; provided for API compatibility with sequence-like code.
136    #[must_use]
137    pub const fn is_empty(&self) -> bool {
138        false
139    }
140
141    /// Returns whether this is a one-segment identifier path.
142    #[must_use]
143    pub const fn is_bare(&self) -> bool {
144        self.segments.len() == 1
145    }
146
147    /// Returns the source span covering the whole path.
148    #[must_use]
149    pub fn span(&self) -> Span {
150        self.segments.first().span.merge(self.segments.last().span)
151    }
152
153    /// The written path with per-segment spans dropped.
154    ///
155    /// Use this for span-independent written identity (semantic metadata
156    /// keys); keep the `IdentPath` itself when diagnostics need segment spans.
157    #[must_use]
158    pub fn to_name_path(&self) -> crate::syntax::names::NamePath {
159        crate::syntax::names::NamePath::new(self.segments.clone().map(|ident| ident.name))
160    }
161
162    /// Returns the leaf segment of the path.
163    #[must_use]
164    pub fn leaf(&self) -> &Ident {
165        self.segments.last()
166    }
167
168    /// Split the path into qualifier segments and the leaf segment.
169    ///
170    /// The qualifier slice is empty for one-segment paths.
171    #[must_use]
172    pub fn split_last(&self) -> (&[Ident], &Ident) {
173        let (leaf, qualifier) = self.segments.split_last();
174        (qualifier, leaf)
175    }
176
177    /// Returns the qualifier segments before the leaf. Empty for bare paths.
178    #[must_use]
179    pub fn qualifier_segments(&self) -> &[Ident] {
180        self.split_last().0
181    }
182
183    /// Returns qualifier segments and leaf only when this path is qualified.
184    #[must_use]
185    pub fn qualifier_and_leaf(&self) -> Option<(&[Ident], &Ident)> {
186        let (qualifier, leaf) = self.split_last();
187        (!qualifier.is_empty()).then_some((qualifier, leaf))
188    }
189
190    /// Returns the only segment when this is a bare identifier path.
191    #[must_use]
192    pub fn as_bare(&self) -> Option<&Ident> {
193        match self.segments.as_slice() {
194            [ident] => Some(ident),
195            _ => None,
196        }
197    }
198
199    /// Mutably returns the only segment when this is a bare identifier path.
200    pub fn as_bare_mut(&mut self) -> Option<&mut Ident> {
201        match self.segments.as_mut_slice() {
202            [ident] => Some(ident),
203            _ => None,
204        }
205    }
206
207    /// Consume this path and return its segment when it is bare.
208    ///
209    /// Returns the original path unchanged when it is qualified.
210    pub fn into_bare(self) -> Result<Ident, Self> {
211        if self.is_bare() {
212            let mut segments = self.segments.into_vec();
213            Ok(segments.remove(0))
214        } else {
215            Err(self)
216        }
217    }
218
219    /// Convert this spanned syntax path into a span-less [`NamePath`].
220    #[must_use]
221    pub fn into_name_path(self) -> NamePath {
222        NamePath::new(self.segments.map(|ident| ident.name))
223    }
224
225    /// Convert this syntax path into a [`NamePath`] paired with the path's full span.
226    #[must_use]
227    pub fn into_spanned_name_path(self) -> Spanned<NamePath> {
228        let span = self.span();
229        Spanned::new(self.into_name_path(), span)
230    }
231
232    /// Human-readable path string for diagnostics and formatting boundaries.
233    #[must_use]
234    pub fn display_path(&self) -> String {
235        self.segments
236            .iter()
237            .map(|segment| segment.name.as_str())
238            .collect::<Vec<_>>()
239            .join(".")
240    }
241}
242
243impl From<Ident> for IdentPath {
244    fn from(ident: Ident) -> Self {
245        Self::bare(ident)
246    }
247}
248
249impl std::fmt::Display for IdentPath {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        for (idx, segment) in self.segments.iter().enumerate() {
252            if idx > 0 {
253                f.write_str(".")?;
254            }
255            f.write_str(segment.name.as_str())?;
256        }
257        Ok(())
258    }
259}
260
261impl UnresolvedRef {
262    /// Returns the source span of the underlying identifier path.
263    #[must_use]
264    pub fn span(&self) -> Span {
265        match self {
266            Self::Path(path) => path.span(),
267        }
268    }
269}
270/// A param binding in a module instantiation: `name: expr`.
271///
272/// Used in `include "path"(name: expr, ...) { ... };`
273#[derive(Debug, Clone)]
274pub struct ParamBinding<P: Phase = Raw> {
275    /// The param name in the imported file.
276    pub name: Ident,
277    /// The value expression (evaluated in the importer's scope).
278    pub value: Expr<P>,
279    /// Span covering the entire `name: expr`.
280    pub span: Span,
281}
282/// The kind of a domain constraint bound: `min` or `max`.
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum DomainBoundKind {
285    Min,
286    Max,
287}
288
289impl std::fmt::Display for DomainBoundKind {
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        match self {
292            Self::Min => write!(f, "min"),
293            Self::Max => write!(f, "max"),
294        }
295    }
296}
297
298/// A domain constraint bound on a type expression: `min: expr` or `max: expr`.
299///
300/// Used in `Type(min: 100 kg, max: 2000 kg)` to declare valid value ranges.
301#[derive(Debug, Clone)]
302pub struct DomainBound<P: Phase = Raw> {
303    /// The bound kind (`min` or `max`).
304    pub kind: DomainBoundKind,
305    /// The span of the keyword (`min` or `max`).
306    pub kind_span: Span,
307    /// The bound value expression.
308    pub value: Expr<P>,
309    pub span: Span,
310}
311
312/// A type expression (dimension annotation on declarations).
313/// An expression in index position of an indexed type.
314///
315/// In `Velocity[Maneuver]` or `Velocity[module.Maneuver]`, the index path is
316/// an `IndexExpr::Name`.
317/// In `Dimensionless[3, 4]`, `3` and `4` are `IndexExpr::NatExpr(NatExpr::Literal(..))`.
318/// In `D[N + 1]`, `N + 1` is an `IndexExpr::NatExpr`.
319#[derive(Debug, Clone)]
320pub enum IndexExpr {
321    /// A named index or generic parameter path: `Maneuver`, `I`, `N`, `module.Maneuver`
322    Name(Spanned<NamePath>),
323    /// A type-level natural-number expression in index position: `3`, `N + 1`, `M + N`.
324    NatExpr(NatExpr),
325}
326
327impl IndexExpr {
328    /// Get the source span of this index expression.
329    #[must_use]
330    pub const fn span(&self) -> Span {
331        match self {
332            Self::Name(name) => name.span,
333            Self::NatExpr(nat_expr) => nat_expr.span(),
334        }
335    }
336}
337
338/// E.g., `Length`, `Dimensionless`, `Length^3 / Time^2`
339///
340/// Optionally carries domain constraints: `Mass(min: 100 kg, max: 2000 kg)`.
341#[derive(Debug, Clone)]
342pub struct TypeExpr<P: Phase = Raw> {
343    pub kind: TypeExprKind<P>,
344    /// Optional domain constraints on the type.
345    pub constraints: Vec<DomainBound<P>>,
346    pub span: Span,
347}
348
349impl<P: Phase> TypeExpr<P> {
350    /// The domain bounds attached to this type expression.
351    ///
352    /// Bounds on an indexed type (`Velocity[Maneuver](min: 0.0 m/s)`) are
353    /// parsed onto the base type expression, so this looks through one
354    /// `Indexed` wrapper when the outer expression carries none.
355    #[must_use]
356    pub fn domain_bounds(&self) -> &[DomainBound<P>] {
357        if !self.constraints.is_empty() {
358            return &self.constraints;
359        }
360        match &self.kind {
361            TypeExprKind::Indexed { base, .. } => &base.constraints,
362            _ => &[],
363        }
364    }
365}
366
367/// The kind of a type expression.
368#[derive(Debug, Clone)]
369pub enum TypeExprKind<P: Phase = Raw> {
370    /// `Dimensionless`
371    Dimensionless,
372    /// `Bool`
373    Bool,
374    /// `Int`
375    Int,
376    /// `Datetime` (bare, without time scale parameter — defaults to UTC)
377    Datetime,
378    /// `Datetime<TimeScale>` — built-in datetime type parameterized by a time
379    /// scale. Kept separate from [`Self::TypeApplication`] so downstream
380    /// resolution dispatches on the variant rather than string-matching the
381    /// built-in name.
382    DatetimeApplication { type_args: Vec<TypeExpr<P>> },
383    /// A dimension expression like `Length`, `Length^2`, `Mass * Length / Time^2`
384    DimExpr(DimExpr),
385    /// An indexed type like `Velocity[Maneuver]`, `Dimensionless[3, 4]`, or `D[M, N]`
386    Indexed {
387        base: Box<TypeExpr<P>>,
388        indexes: Vec<IndexExpr>,
389    },
390    /// A user-defined generic type application like `Vec3<Length, ECI>`.
391    /// Built-in parameterized types (currently only `Datetime<...>`) have their
392    /// own variants instead — see [`Self::DatetimeApplication`].
393    TypeApplication {
394        name: Spanned<NamePath>,
395        type_args: Vec<TypeExpr<P>>,
396    },
397}
398
399/// A dimension expression: product/quotient of dimension terms.
400/// E.g., `Length^3 / Time^2`
401#[derive(Debug, Clone)]
402pub struct DimExpr {
403    pub terms: Vec<DimExprItem>,
404    pub span: Span,
405}
406
407/// One term in a dimension expression with its combining operator.
408#[derive(Debug, Clone)]
409pub struct DimExprItem {
410    /// `Mul` for the first term and for `*`, `Div` for `/`.
411    pub op: MulDivOp,
412    pub term: DimTerm,
413}
414
415/// A single dimension term: `ident_path` or `ident_path ^ INTEGER`
416#[derive(Debug, Clone)]
417pub struct DimTerm {
418    pub name: Spanned<NamePath>,
419    /// `None` means exponent 1. Rational exponents (`^(1/2)`) are kept exact.
420    pub power: Option<Rational>,
421    pub span: Span,
422}
423
424// --- Unit expressions ---
425
426/// A unit expression (for literals and conversion targets).
427/// E.g., `km`, `m/s^2`, `kg * m / s^2`
428#[derive(Debug, Clone)]
429pub struct UnitExpr {
430    pub terms: Vec<UnitExprItem>,
431    pub span: Span,
432}
433
434/// One term in a unit expression.
435#[derive(Debug, Clone)]
436pub struct UnitExprItem {
437    /// `Mul` for the first term and for `*`, `Div` for `/`.
438    pub op: MulDivOp,
439    pub name: Spanned<UnitRef>,
440    /// `None` means exponent 1. Rational exponents (`^(1/2)`) are kept exact.
441    pub power: Option<Rational>,
442}
443
444/// Multiply or divide operator used in dimension/unit expressions.
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub enum MulDivOp {
447    Mul,
448    Div,
449}
450
451// --- Expressions ---
452
453/// An expression node.
454///
455/// Construct via [`Expr::new`] — direct struct literal syntax is blocked
456/// by the private phase marker.
457#[derive(Debug)]
458pub struct Expr<P: Phase = Raw> {
459    pub kind: ExprKind<P>,
460    pub span: Span,
461    // Marker forcing a concrete (non-recursive) use of `P` so the compiler
462    // can determine variance for `Expr<P>` and, transitively, every type
463    // that contains `Expr<P>`. Private so callers must use `Expr::new` —
464    // that keeps the phase marker out of their sight entirely.
465    _phase: PhantomData<fn() -> P>,
466}
467
468// Manual impl instead of `#[derive(Clone)]`: derived clone glue recurses
469// once per tree level without any stack-growth guard, so cloning a long
470// left-nested operator chain overflows the stack. Routing each level
471// through `with_stack_growth` lets the stack grow on demand (the derived
472// `ExprKind` clone calls back into this impl through `Box<Expr<P>>`).
473impl<P: Phase> Clone for Expr<P>
474where
475    ExprKind<P>: Clone,
476{
477    fn clone(&self) -> Self {
478        crate::stack::with_stack_growth(|| Self {
479            kind: self.kind.clone(),
480            span: self.span,
481            _phase: PhantomData,
482        })
483    }
484}
485
486impl<P: Phase> Expr<P> {
487    /// Construct an expression with the given kind and span.
488    #[must_use]
489    pub const fn new(kind: ExprKind<P>, span: Span) -> Self {
490        Self {
491            kind,
492            span,
493            _phase: PhantomData,
494        }
495    }
496}
497
498#[derive(Debug, Clone)]
499pub enum ExprKind<P: Phase = Raw> {
500    /// Numeric literal: `1200.0`, `3.98e5`, `200_000.0`
501    Number(f64),
502    /// Integer literal: `42`, `1_000`
503    Integer(i64),
504    /// Boolean literal: `true`, `false`
505    Bool(bool),
506    /// String literal: `"hello"` (used as arguments to `datetime()`, `epoch()`, etc.)
507    StringLiteral(String),
508    /// Graph reference: `@name` or `@alias.member`. The payload encodes
509    /// qualification structurally — `Local` for bare `@name`, `Qualified`
510    /// for `@alias.member` (after the namespace-alias rewrite). Producers
511    /// never invent or interpret a flat-string separator.
512    GraphRef(Spanned<ScopedName>),
513    /// Binary operation: `a + b`, `a * b`, `a ^ b`, `a && b`, etc.
514    BinOp {
515        op: BinOp,
516        lhs: Box<Expr<P>>,
517        rhs: Box<Expr<P>>,
518    },
519    /// Unary operation: `-x`, `!x`
520    UnaryOp { op: UnaryOp, operand: Box<Expr<P>> },
521    /// Function call syntax: `sqrt(x)`, `atan2(y, x)`, `eye<3>()`, or `module.fn(x)`.
522    ///
523    /// The callee is a syntactic path. Bare calls and qualified calls have the
524    /// same AST shape; semantic categorization/resolution happens later.
525    FnCall {
526        callee: IdentPath,
527        type_args: Vec<GenericArg<P>>,
528        args: Vec<Expr<P>>,
529    },
530    /// Conditional: `if cond { then_expr } else { else_expr }`
531    If {
532        condition: Box<Expr<P>>,
533        then_branch: Box<Expr<P>>,
534        else_branch: Box<Expr<P>>,
535    },
536    /// Unit-annotated literal: `400 km`, `9.80665 m/s^2`
537    UnitLiteral { value: f64, unit: UnitExpr },
538    /// Conversion: `expr -> unit_expr`
539    Convert {
540        expr: Box<Expr<P>>,
541        target: UnitExpr,
542    },
543    /// Timezone display: `expr -> "America/New_York"` (datetime only)
544    DisplayTimezone {
545        expr: Box<Expr<P>>,
546        timezone: String,
547    },
548    /// Field access: `@transfer.dv1`, `@mission.transfer.dv1`
549    FieldAccess {
550        expr: Box<Expr<P>>,
551        field: Spanned<FieldName>,
552    },
553    /// Constructor-call syntax for values of user-defined unified `type` declarations.
554    ///
555    /// Payload constructors use named arguments, e.g.
556    /// `TransferResult(dv1: @dv1, dv2: @dv2)` or
557    /// `module.TransferResult(dv1: @dv1)`. The callee is a syntactic path; name
558    /// resolution decides whether it denotes a constructor.
559    ConstructorCall {
560        callee: IdentPath,
561        generic_args: Vec<GenericArg<P>>,
562        fields: Vec<FieldInit<P>>,
563    },
564    /// Map literal: `{ Maneuver.Departure: 2.46 km/s, Maneuver.Correction: 0.05 km/s }`
565    MapLiteral { entries: Vec<MapEntry<P>> },
566    /// For comprehension: `for m: Maneuver { @delta_v[m] + 1.0 }`
567    ForComp {
568        bindings: Vec<ForBinding>,
569        body: Box<Expr<P>>,
570    },
571    /// Index access: `@delta_v[m]`, `@delta_v[Maneuver.Departure]`, `@P[a, b]`
572    IndexAccess {
573        expr: Box<Expr<P>>,
574        args: Vec<IndexArg<P>>,
575    },
576    /// Scan: `scan(source, init, |acc, val| body)`
577    Scan {
578        source: Box<Expr<P>>,
579        init: Box<Expr<P>>,
580        acc_name: Spanned<LocalName>,
581        val_name: Spanned<LocalName>,
582        body: Box<Expr<P>>,
583    },
584    /// Unfold: `unfold(init, |prev_i, i| body)`
585    ///
586    /// Generates an indexed value from a seed by iterating over a range index.
587    /// The closure receives `(prev_i, i)` bindings for the previous and current
588    /// step indices, and the body can reference `@node_name[prev_i]`.
589    Unfold {
590        init: Box<Expr<P>>,
591        prev_name: Spanned<LocalName>,
592        curr_name: Spanned<LocalName>,
593        body: Box<Expr<P>>,
594    },
595    /// Match expression: `match @status { Nominal => ..., Warning(message: code) => ... }`
596    Match {
597        scrutinee: Box<Expr<P>>,
598        arms: Vec<MatchArm<P>>,
599    },
600    /// Inline DAG invocation: `@dag(args).out` or `@module.dag(args).out`.
601    ///
602    /// Each syntactic occurrence denotes a fresh DAG instantiation that is
603    /// desugared during TIR lowering to the equivalent
604    /// `include <path>(args) as <synthetic>; @<synthetic>.out`. Preserved as
605    /// a distinct AST variant so source spans survive for diagnostics.
606    ///
607    /// The post-`@` expression as a whole must denote a *node* — that is the
608    /// invariant `@` enforces. `@dag(args).out` is well-formed because
609    /// `dag(args).out` projects an output node from a fresh DAG instance, and
610    /// likewise `@module.dag(args).out` projects an output node from a DAG
611    /// brought into scope via `import module.{dag};` or `import path as
612    /// module;`. Bare `@dag(args)` (no projection) is rejected — a DAG
613    /// instance with no projection is not a node.
614    InlineDagRef {
615        /// Path to the DAG being invoked. Single-segment for same-file calls
616        /// (`@dag(args).out`), multi-segment for cross-file qualified calls
617        /// (`@module.dag(args).out`). The leaf segment names the DAG; any
618        /// preceding segments resolve through module aliases brought into
619        /// scope by `import`.
620        path: ModulePath,
621        /// Param/index bindings, same shape as `include` bindings.
622        args: Vec<ParamBinding<P>>,
623        /// Projected output node name (after the closing paren `.`).
624        output: Spanned<DeclName>,
625    },
626    /// Unresolved reference produced by the parser.
627    ///
628    /// Carries an unresolved identifier path. HIR expression lowering is the
629    /// single stage that classifies and resolves these paths.
630    UnresolvedRef(UnresolvedRef),
631    /// Phase-specific expression sugar.
632    ///
633    /// In [`Raw`], this is [`crate::syntax::ast::RawExprSugar`] and carries
634    /// surface forms like `TableLiteral` that are eliminated by the desugar
635    /// pass. In `Desugared`, the payload is [`core::convert::Infallible`] —
636    /// the variant is statically unreachable.
637    Sugar(P::ExprSugar),
638}
639
640/// An index specification in a table literal's bracket list: `table[Phase, 3]`
641///
642/// Named indexes reference declared index types, while Nat range literals
643/// desugar to `range(N)` with synthetic variants `#0`, `#1`, etc.
644#[derive(Debug, Clone)]
645pub enum TableIndexSpec {
646    /// A named index: `Phase`, `Maneuver`, or `module.Maneuver`
647    Named(Spanned<NamePath>),
648    /// A Nat range literal: `3` (desugars to `range(3)`)
649    NatRange(u64, Span),
650}
651
652/// Shared axes in a multi-declaration table prefix.
653///
654/// The final axis has a distinct semantic role: it is the row axis. Any axes
655/// before it are slice axes. This is intentionally not modeled as a generic
656/// `NonEmpty<TableIndexSpec>` because the tail element is special.
657#[derive(Debug, Clone)]
658pub struct MultiDeclSharedAxes {
659    slice_axes: Vec<TableIndexSpec>,
660    row_axis: TableIndexSpec,
661}
662
663impl MultiDeclSharedAxes {
664    /// Construct shared axes from zero or more slice axes and the always-present row axis.
665    #[must_use]
666    pub const fn new(slice_axes: Vec<TableIndexSpec>, row_axis: TableIndexSpec) -> Self {
667        Self {
668            slice_axes,
669            row_axis,
670        }
671    }
672
673    /// Convert a parser-order vector into semantic slice/row axes.
674    ///
675    /// # Errors
676    ///
677    /// Returns [`crate::syntax::non_empty::EmptyVecError`] when `axes` is empty.
678    pub fn try_from_vec(
679        mut axes: Vec<TableIndexSpec>,
680    ) -> Result<Self, crate::syntax::non_empty::EmptyVecError> {
681        let row_axis = axes.pop().ok_or(crate::syntax::non_empty::EmptyVecError)?;
682        Ok(Self::new(axes, row_axis))
683    }
684
685    /// Slice axes preceding the row axis.
686    #[must_use]
687    pub fn slice_axes(&self) -> &[TableIndexSpec] {
688        &self.slice_axes
689    }
690
691    /// The row axis.
692    #[must_use]
693    pub const fn row_axis(&self) -> &TableIndexSpec {
694        &self.row_axis
695    }
696
697    /// Number of shared axes. Always at least 1.
698    #[must_use]
699    pub const fn len(&self) -> usize {
700        self.slice_axes.len() + 1
701    }
702
703    /// Returns `false`; provided for sequence-like callers.
704    #[must_use]
705    pub const fn is_empty(&self) -> bool {
706        false
707    }
708
709    /// Iterate over axes in source order: slice axes first, then row axis.
710    pub fn iter(&self) -> impl Iterator<Item = &TableIndexSpec> {
711        self.slice_axes
712            .iter()
713            .chain(std::iter::once(&self.row_axis))
714    }
715}
716
717impl std::ops::Index<usize> for MultiDeclSharedAxes {
718    type Output = TableIndexSpec;
719
720    #[expect(
721        clippy::panic,
722        reason = "Index implementations conventionally panic on out-of-bounds access"
723    )]
724    fn index(&self, index: usize) -> &Self::Output {
725        match index.cmp(&self.slice_axes.len()) {
726            std::cmp::Ordering::Less => &self.slice_axes[index],
727            std::cmp::Ordering::Equal => &self.row_axis,
728            std::cmp::Ordering::Greater => {
729                panic!("multi-decl shared axis index out of bounds")
730            }
731        }
732    }
733}
734
735/// An index key in a map literal entry.
736///
737/// Plain map literals use named indexes. Table literals over Nat axes desugar
738/// to map entries with an explicitly typed Nat range key so downstream passes
739/// do not have to recover `range(N)` semantics from a fabricated index name.
740#[derive(Debug, Clone, PartialEq, Eq)]
741pub enum MapEntryIndex {
742    /// A declared named index.
743    Named(NamePath),
744    /// A Nat range literal index, `range(N)`.
745    NatRange(u64),
746}
747
748impl MapEntryIndex {
749    /// Return the leaf registry name for declared named indexes only.
750    ///
751    /// Nat ranges are not declared registry indexes; callers must pattern-match
752    /// and use a typed Nat-range identity instead of fabricating an `IndexName`.
753    #[must_use]
754    pub fn named_registry_name(&self) -> Option<IndexName> {
755        match self {
756            Self::Named(name) => Some(IndexName::from(name.leaf().clone())),
757            Self::NatRange(_) => None,
758        }
759    }
760}
761
762impl From<IndexName> for MapEntryIndex {
763    fn from(value: IndexName) -> Self {
764        Self::Named(NamePath::from(value))
765    }
766}
767
768impl From<NamePath> for MapEntryIndex {
769    fn from(value: NamePath) -> Self {
770        Self::Named(value)
771    }
772}
773
774impl std::fmt::Display for MapEntryIndex {
775    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776        match self {
777            Self::Named(name) => write!(f, "{name}"),
778            Self::NatRange(size) => write!(f, "range({size})"),
779        }
780    }
781}
782
783impl TableIndexSpec {
784    /// Get the source span of this table index specification.
785    #[must_use]
786    pub const fn span(&self) -> Span {
787        match self {
788            Self::Named(spanned) => spanned.span,
789            Self::NatRange(_, span) => *span,
790        }
791    }
792
793    /// Returns `true` if this is a Nat range index.
794    #[must_use]
795    pub const fn is_nat_range(&self) -> bool {
796        matches!(self, Self::NatRange(..))
797    }
798}
799
800/// A single key in a map literal entry: `Index.Variant`
801#[derive(Debug, Clone)]
802pub struct MapEntryKey {
803    pub index: Spanned<MapEntryIndex>,
804    pub variant: Spanned<IndexVariantName>,
805}
806
807/// An entry in a map literal.
808///
809/// Single-axis: `Maneuver.Departure: 2.46 km/s` (keys has 1 element)
810/// Multi-axis:  `(Phase.Launch, Maneuver.Departure): 2.46 km/s` (keys has 2+ elements)
811#[derive(Debug, Clone)]
812pub struct MapEntry<P: Phase = Raw> {
813    pub keys: NonEmpty<MapEntryKey>,
814    pub value: Expr<P>,
815}
816
817/// A binding in a `for` comprehension: `m: Maneuver` or `i: range(3)`
818#[derive(Debug, Clone)]
819pub struct ForBinding {
820    pub var: Spanned<LocalName>,
821    pub index: ForBindingIndex,
822}
823
824/// The index in a for binding: either a named index or a `range(...)` expression.
825#[derive(Debug, Clone)]
826pub enum ForBindingIndex {
827    /// A named index: `for m: Maneuver { ... }` or `for m: module.Maneuver { ... }`
828    Named(Spanned<NamePath>),
829    /// A range expression: `for i: range(3) { ... }` or `for i: range(N) { ... }`
830    Range {
831        /// The argument to `range(...)` — a nat literal or generic nat param.
832        arg: NatExpr,
833        /// Span of the entire `range(...)` expression.
834        span: Span,
835    },
836}
837
838/// A Nat expression (type-level natural number).
839///
840/// Supports literals, variables, addition (Level 1), and multiplication (Level 2).
841#[derive(Debug, Clone)]
842pub enum NatExpr {
843    /// An integer literal, e.g., `3`
844    Literal(u64, Span),
845    /// A variable (generic Nat parameter), e.g., `N`
846    Var(Ident),
847    /// Addition of two nat expressions, e.g., `N + 1`, `M + N`
848    Add(Box<Self>, Box<Self>, Span),
849    /// Multiplication of two nat expressions, e.g., `N * 3`, `M * N`
850    Mul(Box<Self>, Box<Self>, Span),
851}
852
853impl NatExpr {
854    /// Get the source span.
855    #[must_use]
856    pub const fn span(&self) -> Span {
857        match self {
858            Self::Literal(_, span) | Self::Add(_, _, span) | Self::Mul(_, _, span) => *span,
859            Self::Var(ident) => ident.span,
860        }
861    }
862}
863
864impl std::fmt::Display for NatExpr {
865    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
866        match self {
867            Self::Literal(n, _) => write!(f, "{n}"),
868            Self::Var(ident) => f.write_str(&ident.name),
869            Self::Add(lhs, rhs, _) => write!(f, "{lhs} + {rhs}"),
870            Self::Mul(lhs, rhs, _) => write!(f, "{lhs} * {rhs}"),
871        }
872    }
873}
874
875/// A generic argument at a call site (turbofish syntax).
876///
877/// `eye<3>()` has one `GenericArg::Nat(NatExpr::Literal(3, ..))`.
878/// `some_fn<Length>()` has one `GenericArg::Type(TypeExpr { kind: DimExpr(..), .. })`.
879#[derive(Debug, Clone)]
880pub enum GenericArg<P: Phase = Raw> {
881    /// A type expression (for Dim or Index generic params): `Length`, `Maneuver`
882    Type(TypeExpr<P>),
883    /// A nat expression (for Nat generic params): `3`, `N + 1`
884    Nat(NatExpr),
885}
886
887impl<P: Phase> GenericArg<P> {
888    /// Get the source span of this generic argument.
889    #[must_use]
890    pub const fn span(&self) -> Span {
891        match self {
892            Self::Type(te) => te.span,
893            Self::Nat(ne) => ne.span(),
894        }
895    }
896}
897
898/// An argument in an index access: a qualified variant, a loop variable, or an expression.
899#[derive(Debug, Clone)]
900pub enum IndexArg<P: Phase = Raw> {
901    /// Qualified variant: `Maneuver.Departure` or `module.Maneuver.Departure`
902    Variant {
903        index: Spanned<NamePath>,
904        variant: Spanned<IndexVariantName>,
905    },
906    /// Loop variable: `m`
907    Var(Ident),
908    /// Arbitrary expression: `i + 1`, `i - M`
909    Expr(Box<Expr<P>>),
910}
911
912/// A field initializer in a constructor call.
913#[derive(Debug, Clone)]
914pub struct FieldInit<P: Phase = Raw> {
915    pub name: Spanned<FieldName>,
916    pub value: Expr<P>,
917}
918
919/// One arm of a `match` expression: `Impulsive(delta_v: dv) => expr`
920#[derive(Debug, Clone)]
921pub struct MatchArm<P: Phase = Raw> {
922    pub pattern: MatchPattern,
923    pub body: Expr<P>,
924    pub span: Span,
925}
926
927/// A match pattern: `Impulsive(delta_v: dv)`, `Nominal`, `Maneuver.Departure`.
928#[derive(Debug, Clone)]
929pub enum MatchPattern {
930    /// Syntactic path pattern before semantic categorization.
931    ///
932    /// The parser emits this for both constructor-looking and index-label-looking
933    /// patterns. Name resolution may rewrite it to [`Self::Constructor`] or
934    /// [`Self::IndexLabel`] only when it can prove that categorization from the
935    /// local symbol context. Qualified paths that require module-aware lookup
936    /// remain syntactic paths instead of being half-resolved.
937    Path {
938        path: IdentPath,
939        bindings: Vec<PatternBinding>,
940        span: Span,
941    },
942    /// Tagged-union constructor pattern: `Impulsive(delta_v: dv)` or `Nominal`.
943    Constructor {
944        name: Spanned<ConstructorName>,
945        bindings: Vec<PatternBinding>,
946        span: Span,
947    },
948    /// Named-index label pattern: `Maneuver.Departure`.
949    ///
950    /// Index labels are fieldless, so this variant deliberately has no
951    /// binding payload. This variant is semantic: producers should construct it
952    /// only after proving that the path denotes an index variant.
953    IndexLabel {
954        index: Spanned<NamePath>,
955        variant: Spanned<IndexVariantName>,
956        span: Span,
957    },
958}
959
960impl MatchPattern {
961    #[must_use]
962    pub const fn span(&self) -> Span {
963        match self {
964            Self::Path { span, .. }
965            | Self::Constructor { span, .. }
966            | Self::IndexLabel { span, .. } => *span,
967        }
968    }
969
970    #[must_use]
971    pub fn bindings(&self) -> &[PatternBinding] {
972        match self {
973            Self::Path { bindings, .. } | Self::Constructor { bindings, .. } => bindings,
974            Self::IndexLabel { .. } => &[],
975        }
976    }
977}
978
979/// A binding in a match pattern.
980#[derive(Debug, Clone)]
981pub enum PatternBinding {
982    /// Bind a field to a variable: `message: msg`.
983    Bind {
984        field: Spanned<FieldName>,
985        var: Ident,
986    },
987    /// Wildcard: `message: _`
988    Wildcard {
989        field: Spanned<FieldName>,
990        span: Span,
991    },
992}
993
994#[derive(Debug, Clone, Copy, PartialEq, Eq)]
995pub enum BinOp {
996    Add,
997    Sub,
998    Mul,
999    Div,
1000    Mod,
1001    Pow,
1002    Eq,
1003    Ne,
1004    Lt,
1005    Gt,
1006    Le,
1007    Ge,
1008    And,
1009    Or,
1010}
1011
1012#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1013pub enum UnaryOp {
1014    Neg,
1015    Not,
1016}