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