1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! Phase 4: post-pass sanity check. Walks the IR looking for any
//! `Generic`, `TypeParam`, generic trait, or unresolved `Virtual`
//! dispatch left behind by the rewrite/compaction pipeline. The first
//! finding is surfaced as an `InternalError`; anything later is dropped
//! to keep the diagnostic short.
use crate::ir::{GenericBase, IrExpr, IrModule, ResolvedType};
use super::expr_walk::walk_expr;
use super::rewrite::receiver_to_base;
use super::walkers::{walk_expr_types, walk_function_types};
#[derive(Default)]
pub(super) struct LeftoverScanner {
first: Option<String>,
}
impl LeftoverScanner {
fn note(&mut self, detail: String) {
if self.first.is_none() {
self.first = Some(detail);
}
}
pub(super) fn first_error(self) -> Option<String> {
self.first
.map(|s| format!("monomorphise: leftover after pass — {s}"))
}
pub(super) fn scan(&mut self, module: &IrModule) {
// Phase F: generic traits are now compacted alongside generic
// structs and enums; a survivor here means the rewrite/remap
// chain dropped a reference somewhere. Prelude-shipped generic
// built-ins (`Optional`, `Array`, `Dictionary`, `Range`) are
// exempt — they're carriers, not specialisable templates.
for t in &module.traits {
if !t.generic_params.is_empty() {
self.note(format!(
"generic trait `{}` survived compaction (rewrite_trait_refs missed a reference)",
t.name
));
}
}
let is_prelude_builtin =
|name: &str| matches!(name, "Array" | "Dictionary" | "Range" | "Optional");
let mut check = |ty: &ResolvedType| {
if let Some(sample) = first_leftover(ty, module) {
self.note(sample);
}
};
// Walk every type slot in the module *except* the bodies of the
// prelude built-ins themselves and their extern impl blocks.
// Their fields, variants, and method signatures legitimately
// mention `TypeParam(T)` because they're surviving generic
// carriers; everything else is real code that must be fully
// specialised.
for s in &module.structs {
if is_prelude_builtin(&s.name) {
continue;
}
for field in &s.fields {
check(&field.ty);
if let Some(d) = &field.default {
walk_expr_types(d, &mut check);
}
}
}
for e in &module.enums {
if is_prelude_builtin(&e.name) {
continue;
}
for v in &e.variants {
for f in &v.fields {
check(&f.ty);
if let Some(d) = &f.default {
walk_expr_types(d, &mut check);
}
}
}
}
for t in &module.traits {
for f in &t.fields {
check(&f.ty);
if let Some(d) = &f.default {
walk_expr_types(d, &mut check);
}
}
for sig in &t.methods {
for p in &sig.params {
if let Some(ty) = &p.ty {
check(ty);
}
if let Some(d) = &p.default {
walk_expr_types(d, &mut check);
}
}
if let Some(ty) = &sig.return_type {
check(ty);
}
}
}
for imp in &module.impls {
let on_builtin = match imp.target {
crate::ir::ImplTarget::Struct(id) => module
.get_struct(id)
.is_some_and(|s| is_prelude_builtin(&s.name)),
crate::ir::ImplTarget::Enum(id) => module
.get_enum(id)
.is_some_and(|e| is_prelude_builtin(&e.name)),
crate::ir::ImplTarget::Primitive(_) => false,
};
if on_builtin {
continue;
}
for f in &imp.functions {
walk_function_types(f, &mut check);
}
}
for f in &module.functions {
walk_function_types(f, &mut check);
}
for l in &module.lets {
check(&l.ty);
walk_expr_types(&l.value, &mut check);
}
// Tier-1 item E2: any `DispatchKind::Virtual` whose receiver
// type is concrete (Struct/Enum) means Phase 2d failed to find
// an impl that should exist. Surface the gap rather than
// silently leaving the call unresolved. Calls on TypeParam
// receivers (uninstantiated generic bodies) are tolerated.
scan_dispatch_leftovers(module, self);
}
}
/// Walk every method-call site in the module; report any `Virtual`
/// dispatch on a concrete (`Struct`/`Enum`) receiver. Used by the
/// monomorphise leftover scanner.
fn scan_dispatch_leftovers(module: &IrModule, scanner: &mut LeftoverScanner) {
let mut check = |expr: &IrExpr| {
if let IrExpr::MethodCall {
receiver,
method,
dispatch: crate::ir::DispatchKind::Virtual { trait_id, .. },
..
} = expr
{
if let Some(base) = receiver_to_base(receiver.ty()) {
let kind = match base {
GenericBase::Struct(id) => format!("struct id {}", id.0),
GenericBase::Enum(id) => format!("enum id {}", id.0),
// Trait base shouldn't appear as a method-call
// receiver post item E2 — surface it in the
// diagnostic anyway so unexpected leftovers are
// visible.
GenericBase::Trait(id) => format!("trait id {}", id.0),
};
scanner.note(format!(
"unresolved Virtual dispatch — method `{method}` on concrete receiver \
({kind}) for trait id {} (devirtualisation should have rewritten this)",
trait_id.0
));
}
}
};
for f in &module.functions {
if let Some(body) = &f.body {
walk_expr(body, &mut check);
}
}
for imp in &module.impls {
for f in &imp.functions {
if let Some(body) = &f.body {
walk_expr(body, &mut check);
}
}
}
for l in &module.lets {
walk_expr(&l.value, &mut check);
}
}
fn first_leftover(ty: &ResolvedType, module: &IrModule) -> Option<String> {
// Lowering never emits `TypeParam` as a placeholder, so a survivor here
// is a real monomorphisation gap — report it. Generic instantiations
// of the prelude-shipped built-in carriers (`Optional`, `Array`,
// `Dictionary`, `Range`) are the canonical post-pass shape and
// never get specialised, so they're allowed.
let prelude_ids = [
module.prelude_array_id().map(GenericBase::Struct),
module.prelude_dictionary_id().map(GenericBase::Struct),
module.prelude_range_id().map(GenericBase::Struct),
module.prelude_optional_id().map(GenericBase::Enum),
];
let is_prelude_builtin =
|base: &GenericBase| prelude_ids.iter().any(|p| p.as_ref() == Some(base));
match ty {
ResolvedType::TypeParam(name) => Some(format!("unresolved TypeParam(`{name}`)")),
ResolvedType::Generic { base, args } => {
if is_prelude_builtin(base) {
return args.iter().find_map(|a| first_leftover(a, module));
}
let (kind, id) = match base {
GenericBase::Struct(s) => ("struct", s.0),
GenericBase::Enum(e) => ("enum", e.0),
GenericBase::Trait(t) => ("trait", t.0),
};
Some(format!(
"unresolved Generic(base={kind}_id={id}, {} args)",
args.len()
))
}
ResolvedType::Tuple(fields) => fields.iter().find_map(|(_, t)| first_leftover(t, module)),
ResolvedType::Closure {
param_tys,
return_ty,
} => param_tys
.iter()
.find_map(|(_, t)| first_leftover(t, module))
.or_else(|| first_leftover(return_ty, module)),
ResolvedType::External { type_args, .. } => {
type_args.iter().find_map(|a| first_leftover(a, module))
}
// `Error` shouldn't reach monomorphisation under normal compilation
// (upstream `CompilerError`s would have aborted before passes run);
// surface it explicitly when an externally-loaded IR contains one.
ResolvedType::Error => Some("ResolvedType::Error placeholder".to_string()),
ResolvedType::Primitive(_)
| ResolvedType::Struct(_)
| ResolvedType::Trait(_)
| ResolvedType::Enum(_) => None,
}
}