Skip to main content

aver/ir/mir/
stats.rs

1//! `LowerStats` + `SkipReason` — coverage telemetry for the
2//! HIR → MIR lowerer.
3//!
4//! Phase 3 lowers `ResolvedProgramView` into `MirProgram` in
5//! widening waves. Until the lowerer covers everything, fns
6//! containing unsupported shapes get dropped from
7//! `MirProgram.fns` — `MirProgram` is "what MIR could lower so
8//! far", not "what HIR contained". Without telemetry, that drop
9//! is invisible: dump renders, tests pass on the supported
10//! subset, but Phase 4 (VM slice) could land thinking everything
11//! is wired when in fact 60% of the corpus silently disappeared.
12//!
13//! `LowerStats` is the gate. It rides on `MirProgram` so any
14//! consumer can ask "how complete is this lowering". Each
15//! skipped fn bumps a counter keyed by `SkipReason` — the first
16//! unsupported shape the lowerer hit — so widening-wave PRs can
17//! prove that the reason set shrinks.
18//!
19//! ## Coverage contract (Phase 3 → 4 gate)
20//!
21//! - Every dropped fn must produce exactly one `SkipReason`. No
22//!   silent drops, no double-counting.
23//! - The Phase 4 (VM slice) entry gate asserts a corpus-wide
24//!   coverage ratio (lowered / total ≥ target). Each wave PR
25//!   nudges the target up.
26
27use std::collections::HashMap;
28
29/// Why a single `ResolvedFnDef` failed to lower.
30///
31/// One variant per source-level reason — the dominant unsupported
32/// shape inside the fn body. Wave PRs sequentially remove
33/// reasons from the live set; once the set is empty the lowerer
34/// is corpus-complete on the shipped examples.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum SkipReason {
37    /// `ResolvedFnDef.body` had no statements. Frontend should
38    /// have rejected this earlier; recorded for safety.
39    EmptyBody,
40    /// Single-stmt body whose only statement is a binding (no
41    /// tail expression). Well-typed Aver can't produce this, but
42    /// the lowerer guards anyway.
43    BindingOnlyTail,
44    /// Multi-stmt body, `FnResolution` not populated by the
45    /// resolver pass — usually means the fn wasn't visited.
46    MissingResolution,
47    /// Binding referenced by name didn't resolve to a slot via
48    /// `FnResolution.local_slots`. Indicates a resolver gap.
49    BindingSlotLookupMissing,
50    /// `Match` arm's `binding_slots` had fewer slots than the
51    /// pattern's preorder binding count. Indicates a resolver /
52    /// pattern desync.
53    PatternSlotShortfall,
54    /// Built-in constructor *construction* (`Result.Ok(v)`,
55    /// `Option.Some(v)`, …) — wave 3c-i territory. Built-in
56    /// ctors need a typed identity story beyond raw `CtorId`.
57    BuiltinCtorConstruction,
58    /// Built-in constructor *pattern* (`Result.Ok(v) -> ...`,
59    /// `Option.Some(v) -> ...`) — same wave 3c-i territory as
60    /// the construction side.
61    BuiltinCtorPattern,
62    /// `Result-`like ctor whose `CtorId` didn't resolve — usually
63    /// a typecheck error that leaked past the gate. Drops the fn
64    /// rather than panic on a half-resolved ctor.
65    UnresolvedCtor,
66    /// First-class fn value / intrinsic / unresolved call target.
67    /// Wave 3c-iii (closures / intrinsics) territory.
68    UnsupportedCallee,
69    /// `Record{...}` / `Record{base & ...}` on a built-in type
70    /// (`HttpResponse`, `Header`, `Buffer`, …) with no `TypeId`.
71    /// Wave 3c-i / 3c-iv territory depending on the type.
72    BuiltinRecord,
73    /// `Try` (the `?` propagation node) — wave 3c-ii.
74    UnsupportedTry,
75    /// Tail call — wave 3c-iii.
76    UnsupportedTailCall,
77    /// List literal — wave 3c-iv.
78    UnsupportedList,
79    /// Tuple literal — wave 3c-iv.
80    UnsupportedTuple,
81    /// Map literal — wave 3c-iv.
82    UnsupportedMap,
83    /// Interpolated string — wave 3c-iv.
84    UnsupportedInterpolatedStr,
85    /// `IndependentProduct` (`?!` / `!`) — wave 3c-v.
86    UnsupportedIndependentProduct,
87    /// Unresolved top-level `Ident` that survived past the
88    /// resolver — usually a resolver gap. Drops the fn rather than
89    /// emit a half-resolved name.
90    UnresolvedIdent,
91    /// A `ResolvedExpr` variant the lowerer doesn't recognise at
92    /// all. Should be empty after wave 3c lands — anything
93    /// here points to a missed variant.
94    UnsupportedOther,
95}
96
97impl SkipReason {
98    /// Short human-readable label for dumps / diagnostics.
99    pub fn label(self) -> &'static str {
100        match self {
101            Self::EmptyBody => "empty body",
102            Self::BindingOnlyTail => "binding-only tail",
103            Self::MissingResolution => "missing resolution",
104            Self::BindingSlotLookupMissing => "binding slot lookup missing",
105            Self::PatternSlotShortfall => "pattern slot shortfall",
106            Self::BuiltinCtorConstruction => "built-in ctor construction",
107            Self::BuiltinCtorPattern => "built-in ctor pattern",
108            Self::UnresolvedCtor => "unresolved ctor",
109            Self::UnsupportedCallee => "unsupported callee",
110            Self::BuiltinRecord => "built-in record type",
111            Self::UnsupportedTry => "unsupported Try (`?`)",
112            Self::UnsupportedTailCall => "unsupported tail call",
113            Self::UnsupportedList => "unsupported list literal",
114            Self::UnsupportedTuple => "unsupported tuple literal",
115            Self::UnsupportedMap => "unsupported map literal",
116            Self::UnsupportedInterpolatedStr => "unsupported interpolated string",
117            Self::UnsupportedIndependentProduct => "unsupported IndependentProduct",
118            Self::UnresolvedIdent => "unresolved Ident (resolver gap)",
119            Self::UnsupportedOther => "unsupported (other)",
120        }
121    }
122}
123
124/// Coverage counters for a single `lower_program` call.
125///
126/// `lowered` = fns successfully placed into `MirProgram.fns`.
127/// `skipped` = fns dropped, keyed by the first `SkipReason` the
128/// lowerer hit inside that fn. Total = `lowered + skipped.values().sum()`
129/// equals the number of `ResolvedFnDef` items the call saw.
130#[derive(Debug, Clone, Default)]
131pub struct LowerStats {
132    /// Number of fns whose body lowered successfully into
133    /// `MirProgram.fns`.
134    pub lowered: u32,
135    /// Skipped fns keyed by reason. One bump per dropped fn.
136    pub skipped: HashMap<SkipReason, u32>,
137}
138
139impl LowerStats {
140    /// Total fns the lowerer saw, lowered or not.
141    pub fn total(&self) -> u32 {
142        self.lowered + self.skipped.values().sum::<u32>()
143    }
144
145    /// Coverage ratio in `[0.0, 1.0]`. Returns `1.0` for an empty
146    /// program so empty corpora don't poison the gate.
147    pub fn coverage_ratio(&self) -> f64 {
148        let total = self.total();
149        if total == 0 {
150            return 1.0;
151        }
152        f64::from(self.lowered) / f64::from(total)
153    }
154
155    /// Record one lowered fn.
156    pub fn record_lowered(&mut self) {
157        self.lowered += 1;
158    }
159
160    /// Record one dropped fn with its dominant reason.
161    pub fn record_skip(&mut self, reason: SkipReason) {
162        *self.skipped.entry(reason).or_insert(0) += 1;
163    }
164
165    /// Iterate `(SkipReason, count)` pairs in a stable order
166    /// (variant order via `SkipReason as u8` ascending), so dumps
167    /// + test assertions don't depend on `HashMap` iteration drift.
168    pub fn skipped_sorted(&self) -> Vec<(SkipReason, u32)> {
169        let mut entries: Vec<_> = self.skipped.iter().map(|(r, c)| (*r, *c)).collect();
170        entries.sort_by_key(|(r, _)| *r as u8);
171        entries
172    }
173}