Skip to main content

aver/ast/
mod.rs

1pub mod types;
2pub use types::Type;
3
4/// Source line number (1-based). 0 = synthetic/unknown.
5pub type SourceLine = usize;
6
7/// A `bool` that compares as always-equal. Used for `last_use` annotations
8/// on `Expr::Resolved` — metadata that should not affect AST equality
9/// (same pattern as `Spanned` ignoring `line` in its `PartialEq`).
10#[derive(Debug, Clone, Copy, Default)]
11pub struct AnnotBool(pub bool);
12
13impl PartialEq for AnnotBool {
14    fn eq(&self, _: &Self) -> bool {
15        true
16    }
17}
18
19impl From<bool> for AnnotBool {
20    fn from(b: bool) -> Self {
21        Self(b)
22    }
23}
24
25/// AST node with source location plus an optional inferred type.
26///
27/// Line-agnostic equality: two `Spanned` values are equal iff their inner
28/// nodes are equal, regardless of line or attached type. The type slot is a
29/// `OnceLock<Type>` populated by the type checker; backends that have not
30/// been migrated to consume it stay agnostic and continue inferring locally.
31/// `OnceLock` (rather than `OnceCell`) keeps `Spanned` `Sync`, which matters
32/// because parts of the AST live behind `Arc` and cross thread boundaries
33/// (e.g. parallel verify execution, REPL background tasks).
34#[derive(Debug)]
35pub struct Spanned<T> {
36    pub node: T,
37    pub line: SourceLine,
38    pub ty: std::sync::OnceLock<Type>,
39}
40
41// `OnceLock` does not derive `Clone` (the cell is invariant over `T`), so the
42// inner type is cloned manually.
43impl<T: Clone> Clone for Spanned<T> {
44    fn clone(&self) -> Self {
45        let ty = std::sync::OnceLock::new();
46        if let Some(t) = self.ty.get() {
47            let _ = ty.set(t.clone());
48        }
49        Self {
50            node: self.node.clone(),
51            line: self.line,
52            ty,
53        }
54    }
55}
56
57impl<T: PartialEq> PartialEq for Spanned<T> {
58    fn eq(&self, other: &Self) -> bool {
59        self.node == other.node
60    }
61}
62
63impl<T> Spanned<T> {
64    pub fn new(node: T, line: SourceLine) -> Self {
65        Self {
66            node,
67            line,
68            ty: std::sync::OnceLock::new(),
69        }
70    }
71
72    /// Create a Spanned with line=0 (synthetic/generated AST, no source location).
73    pub fn bare(node: T) -> Self {
74        Self::new(node, 0)
75    }
76
77    /// Record the inferred type for this node. No-op if a type is already set
78    /// (later inference passes must not contradict the first one).
79    pub fn set_ty(&self, ty: Type) {
80        let _ = self.ty.set(ty);
81    }
82
83    /// Inferred type for this node, if the type checker has visited it.
84    pub fn ty(&self) -> Option<&Type> {
85        self.ty.get()
86    }
87}
88
89#[derive(Debug, Clone, PartialEq)]
90pub enum Literal {
91    Int(i64),
92    Float(f64),
93    Str(String),
94    Bool(bool),
95    Unit,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub enum BinOp {
100    Add,
101    Sub,
102    Mul,
103    Div,
104    Eq,
105    Neq,
106    Lt,
107    Gt,
108    Lte,
109    Gte,
110}
111
112#[derive(Debug)]
113pub struct MatchArm {
114    pub pattern: Pattern,
115    pub body: Box<Spanned<Expr>>,
116    /// Per-arm slot table for the pattern's bindings, in pattern order.
117    /// Filled by the resolver pass; backend code reads from here
118    /// instead of doing a name lookup, so two arms with the same
119    /// binding name (e.g. `deadline` showing up in both `TaskCreated`
120    /// and `DeadlineSet` with different field types) get separate
121    /// slots without colliding in the function-level slot table.
122    /// Wildcard-position bindings (`_`) are stored as `u16::MAX` and
123    /// must never be read.
124    pub binding_slots: std::sync::OnceLock<Vec<u16>>,
125}
126
127// `OnceLock` doesn't derive Clone (cell is invariant over T); copy
128// the inner manually so the resolver's allocations survive the
129// `Arc::make_mut` clones that happen during multimodule flatten.
130impl Clone for MatchArm {
131    fn clone(&self) -> Self {
132        let binding_slots = std::sync::OnceLock::new();
133        if let Some(v) = self.binding_slots.get() {
134            let _ = binding_slots.set(v.clone());
135        }
136        Self {
137            pattern: self.pattern.clone(),
138            body: self.body.clone(),
139            binding_slots,
140        }
141    }
142}
143
144impl PartialEq for MatchArm {
145    fn eq(&self, other: &Self) -> bool {
146        self.pattern == other.pattern && self.body == other.body
147    }
148}
149
150impl MatchArm {
151    /// Build a fresh arm with no binding-slot stamp yet — resolver
152    /// fills `binding_slots` after slot allocation. Use this from any
153    /// site that synthesises an arm (parser, AST rewrites, effect
154    /// lifting, tests).
155    pub fn new(pattern: Pattern, body: Spanned<Expr>) -> Self {
156        Self {
157            pattern,
158            body: Box::new(body),
159            binding_slots: std::sync::OnceLock::new(),
160        }
161    }
162
163    pub fn new_boxed(pattern: Pattern, body: Box<Spanned<Expr>>) -> Self {
164        Self {
165            pattern,
166            body,
167            binding_slots: std::sync::OnceLock::new(),
168        }
169    }
170}
171
172#[derive(Debug, Clone, PartialEq)]
173pub enum Pattern {
174    Wildcard,
175    Literal(Literal),
176    Ident(String),
177    /// Empty list pattern: `[]`
178    EmptyList,
179    /// Cons-like list pattern: `[head, ..tail]`
180    Cons(String, String),
181    /// Tuple pattern: `(a, b)` / `(_, x)` / nested tuples.
182    Tuple(Vec<Pattern>),
183    /// Constructor pattern: fully-qualified name + list of binding names.
184    /// Built-ins: Result.Ok(x), Result.Err(x), Option.Some(x), Option.None.
185    /// User-defined: Shape.Circle(r), Shape.Rect(w, h), Shape.Point.
186    Constructor(String, Vec<String>),
187}
188
189#[derive(Debug, Clone, PartialEq)]
190pub enum StrPart {
191    Literal(String),
192    Parsed(Box<Spanned<Expr>>),
193}
194
195/// Data for a tail-call expression.
196#[derive(Debug, Clone, PartialEq)]
197pub struct TailCallData {
198    /// Target function name (self or mutual-recursive peer).
199    pub target: String,
200    /// Arguments to pass.
201    pub args: Vec<Spanned<Expr>>,
202}
203
204impl TailCallData {
205    pub fn new(target: String, args: Vec<Spanned<Expr>>) -> Self {
206        Self { target, args }
207    }
208}
209
210#[derive(Debug, Clone, PartialEq)]
211pub enum Expr {
212    Literal(Literal),
213    Ident(String),
214    Attr(Box<Spanned<Expr>>, String),
215    FnCall(Box<Spanned<Expr>>, Vec<Spanned<Expr>>),
216    BinOp(BinOp, Box<Spanned<Expr>>, Box<Spanned<Expr>>),
217    Match {
218        subject: Box<Spanned<Expr>>,
219        arms: Vec<MatchArm>,
220    },
221    Constructor(String, Option<Box<Spanned<Expr>>>),
222    ErrorProp(Box<Spanned<Expr>>),
223    InterpolatedStr(Vec<StrPart>),
224    List(Vec<Spanned<Expr>>),
225    Tuple(Vec<Spanned<Expr>>),
226    /// Map literal: `{"a" => 1, "b" => 2}`
227    MapLiteral(Vec<(Spanned<Expr>, Spanned<Expr>)>),
228    /// Record creation: `User(name = "Alice", age = 30)`
229    RecordCreate {
230        type_name: String,
231        fields: Vec<(String, Spanned<Expr>)>,
232    },
233    /// Record update: `User.update(base, field = newVal, ...)`
234    RecordUpdate {
235        type_name: String,
236        base: Box<Spanned<Expr>>,
237        updates: Vec<(String, Spanned<Expr>)>,
238    },
239    /// Tail-position call to a function in the same SCC (self or mutual recursion).
240    /// Produced by the TCO transform pass before type-checking.
241    /// Reuse info is populated by `ir::reuse::annotate_program_reuse`.
242    TailCall(Box<TailCallData>),
243    /// Independent product: `(a, b, c)!` or `(a, b, c)?!`.
244    /// Elements are independent effectful expressions evaluated with no guaranteed order.
245    /// `unwrap=true` (`?!`): all elements must be Result; unwraps Ok values, propagates first Err.
246    /// `unwrap=false` (`!`): returns raw tuple of results.
247    /// Produces a replay group (effects matched by branch_path + effect_occurrence + type + args).
248    IndependentProduct(Vec<Spanned<Expr>>, bool),
249    /// Compiled variable lookup: `env[last][slot]` — O(1) instead of HashMap scan.
250    /// Produced by the resolver pass for locals inside function bodies.
251    /// `last_use` is set by `ir::last_use` — when true, this is the final
252    /// reference to this slot and backends can move instead of copy.
253    Resolved {
254        slot: u16,
255        name: String,
256        last_use: AnnotBool,
257    },
258}
259
260#[derive(Debug, Clone, PartialEq)]
261pub enum Stmt {
262    Binding(String, Option<String>, Spanned<Expr>),
263    Expr(Spanned<Expr>),
264}
265
266#[derive(Debug, Clone, PartialEq)]
267pub enum FnBody {
268    Block(Vec<Stmt>),
269}
270
271impl FnBody {
272    pub fn from_expr(expr: Spanned<Expr>) -> Self {
273        Self::Block(vec![Stmt::Expr(expr)])
274    }
275
276    pub fn stmts(&self) -> &[Stmt] {
277        match self {
278            Self::Block(stmts) => stmts,
279        }
280    }
281
282    pub fn stmts_mut(&mut self) -> &mut Vec<Stmt> {
283        match self {
284            Self::Block(stmts) => stmts,
285        }
286    }
287
288    pub fn tail_expr(&self) -> Option<&Spanned<Expr>> {
289        match self.stmts().last() {
290            Some(Stmt::Expr(expr)) => Some(expr),
291            _ => None,
292        }
293    }
294
295    pub fn tail_expr_mut(&mut self) -> Option<&mut Spanned<Expr>> {
296        match self.stmts_mut().last_mut() {
297            Some(Stmt::Expr(expr)) => Some(expr),
298            _ => None,
299        }
300    }
301}
302
303/// Compile-time resolution metadata for a function body.
304/// Produced by `resolver::resolve_fn` — maps local variable names to slot indices
305/// so the VM can use `Vec<Value>` instead of `HashMap` lookups.
306#[derive(Debug, Clone, PartialEq)]
307pub struct FnResolution {
308    /// Total number of local slots needed (params + bindings in body).
309    pub local_count: u16,
310    /// Map from local variable name → slot index in the local `Slots` frame.
311    pub local_slots: std::sync::Arc<std::collections::HashMap<String, u16>>,
312    /// Aver type per slot index. Length == `local_count`. Built post-
313    /// typecheck so each entry pulls from the matching `Spanned::ty()`
314    /// stamp on the producer expression, plus pattern-binding shape
315    /// rules (`Result.Ok` → T, `Cons head` → list element, tuple item
316    /// → tuple element, …). Backends that need a typed local table
317    /// (the wasm-gc lowering uses one to declare each `local` with a
318    /// concrete `ValType`) consume this directly instead of re-deriving
319    /// the same information from patterns.
320    ///
321    /// Default `Type::Invalid` for unreachable / unstamped slots — every
322    /// real binding gets overwritten during the slot-types pass, so an
323    /// `Invalid` reaching the backend means the slot was never the
324    /// target of a binding (resolver counted but no expression
325    /// produced into it; usually a wildcard slot the backend skips).
326    pub local_slot_types: std::sync::Arc<Vec<Type>>,
327}
328
329#[derive(Debug, Clone, PartialEq)]
330pub struct FnDef {
331    pub name: String,
332    pub line: usize,
333    pub params: Vec<(String, String)>,
334    pub return_type: String,
335    pub effects: Vec<Spanned<String>>,
336    pub desc: Option<String>,
337    pub body: std::sync::Arc<FnBody>,
338    /// `None` for unresolved (REPL, module loading).
339    pub resolution: Option<FnResolution>,
340}
341
342#[derive(Debug, Clone, PartialEq)]
343pub struct Module {
344    pub name: String,
345    pub line: usize,
346    pub depends: Vec<String>,
347    pub exposes: Vec<String>,
348    pub exposes_opaque: Vec<String>,
349    pub exposes_line: Option<usize>,
350    pub intent: String,
351    /// Module-level effect surface declaration. `None` is legacy/mixed
352    /// (no enforcement, soft warning emitted by `aver check`); `Some([])`
353    /// is explicit pure; `Some([...])` is a declared boundary — every
354    /// function's `! [...]` must be a subset (namespace-level entry like
355    /// `Disk` admits any `Disk.*` method).
356    pub effects: Option<Vec<String>>,
357    pub effects_line: Option<usize>,
358}
359
360#[derive(Debug, Clone, PartialEq)]
361pub enum VerifyGivenDomain {
362    /// Integer range domain in verify law: `1..50` (inclusive).
363    IntRange { start: i64, end: i64 },
364    /// Explicit domain values in verify law: `[v1, v2, ...]`.
365    Explicit(Vec<Spanned<Expr>>),
366}
367
368#[derive(Debug, Clone, PartialEq)]
369pub struct VerifyGiven {
370    pub name: String,
371    pub type_name: String,
372    pub domain: VerifyGivenDomain,
373}
374
375#[derive(Debug, Clone, PartialEq)]
376pub struct VerifyLaw {
377    pub name: String,
378    pub givens: Vec<VerifyGiven>,
379    /// Optional precondition for the law template, written as `when <bool-expr>`.
380    pub when: Option<Spanned<Expr>>,
381    /// Template assertion from source before given-domain expansion.
382    pub lhs: Spanned<Expr>,
383    pub rhs: Spanned<Expr>,
384    /// Per-sample substituted guards for `when`, aligned with `VerifyBlock.cases`.
385    pub sample_guards: Vec<Spanned<Expr>>,
386}
387
388/// Source range for AST nodes that need location tracking.
389/// Used by verify case spans: `cases[i] <-> case_spans[i]`.
390#[derive(Debug, Clone, PartialEq, Default)]
391pub struct SourceSpan {
392    pub line: usize,
393    pub col: usize,
394    pub end_line: usize,
395    pub end_col: usize,
396}
397
398#[derive(Debug, Clone, PartialEq)]
399pub enum VerifyKind {
400    Cases,
401    Law(Box<VerifyLaw>),
402}
403
404#[derive(Debug, Clone, PartialEq)]
405pub struct VerifyBlock {
406    pub fn_name: String,
407    pub line: usize,
408    pub cases: Vec<(Spanned<Expr>, Spanned<Expr>)>,
409    pub case_spans: Vec<SourceSpan>,
410    /// Per-case given bindings for law verify (empty for Cases kind).
411    pub case_givens: Vec<Vec<(String, Spanned<Expr>)>>,
412    /// Parallel to `cases`: `true` when the case was injected by
413    /// `aver verify --hostile` (boundary-value expansion of a law's
414    /// `given` clause), `false` for cases the user wrote directly.
415    /// Empty under non-hostile runs; the renderer uses this to label
416    /// failures as "outside declared given — encode as `when` if
417    /// precondition" when they only fail under the hostile expansion.
418    pub case_hostile_origins: Vec<bool>,
419    /// Parallel to `cases`: per-case hostile effect-profile assignment
420    /// for `--hostile` mode. Each inner Vec lists `(method, profile)`
421    /// pairs (e.g. `("Time.now", "frozen")`) that the runner installs
422    /// as oracle stubs before running the case, alongside any user-given
423    /// stubs. Empty inner Vec for cases that aren't effect-hostile-
424    /// expanded (declared, value-hostile-only, or fns without applicable
425    /// classified effects). All entries empty under non-hostile runs.
426    pub case_hostile_profiles: Vec<Vec<(String, String)>>,
427    pub kind: VerifyKind,
428    /// Oracle v1: `trace` keyword enables trace-aware assertions
429    /// (`.trace.*`, `.result`, event literals in `.contains` / match
430    /// patterns). Without it, a law checks only the return value, so
431    /// adding a debug print does not break proofs that do not care
432    /// about traces.
433    pub trace: bool,
434    /// Oracle v1: `given` clauses declared at the top of a cases-form
435    /// trace block. Law-form stores its givens inside `VerifyKind::Law`;
436    /// cases-form doesn't have that wrapper, so this field carries them
437    /// so the verify runner can build oracle-stub mappings from the
438    /// same data. Empty for non-trace or law-form blocks.
439    pub cases_givens: Vec<VerifyGiven>,
440}
441
442impl VerifyBlock {
443    /// Construct a VerifyBlock with default (zero) spans for each case.
444    /// Use when source location tracking is not needed (codegen, tests).
445    pub fn new_unspanned(
446        fn_name: String,
447        line: usize,
448        cases: Vec<(Spanned<Expr>, Spanned<Expr>)>,
449        kind: VerifyKind,
450    ) -> Self {
451        let case_spans = vec![SourceSpan::default(); cases.len()];
452        let case_hostile_origins = vec![false; cases.len()];
453        let case_hostile_profiles = vec![Vec::new(); cases.len()];
454        Self {
455            fn_name,
456            line,
457            cases,
458            case_spans,
459            case_givens: vec![],
460            case_hostile_origins,
461            case_hostile_profiles,
462            kind,
463            trace: false,
464            cases_givens: vec![],
465        }
466    }
467
468    pub fn iter_cases_with_spans(
469        &self,
470    ) -> impl Iterator<Item = (&(Spanned<Expr>, Spanned<Expr>), &SourceSpan)> {
471        debug_assert_eq!(self.cases.len(), self.case_spans.len());
472        self.cases.iter().zip(&self.case_spans)
473    }
474}
475
476#[derive(Debug, Clone, PartialEq)]
477pub struct DecisionBlock {
478    pub name: String,
479    pub line: usize,
480    pub date: String,
481    pub reason: String,
482    pub chosen: Spanned<DecisionImpact>,
483    pub rejected: Vec<Spanned<DecisionImpact>>,
484    pub impacts: Vec<Spanned<DecisionImpact>>,
485    pub author: Option<String>,
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Hash)]
489pub enum DecisionImpact {
490    Symbol(String),
491    Semantic(String),
492}
493
494impl DecisionImpact {
495    pub fn text(&self) -> &str {
496        match self {
497            DecisionImpact::Symbol(s) | DecisionImpact::Semantic(s) => s,
498        }
499    }
500
501    pub fn as_context_string(&self) -> String {
502        match self {
503            DecisionImpact::Symbol(s) => s.clone(),
504            DecisionImpact::Semantic(s) => format!("\"{}\"", s),
505        }
506    }
507}
508
509/// A variant in a sum type definition.
510/// e.g. `Circle(Float)` → `TypeVariant { name: "Circle", fields: ["Float"] }`
511#[derive(Debug, Clone, PartialEq)]
512pub struct TypeVariant {
513    pub name: String,
514    pub fields: Vec<String>, // type annotations (e.g. "Float", "String")
515}
516
517/// A user-defined type definition.
518#[derive(Debug, Clone, PartialEq)]
519pub enum TypeDef {
520    /// `type Shape` with variants Circle(Float), Rect(Float, Float), Point
521    Sum {
522        name: String,
523        variants: Vec<TypeVariant>,
524        line: usize,
525    },
526    /// `record User` with fields name: String, age: Int
527    Product {
528        name: String,
529        fields: Vec<(String, String)>,
530        line: usize,
531    },
532}
533
534#[derive(Debug, Clone, PartialEq)]
535pub enum TopLevel {
536    Module(Module),
537    FnDef(FnDef),
538    Verify(VerifyBlock),
539    Decision(DecisionBlock),
540    Stmt(Stmt),
541    TypeDef(TypeDef),
542}