Refactor Binder to use Diagnostics

The Binder and its helper functions have been updated to accept and
propagate a `Diagnostics` struct. This allows for better error reporting
during the binding phase, enabling the compiler to continue processing
even after encountering errors and collect all issues before halting.

The `BoundKind::Error` node and `StaticType::Error` are introduced as
"poison" nodes/types. These nodes indicate that an error occurred during
compilation, preventing further valid processing of that specific AST
fragment but allowing the compiler to continue with other parts of the
code.

The `Parser` has also been updated to return a `Diagnostics` struct,
enabling it to report errors during the initial parsing stage while
still attempting to build a partial AST. This adheres to the same error
recovery strategy.
This commit is contained in:
Michael Schimmel
2026-03-02 23:12:51 +01:00
parent f7cb6655af
commit 5bc69c267b
19 changed files with 2678 additions and 2334 deletions
+1
View File
@@ -306,6 +306,7 @@ impl<'a> Analyzer<'a> {
}
BoundKind::Extension(_) => (BoundKind::Nop, Purity::Impure),
BoundKind::Error => (BoundKind::Error, Purity::Impure),
};
crate::ast::nodes::Node {
+708 -665
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -161,6 +161,9 @@ pub enum BoundKind<T = ()> {
bound_expanded: Box<BoundNode<T>>,
},
/// A diagnostic poison node, allowing compilation to continue after an error.
Error,
/// DSL-specific extension slot
Extension(Box<dyn BoundExtension<T>>),
}
@@ -260,14 +263,15 @@ where
}
(
BoundKind::Expansion {
original_call: oa,
bound_expanded: ba,
original_call: ca,
bound_expanded: ea,
},
BoundKind::Expansion {
original_call: ob,
bound_expanded: bb,
original_call: cb,
bound_expanded: eb,
},
) => Rc::ptr_eq(oa, ob) && ba == bb,
) => Rc::ptr_eq(ca, cb) && ea == eb,
(BoundKind::Error, BoundKind::Error) => true,
(BoundKind::Extension(_), BoundKind::Extension(_)) => false, // Currently no equality for extensions
_ => false,
}
@@ -314,6 +318,7 @@ impl<T> BoundKind<T> {
BoundKind::Record { values, .. } => format!("RECORD({})", values.len()),
BoundKind::Expansion { .. } => "EXPANSION".to_string(),
BoundKind::Extension(ext) => ext.display_name(),
BoundKind::Error => "ERROR".to_string(),
}
}
}
+2 -1
View File
@@ -141,7 +141,8 @@ impl CapturePass {
BoundKind::Nop
| BoundKind::Constant(_)
| BoundKind::Get { .. }
| BoundKind::Extension(_) => {}
| BoundKind::Extension(_)
| BoundKind::Error => {}
}
node
}
+3
View File
@@ -256,6 +256,9 @@ impl Dumper {
BoundKind::Extension(ext) => {
self.log(&ext.display_name(), node);
}
BoundKind::Error => {
self.log("ERROR_NODE", node);
}
}
}
}
+819 -816
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -158,7 +158,7 @@ impl UsageInfo {
}
self.collect(lambda);
}
BoundKind::Nop | BoundKind::FieldAccessor(_) | BoundKind::Extension(_) => {}
BoundKind::Nop | BoundKind::FieldAccessor(_) | BoundKind::Extension(_) | BoundKind::Error => {}
}
}
+1
View File
@@ -167,6 +167,7 @@ impl TCO {
)),
},
BoundKind::Extension(_) => BoundKind::Nop,
BoundKind::Error => BoundKind::Error,
};
Node {
+65 -64
View File
@@ -1,4 +1,5 @@
use crate::ast::compiler::bound_nodes::{Address, BoundKind, BoundNode, GlobalIdx, TypedNode};
use crate::ast::diagnostics::Diagnostics;
use crate::ast::nodes::Node;
use crate::ast::types::StaticType;
use std::collections::HashMap;
@@ -78,7 +79,7 @@ impl TypeChecker {
Self { global_types }
}
pub fn check(&self, node: BoundNode, arg_types: &[StaticType]) -> Result<TypedNode, String> {
pub fn check(&self, node: BoundNode, arg_types: &[StaticType], diag: &mut Diagnostics) -> TypedNode {
match node.kind {
BoundKind::Lambda {
params,
@@ -104,11 +105,10 @@ impl TypeChecker {
StaticType::Tuple(arg_types.to_vec())
};
let params_typed =
self.check_params(params.as_ref().clone(), &arg_tuple_ty, &mut lambda_ctx)?;
let params_typed = self.check_params(params.as_ref().clone(), &arg_tuple_ty, &mut lambda_ctx, diag);
// 4. Check body with the new types
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx, diag);
let ret_ty = body_typed.ty.clone();
// 5. Construct specialized function type
@@ -119,7 +119,7 @@ impl TypeChecker {
ret: ret_ty,
}));
Ok(Node {
Node {
identity: node.identity,
kind: BoundKind::Lambda {
params: Rc::new(params_typed),
@@ -128,7 +128,7 @@ impl TypeChecker {
positional_count,
},
ty: fn_ty,
})
}
}
_ => {
// Fallback: Wrap in implicit lambda
@@ -146,7 +146,7 @@ impl TypeChecker {
},
ty: (),
};
self.check(virtual_lambda, &[])
self.check(virtual_lambda, &[], diag)
}
}
}
@@ -156,7 +156,8 @@ impl TypeChecker {
node: BoundNode,
specialized_ty: &StaticType,
ctx: &mut TypeContext,
) -> Result<TypedNode, String> {
diag: &mut Diagnostics,
) -> TypedNode {
let (kind, ty) = match node.kind {
BoundKind::Define {
name,
@@ -208,12 +209,15 @@ impl TypeChecker {
| StaticType::Vector(_, _)
| StaticType::Matrix(_, _)
| StaticType::List(_)
| StaticType::Record(_) => {}
| StaticType::Record(_)
| StaticType::Error => {}
_ => {
return Err(format!(
"Cannot destructure type {} as a tuple/vector",
specialized_ty
));
diag.push_error(format!("Cannot destructure type {} as a tuple/vector", specialized_ty), Some(node.identity.clone()));
return Node {
identity: node.identity,
kind: BoundKind::Error,
ty: StaticType::Error,
};
}
}
@@ -231,9 +235,10 @@ impl TypeChecker {
.get(i)
.map(|(_, ty): &(crate::ast::types::Keyword, StaticType)| ty.clone())
.unwrap_or(StaticType::Any),
StaticType::Error => StaticType::Error,
_ => StaticType::Any,
};
let t = self.check_params(el, &sub_ty, ctx)?;
let t = self.check_params(el, &sub_ty, ctx, diag);
elem_types.push(t.ty.clone());
typed_elements.push(t);
}
@@ -244,17 +249,21 @@ impl TypeChecker {
StaticType::Tuple(elem_types),
)
}
_ => return Err("Invalid node in parameter list".to_string()),
BoundKind::Error => (BoundKind::Error, StaticType::Error),
_ => {
diag.push_error("Invalid node in parameter list", Some(node.identity.clone()));
(BoundKind::Error, StaticType::Error)
}
};
Ok(Node {
Node {
identity: node.identity,
kind,
ty,
})
}
}
fn check_node(&self, node: BoundNode, ctx: &mut TypeContext) -> Result<TypedNode, String> {
fn check_node(&self, node: BoundNode, ctx: &mut TypeContext, diag: &mut Diagnostics) -> TypedNode {
let (kind, ty) = match node.kind {
BoundKind::Nop => (BoundKind::Nop, StaticType::Void),
@@ -270,7 +279,7 @@ impl TypeChecker {
value,
captured_by,
} => {
let val_typed = self.check_node(*value, ctx)?;
let val_typed = self.check_node(*value, ctx, diag);
let ty = val_typed.ty.clone();
ctx.set_type(addr, ty.clone());
@@ -300,22 +309,21 @@ impl TypeChecker {
BoundKind::FieldAccessor(k) => (BoundKind::FieldAccessor(k), StaticType::FieldAccessor(k)),
BoundKind::GetField { rec, field } => {
let rec_typed = self.check_node(*rec, ctx)?;
let rec_typed = self.check_node(*rec, ctx, diag);
let field_ty = match &rec_typed.ty {
StaticType::Record(layout) => {
if let Some(idx) = layout.index_of(field) {
layout.fields[idx].1.clone()
} else {
return Err(format!("Record does not have field :{}", field.name()));
diag.push_error(format!("Record does not have field :{}", field.name()), Some(rec_typed.identity.clone()));
StaticType::Error
}
}
StaticType::Any => StaticType::Any,
StaticType::Error => StaticType::Error,
_ => {
return Err(format!(
"Cannot access field :{} on non-record type {}",
field.name(),
rec_typed.ty
));
diag.push_error(format!("Cannot access field :{} on non-record type {}", field.name(), rec_typed.ty), Some(rec_typed.identity.clone()));
StaticType::Error
}
};
(
@@ -328,7 +336,7 @@ impl TypeChecker {
}
BoundKind::Set { addr, value } => {
let val_typed = self.check_node(*value, ctx)?;
let val_typed = self.check_node(*value, ctx, diag);
let ty = val_typed.ty.clone();
ctx.set_type(addr, ty.clone());
@@ -342,8 +350,8 @@ impl TypeChecker {
}
BoundKind::Destructure { pattern, value } => {
let val_typed = self.check_node(*value, ctx)?;
let pat_typed = self.check_params(*pattern, &val_typed.ty, ctx)?;
let val_typed = self.check_node(*value, ctx, diag);
let pat_typed = self.check_params(*pattern, &val_typed.ty, ctx, diag);
let ty = val_typed.ty.clone();
(
BoundKind::Destructure {
@@ -359,14 +367,14 @@ impl TypeChecker {
then_br,
else_br,
} => {
let cond_typed = self.check_node(*cond, ctx)?;
let then_typed = self.check_node(*then_br, ctx)?;
let cond_typed = self.check_node(*cond, ctx, diag);
let then_typed = self.check_node(*then_br, ctx, diag);
let mut else_typed = None;
let mut final_ty = then_typed.ty.clone();
if let Some(e) = else_br {
let et = self.check_node(*e, ctx)?;
let et = self.check_node(*e, ctx, diag);
// Basic type promotion: if types differ, fall back to Any
if et.ty != final_ty {
final_ty = StaticType::Any;
@@ -391,7 +399,7 @@ impl TypeChecker {
let mut typed_inputs = Vec::with_capacity(inputs.len());
let mut arg_types = Vec::with_capacity(inputs.len());
for input in inputs {
let typed_input = self.check_node(input, ctx)?;
let typed_input = self.check_node(input, ctx, diag);
// Unwrap Series(T) to T for the lambda argument
let arg_ty = if let StaticType::Series(inner) = &typed_input.ty {
*inner.clone()
@@ -403,7 +411,7 @@ impl TypeChecker {
}
// Specialized check for the lambda using the input types!
let typed_lambda = self.check(*lambda, &arg_types)?;
let typed_lambda = self.check(*lambda, &arg_types, diag);
let ret_ty = if let StaticType::Function(sig) = &typed_lambda.ty {
// If the lambda returns an Optional(T), the pipeline filters Void and stores T!
@@ -430,7 +438,7 @@ impl TypeChecker {
let mut last_ty = StaticType::Void;
for e in exprs {
let t = self.check_node(e, ctx)?;
let t = self.check_node(e, ctx, diag);
last_ty = t.ty.clone();
typed_exprs.push(t);
}
@@ -455,13 +463,12 @@ impl TypeChecker {
TypeContext::new(64, upvalue_types, ctx.global_types, Some(ctx));
// 3. Check parameters and body
let params_typed =
self.check_params(params.as_ref().clone(), &StaticType::Any, &mut lambda_ctx)?;
let params_typed = self.check_params(params.as_ref().clone(), &StaticType::Any, &mut lambda_ctx, diag);
// Set current params type for 'again' validation
lambda_ctx.current_params_ty = Some(params_typed.ty.clone());
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx, diag);
let ret_ty = body_typed.ty.clone();
// 4. Construct function type
@@ -482,14 +489,14 @@ impl TypeChecker {
}
BoundKind::Call { callee, args } => {
let callee_typed = self.check_node(*callee, ctx)?;
let callee_typed = self.check_node(*callee, ctx, diag);
// Manually check args (Tuple) to prevent Vector/Matrix promotion
let args_typed = if let BoundKind::Tuple { elements } = args.kind {
let mut typed_elements = Vec::new();
let mut elem_types = Vec::new();
for e in elements {
let t = self.check_node(e, ctx)?;
let t = self.check_node(e, ctx, diag);
elem_types.push(t.ty.clone());
typed_elements.push(t);
}
@@ -502,16 +509,14 @@ impl TypeChecker {
}
} else {
// Should not happen as parser wraps args in Tuple, but fallback safely
self.check_node(*args, ctx)?
self.check_node(*args, ctx, diag)
};
let ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
Some(ty) => ty,
None => {
return Err(format!(
"Invalid arguments for function call. Expected {}, got {}",
callee_typed.ty, args_typed.ty
));
diag.push_error(format!("Invalid arguments for function call. Expected {}, got {}", callee_typed.ty, args_typed.ty), Some(node.identity.clone()));
StaticType::Error
}
};
@@ -530,7 +535,7 @@ impl TypeChecker {
let mut typed_elements = Vec::new();
let mut elem_types = Vec::new();
for e in elements {
let t = self.check_node(e, ctx)?;
let t = self.check_node(e, ctx, diag);
elem_types.push(t.ty.clone());
typed_elements.push(t);
}
@@ -542,16 +547,13 @@ impl TypeChecker {
ty: StaticType::Tuple(elem_types),
}
} else {
self.check_node(*args, ctx)?
self.check_node(*args, ctx, diag)
};
if let Some(expected_ty) = &ctx.current_params_ty
&& !expected_ty.is_assignable_from(&args_typed.ty)
{
return Err(format!(
"Type mismatch in 'again' call: expected {}, but got {}",
expected_ty, args_typed.ty
));
diag.push_error(format!("Type mismatch in 'again' call: expected {}, but got {}", expected_ty, args_typed.ty), Some(node.identity.clone()));
}
(
@@ -565,7 +567,7 @@ impl TypeChecker {
BoundKind::Tuple { elements } => {
let mut typed_elements = Vec::new();
for e in elements {
typed_elements.push(self.check_node(e, ctx)?);
typed_elements.push(self.check_node(e, ctx, diag));
}
let ty = if typed_elements.is_empty() {
@@ -606,7 +608,7 @@ impl TypeChecker {
let mut fields_ty = Vec::with_capacity(values.len());
for (i, v) in values.into_iter().enumerate() {
let vt = self.check_node(v, ctx)?;
let vt = self.check_node(v, ctx, diag);
fields_ty.push((layout.fields[i].0, vt.ty.clone()));
typed_values.push(vt);
}
@@ -625,7 +627,7 @@ impl TypeChecker {
original_call,
bound_expanded,
} => {
let expanded_typed = self.check_node(*bound_expanded, ctx)?;
let expanded_typed = self.check_node(*bound_expanded, ctx, diag);
let ty = expanded_typed.ty.clone();
(
BoundKind::Expansion {
@@ -637,18 +639,17 @@ impl TypeChecker {
}
BoundKind::Extension(_ext) => {
return Err(format!(
"TypeChecking for extension '{}' not implemented",
_ext.display_name()
));
diag.push_error(format!("TypeChecking for extension '{}' not implemented", _ext.display_name()), Some(node.identity.clone()));
(BoundKind::Error, StaticType::Error)
}
BoundKind::Error => (BoundKind::Error, StaticType::Error),
};
Ok(Node {
Node {
identity: node.identity,
kind,
ty,
})
}
}
}
@@ -659,13 +660,13 @@ mod tests {
fn check_source(source: &str) -> TypedNode {
let env = crate::ast::environment::Environment::new();
env.compile(source).unwrap()
env.compile(source).into_result().unwrap()
}
#[test]
fn test_destructuring_scalar_error() {
let env = crate::ast::environment::Environment::new();
let result = env.compile("(def [x] 5)");
let result = env.compile("(def [x] 5)").into_result();
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
@@ -677,7 +678,7 @@ mod tests {
fn test_call_argument_mismatch() {
let env = crate::ast::environment::Environment::new();
// fn takes 1 arg, called with 0
let result = env.compile("(do (def f (fn [x] x)) (f))");
let result = env.compile("(do (def f (fn [x] x)) (f))").into_result();
assert!(result.is_err());
assert!(
result
@@ -690,7 +691,7 @@ mod tests {
fn test_destructuring_vector_args() {
let env = crate::ast::environment::Environment::new();
// ((fn [[x y]] (+ x y)) [10 20]) -> 30
let result = env.compile("((fn [[x y]] (+ x y)) [10 20])");
let result = env.compile("((fn [[x y]] (+ x y)) [10 20])").into_result();
assert!(result.is_ok());
assert_eq!(get_ret_type(&result.unwrap()), StaticType::Int);
}