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}