Skip to content

Commit 22686d5

Browse files
committed
Prevent failed declaration diagnostic cascades
Retain failed global, local, and awaited declarations as semantic poison bindings so later references preserve navigation without producing unrelated unknown-variable or type errors. Propagate the poison through contextual typing and common expression forms while keeping strict compilation and code generation blocked by the original diagnostics.
1 parent 700134e commit 22686d5

9 files changed

Lines changed: 222 additions & 64 deletions

File tree

‎TODO.md‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ or behavioral parity.
5555
compiler revision for future campaigns as provenance, not because this
5656
campaign used a different compiler.
5757
- [ ] Continue refreshing the ASL migration catalog and cookbook for
58-
non-lifecycle misunderstandings found by the campaign. Suppress predictable
59-
follow-on errors after a missing member or failed declaration. Do not add
58+
non-lifecycle misunderstandings found by the campaign. Do not add
6059
compatibility aliases.
6160
- [ ] Use those maintained ports to decide each next implementation slice.
6261
Exercise the existing fixed-array search, `Set<String>`, and explicit native
@@ -155,11 +154,6 @@ or behavioral parity.
155154
straightforward to model process names, 32/64-bit memory, modules, failed
156155
reads, settings snapshots, timer state, attach/detach/restart, and bounded
157156
async polling. A compile-only port is never labelled faithful.
158-
- [ ] Improve missing-member recovery so one unknown method does not create
159-
unrelated unknown-local cascades. The corpus-proven immutable `String`
160-
surface now includes explicit ASCII normalization, fallible numeric parsing,
161-
raw UTF-8 byte inspection, and Unicode `char` inspection under one
162-
byte-index policy; add further operations only from maintained-port evidence.
163157
- [ ] Add an explicit `f32`/`f64` bit-pattern construction form and expose the
164158
exact rounded bits for decimal literals in tooling. Decimal exponent syntax,
165159
representable subnormal lowering, formatter preservation, and target-width

‎docs/ROADMAP_ARCHIVE.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# SplitScript roadmap
22

3+
## 2026-08-09: cascade-free failed-declaration recovery
4+
5+
- Failed global, local, and suspending `let` initializers now retain their
6+
declared source identity with an internal poison type. Later references no
7+
longer become misleading unknown-variable errors merely because the
8+
initializer already produced a focused diagnostic.
9+
- The poison type absorbs contextual unification and capability requirements,
10+
and propagates through member access, indexing, calls, and operators without
11+
inventing secondary type errors. Strict compilation still fails and the
12+
poison type can never reach code generation.
13+
- Preserved editor navigation through invalid code: go to definition and rename
14+
continue to resolve a failed binding while inlay hints hide its deliberately
15+
unknown type. Regression coverage includes globals, ordinary locals, and
16+
awaited declarations plus chained members, indexes, calls, comparisons, and
17+
generic `Display` consumers.
18+
319
## 2026-08-09: Unicode characters and explicit UTF-8 string inspection
420

521
- Added the nominal `char` type and single-quoted literals for exactly one

‎src/inference.rs‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,20 @@ impl InferenceContext {
285285
&self.types
286286
}
287287

288+
/// Produces the semantic poison type used after an already-diagnosed
289+
/// expression failure. It preserves declaration identity for later editor
290+
/// queries without making the failed program eligible for code generation.
291+
pub(crate) fn error_type(&mut self) -> Type {
292+
Type::Known(self.types.id_for_error())
293+
}
294+
295+
pub(crate) fn is_error_type(&mut self, ty: Type) -> bool {
296+
matches!(
297+
self.shallow(ty),
298+
Type::Known(id) if matches!(self.types.kind(id), TypeKind::Error)
299+
)
300+
}
301+
288302
pub(crate) fn standard_type(&self, ty: Type) -> Option<StdlibTypeId> {
289303
let Type::Known(id) = ty else {
290304
return None;
@@ -526,6 +540,9 @@ impl InferenceContext {
526540
pub(crate) fn unify(&mut self, left: Type, right: Type) -> Result<Type, InferenceError> {
527541
let left = self.shallow(left);
528542
let right = self.shallow(right);
543+
if self.is_error_type(left) || self.is_error_type(right) {
544+
return Ok(self.error_type());
545+
}
529546
match (left, right) {
530547
(Type::Variable(left), Type::Variable(right)) => self.unify_variables(left, right),
531548
(Type::Variable(variable), ty) | (ty, Type::Variable(variable)) => {
@@ -580,7 +597,11 @@ impl InferenceContext {
580597
ty: Type,
581598
requirements: Requirements,
582599
) -> Result<(), InferenceError> {
583-
match self.shallow(ty) {
600+
let ty = self.shallow(ty);
601+
if self.is_error_type(ty) {
602+
return Ok(());
603+
}
604+
match ty {
584605
Type::Variable(variable) => {
585606
let variable = self.root(variable);
586607
let combined =
@@ -1347,6 +1368,12 @@ fn type_meets_requirements(
13471368
if matches!(ty, Type::Variable(_)) {
13481369
return true;
13491370
}
1371+
if matches!(
1372+
ty,
1373+
Type::Known(id) if matches!(types.kind(id), TypeKind::Error)
1374+
) {
1375+
return true;
1376+
}
13501377
requirements
13511378
.0
13521379
.iter()

‎src/typeck.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,9 @@ impl Checker {
325325
};
326326
let expected = self.shallow_type(expected);
327327
let actual_shallow = self.shallow_type(actual);
328+
if self.is_error_type(actual_shallow) || self.is_error_type(expected) {
329+
return Some(self.error_type());
330+
}
328331
let none = self.core_type(crate::stdlib::CoreTypeId::None);
329332
let nested_wrapper_lift = match (expected, actual_shallow) {
330333
(Type::Option(option), Type::Result(_)) => matches!(
@@ -417,6 +420,14 @@ impl Checker {
417420
self.inference.shallow(ty)
418421
}
419422

423+
fn error_type(&mut self) -> Type {
424+
self.inference.error_type()
425+
}
426+
427+
fn is_error_type(&mut self, ty: Type) -> bool {
428+
self.inference.is_error_type(ty)
429+
}
430+
420431
fn unify(&mut self, left: Type, right: Type, span: Span) -> Option<Type> {
421432
match self.inference.unify(left, right) {
422433
Ok(ty) => Some(ty),

‎src/typeck/body_pass.rs‎

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -59,43 +59,44 @@ fn check_global_initializers(checker: &mut Checker, program: &Program) {
5959
);
6060
let run_scoped_initializer = checker.semantics.standard_library_item(global.value.id)
6161
== Some(crate::stdlib::StdlibItemId::SetNew);
62-
if !constant_initializer && !run_scoped_initializer {
62+
let initializer_checked = inferred.is_some();
63+
if initializer_checked && !constant_initializer && !run_scoped_initializer {
6364
checker.error(
6465
"global initializers must be literal values composed from None, numbers, booleans, strings, payload-free enums, records, or arrays, or a run-scoped Set.new value",
6566
global.value.span,
6667
);
6768
}
68-
if let Some(ty) = inferred {
69-
let unsupported_standard = checker.standard_type_id(ty).is_some_and(|standard| {
70-
standard != StdlibTypeId::String
71-
&& !checker
72-
.standard_library
73-
.type_decl(standard)
74-
.value_usage
75-
.global_variable
76-
});
77-
if unsupported_standard
78-
|| matches!(ty, Type::Result(_))
79-
|| matches!(ty, Type::Option(_))
80-
&& !matches!(global.value.kind, crate::ast::ExprKind::None)
81-
{
82-
let ty = checker.type_name(ty);
83-
checker.error(
84-
format!("global variables cannot currently store `{ty}`"),
85-
global.span,
86-
);
87-
}
88-
checker.semantics.resolve_value_type(global.id, ty);
89-
checker.declarations.globals.insert(
90-
global.name.clone(),
91-
Binding {
92-
id: Some(global.id),
93-
ty,
94-
mutable: global.mutable,
95-
debug_only: global.debug_only,
96-
},
69+
let mut ty = inferred.unwrap_or_else(|| checker.error_type());
70+
let unsupported_standard = checker.standard_type_id(ty).is_some_and(|standard| {
71+
standard != StdlibTypeId::String
72+
&& !checker
73+
.standard_library
74+
.type_decl(standard)
75+
.value_usage
76+
.global_variable
77+
});
78+
if unsupported_standard
79+
|| matches!(ty, Type::Result(_))
80+
|| matches!(ty, Type::Option(_))
81+
&& !matches!(global.value.kind, crate::ast::ExprKind::None)
82+
{
83+
let name = checker.type_name(ty);
84+
checker.error(
85+
format!("global variables cannot currently store `{name}`"),
86+
global.span,
9787
);
88+
ty = checker.error_type();
9889
}
90+
checker.semantics.resolve_value_type(global.id, ty);
91+
checker.declarations.globals.insert(
92+
global.name.clone(),
93+
Binding {
94+
id: Some(global.id),
95+
ty,
96+
mutable: global.mutable,
97+
debug_only: global.debug_only,
98+
},
99+
);
99100
}
100101
}
101102

‎src/typeck/call_resolution.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,12 @@ impl Checker {
372372
}
373373
let (method, receiver_path) = callee.split_last().unwrap();
374374
let receiver = self.path(receiver_path, span, None)?;
375+
if self.is_error_type(receiver.ty) {
376+
for argument in args {
377+
self.expr(argument, None);
378+
}
379+
return self.expect_expression(expression, receiver.ty, expected, span);
380+
}
375381
let receiver_value = receiver
376382
.value
377383
.expect("method receiver paths resolve to a declaration or snapshot value");
@@ -500,6 +506,12 @@ impl Checker {
500506
let standard_library = self.standard_library.clone();
501507
let base_type = self.expr(written_receiver, None)?;
502508
let base_type = self.shallow_type(base_type);
509+
if self.is_error_type(base_type) {
510+
for argument in args {
511+
self.expr(argument, None);
512+
}
513+
return self.expect_expression(expression, base_type, expected, span);
514+
}
503515
let method = callee.last().expect("postfix calls name a method");
504516
let (receiver_type, receiver_members) =
505517
self.resolve_members(base_type, &callee[..callee.len() - 1], span)?;
@@ -1231,6 +1243,9 @@ impl Checker {
12311243
if fields.is_empty() {
12321244
return Some((ty, Some(Vec::new())));
12331245
}
1246+
if self.is_error_type(ty) {
1247+
return Some((ty, None));
1248+
}
12341249
if matches!(self.shallow_type(ty), Type::Variable(_))
12351250
&& let Some(expression) = expression
12361251
{

‎src/typeck/expressions.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,10 @@ impl Checker {
749749
bracket_span,
750750
} => {
751751
let receiver_ty = self.expr(receiver, None)?;
752+
if self.is_error_type(receiver_ty) {
753+
self.expr(index, None);
754+
return self.expect_expression(expr.id, receiver_ty, expected, expr.span);
755+
}
752756
let element = match self.shallow_type(receiver_ty) {
753757
Type::Array(array) => self.inference.array_element(array),
754758
Type::Known(id) => match self.inference.type_store().kind(id) {
@@ -1018,6 +1022,15 @@ impl Checker {
10181022
};
10191023
let operand_ty = self.unify(left_ty, right_ty, span)?;
10201024

1025+
if self.is_error_type(operand_ty) {
1026+
let result = if result_is_bool {
1027+
self.core_type(crate::stdlib::CoreTypeId::Bool)
1028+
} else {
1029+
operand_ty
1030+
};
1031+
return self.expect_expression(expression, result, expected, span);
1032+
}
1033+
10211034
if let Some(result) =
10221035
self.resolve_binary_operator(op, operand_ty, expression, left.id, span)
10231036
{

‎src/typeck/statements.rs‎

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ impl Checker {
303303
self.unify(result, expected, value.span)
304304
})
305305
});
306-
if let (Some(binding), Some(ty)) = (binding, result) {
306+
if let Some(binding) = binding {
307+
let ty = result.unwrap_or_else(|| self.error_type());
307308
let duplicate = self
308309
.scopes
309310
.iter()
@@ -397,33 +398,34 @@ impl Checker {
397398
);
398399
}
399400
let expected = variable.annotation.map(|ty| self.syntax_type(ty));
400-
if let Some(ty) = self.expr(&variable.value, expected) {
401-
let unsupported_standard = self.standard_type_id(ty).is_some_and(|standard| {
402-
!self
403-
.standard_library
404-
.type_decl(standard)
405-
.value_usage
406-
.local_variable
407-
});
408-
if unsupported_standard {
409-
let ty = self.type_name(ty);
410-
self.error(
411-
format!("local variables cannot currently store `{ty}`"),
412-
variable.span,
413-
);
414-
return;
415-
}
416-
self.semantics.resolve_value_type(variable.id, ty);
417-
self.scopes.last_mut().unwrap().insert(
418-
variable.name.clone(),
419-
Binding {
420-
id: Some(variable.id),
421-
ty,
422-
mutable: variable.mutable,
423-
debug_only: self.debug_context.is_debug() || variable.debug_only,
424-
},
401+
let mut ty = self
402+
.expr(&variable.value, expected)
403+
.unwrap_or_else(|| self.error_type());
404+
let unsupported_standard = self.standard_type_id(ty).is_some_and(|standard| {
405+
!self
406+
.standard_library
407+
.type_decl(standard)
408+
.value_usage
409+
.local_variable
410+
});
411+
if unsupported_standard {
412+
let name = self.type_name(ty);
413+
self.error(
414+
format!("local variables cannot currently store `{name}`"),
415+
variable.span,
425416
);
417+
ty = self.error_type();
426418
}
419+
self.semantics.resolve_value_type(variable.id, ty);
420+
self.scopes.last_mut().unwrap().insert(
421+
variable.name.clone(),
422+
Binding {
423+
id: Some(variable.id),
424+
ty,
425+
mutable: variable.mutable,
426+
debug_only: self.debug_context.is_debug() || variable.debug_only,
427+
},
428+
);
427429
}
428430

429431
pub(super) fn binding(&self, name: &str) -> Option<Binding> {

0 commit comments

Comments
 (0)