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}