Add support for destructuring def

This commit introduces the `DefDestructure` bound kind and modifies the
binder, analyzer, type checker, and VM to support destructuring in `def`
statements. This allows for pattern matching on the right-hand side of a
`def` to bind multiple variables.

The parser has been updated to accept patterns in `def` statements. The
binder now handles `UntypedKind::Def` with a `target` pattern, rather
than a simple `name`. This enables destructuring.

The `Gemini.md` documentation has been updated to include a new rule for
incremental development.
This commit is contained in:
Michael Schimmel
2026-02-24 08:40:45 +01:00
parent eeb6621280
commit 252b725677
13 changed files with 290 additions and 117 deletions
+12
View File
@@ -178,6 +178,18 @@ impl<'a> Analyzer<'a> {
)
}
BoundKind::DefDestructure { pattern, value } => {
let pat_m = self.visit(Rc::new((**pattern).clone()));
let val_m = self.visit(Rc::new((**value).clone()));
(
BoundKind::DefDestructure {
pattern: Box::new(pat_m),
value: Box::new(val_m),
},
Purity::Impure,
)
}
BoundKind::Call { callee, args } => {
let callee_m = self.visit(Rc::new((**callee).clone()));
let args_m = self.visit(Rc::new((**args).clone()));
+104 -56
View File
@@ -128,23 +128,8 @@ impl Binder {
))
}
UntypedKind::Parameter(sym) => {
// Parameters are ONLY allowed if we are inside a function (Binder stack > 1)
if self.functions.len() <= 1 {
return Err(format!(
"Parameter '{}' is not allowed in root scope.",
sym.name
));
}
let current_fn = self.functions.last_mut().unwrap();
let slot = current_fn.scope.define(sym)?;
Ok(self.make_node(
node.identity.clone(),
BoundKind::Parameter {
name: sym.clone(),
slot,
},
))
UntypedKind::Parameter(_) => {
Err("Unexpected 'Parameter' node in general binder context. This should be handled via 'bind_pattern'.".to_string())
}
UntypedKind::If {
@@ -169,52 +154,66 @@ impl Binder {
))
}
UntypedKind::Def { name, value } => {
// 1. Pre-declare name to support recursion
let slot_or_idx = if self.functions.len() == 1 {
let mut globals = self.globals.borrow_mut();
if globals.contains_key(name) {
return Err(format!(
"Global variable '{}' is already defined.",
name.name
));
UntypedKind::Def { target, value } => {
// Special case: Single identifier (to support recursion)
if let UntypedKind::Parameter(ref name) = target.kind {
let slot_or_idx = if self.functions.len() == 1 {
let mut globals = self.globals.borrow_mut();
if globals.contains_key(name) {
return Err(format!(
"Global variable '{}' is already defined.",
name.name
));
}
let idx = globals.len() as u32;
globals.insert(name.clone(), idx);
idx
} else {
let current_fn = self.functions.last_mut().unwrap();
current_fn.scope.define(name)?
};
let val_node = self.bind(value)?;
if self.functions.len() == 1 {
Ok(self.make_node(
node.identity.clone(),
BoundKind::DefGlobal {
name: name.clone(),
global_index: slot_or_idx,
value: Box::new(val_node),
},
))
} else {
let captured_by = self
.capture_map
.get(&target.identity)
.cloned()
.unwrap_or_default();
Ok(self.make_node(
node.identity.clone(),
BoundKind::DefLocal {
name: name.clone(),
slot: slot_or_idx,
value: Box::new(val_node),
captured_by,
},
))
}
let idx = globals.len() as u32;
globals.insert(name.clone(), idx);
idx
} else {
let current_fn = self.functions.last_mut().unwrap();
current_fn.scope.define(name)?
};
// Complex Destructuring Pattern
// NOTE: Destructuring definitions are NOT recursive by default
// (the variables are only available AFTER the definition)
let val_node = self.bind(value)?;
let target_node = self.bind_pattern(target)?;
// 2. Bind Value (now 'name' is visible)
let val_node = self.bind(value)?;
// 3. Return Node
if self.functions.len() == 1 {
Ok(self.make_node(
node.identity.clone(),
BoundKind::DefGlobal {
name: name.clone(),
global_index: slot_or_idx,
BoundKind::DefDestructure {
pattern: Box::new(target_node),
value: Box::new(val_node),
},
))
} else {
let captured_by = self
.capture_map
.get(&node.identity)
.cloned()
.unwrap_or_default();
Ok(self.make_node(
node.identity.clone(),
BoundKind::DefLocal {
name: name.clone(),
slot: slot_or_idx,
value: Box::new(val_node),
captured_by,
},
))
}
}
@@ -240,7 +239,7 @@ impl Binder {
self.functions.push(FunctionCompiler::new());
// 1. Bind the parameter pattern/tuple
let params_bound = self.bind(params)?;
let params_bound = self.bind_pattern(params)?;
// 2. Bind the body
let body_bound = self.bind(body)?;
@@ -405,6 +404,55 @@ impl Binder {
Err(format!("Undefined variable '{}'", sym.name))
}
fn bind_pattern(&mut self, node: &Node<UntypedKind>) -> Result<BoundNode, String> {
match &node.kind {
UntypedKind::Parameter(sym) => {
if self.functions.len() == 1 {
// Global Definition in pattern
let mut globals = self.globals.borrow_mut();
if globals.contains_key(sym) {
return Err(format!("Global variable '{}' is already defined.", sym.name));
}
let idx = globals.len() as u32;
globals.insert(sym.clone(), idx);
// For Global Destructuring, we return a DefGlobal with Nop value as it's a pattern part
Ok(self.make_node(
node.identity.clone(),
BoundKind::DefGlobal {
name: sym.clone(),
global_index: idx,
value: Box::new(self.make_node(node.identity.clone(), BoundKind::Nop)),
},
))
} else {
// Local Parameter/Definition in pattern
let current_fn = self.functions.last_mut().unwrap();
let slot = current_fn.scope.define(sym)?;
Ok(self.make_node(
node.identity.clone(),
BoundKind::Parameter {
name: sym.clone(),
slot,
},
))
}
}
UntypedKind::Tuple { elements } => {
let mut bound_elems = Vec::new();
for e in elements {
bound_elems.push(self.bind_pattern(e)?);
}
Ok(self.make_node(
node.identity.clone(),
BoundKind::Tuple {
elements: bound_elems,
},
))
}
_ => Err(format!("Invalid node in pattern: {:?}", node.kind)),
}
}
fn make_node(&self, identity: Identity, kind: BoundKind<()>) -> BoundNode {
Node {
identity,
+17
View File
@@ -82,6 +82,12 @@ pub enum BoundKind<T = ()> {
value: Box<BoundNode<T>>,
},
/// A destructuring definition (can be local or global depending on the pattern)
DefDestructure {
pattern: Box<BoundNode<T>>,
value: Box<BoundNode<T>>,
},
Lambda {
params: Rc<BoundNode<T>>,
// The list of variables captured from enclosing scopes
@@ -187,6 +193,16 @@ where
value: vb,
},
) => na == nb && ga == gb && va == vb,
(
BoundKind::DefDestructure {
pattern: pa,
value: va,
},
BoundKind::DefDestructure {
pattern: pb,
value: vb,
},
) => pa == pb && va == vb,
(
BoundKind::Lambda {
params: pa,
@@ -249,6 +265,7 @@ impl<T> BoundKind<T> {
BoundKind::DefGlobal {
name, global_index, ..
} => format!("DEF_GLOBAL({}, Idx:{})", name.name, global_index),
BoundKind::DefDestructure { .. } => "DEF_DESTRUCTURE".to_string(),
BoundKind::Lambda {
params, upvalues, ..
} => {
+12
View File
@@ -120,6 +120,18 @@ impl Dumper {
self.indent -= 1;
}
BoundKind::DefDestructure { pattern, value } => {
self.log("DefDestructure", node);
self.indent += 1;
self.write_indent();
self.output.push_str("Pattern:\n");
self.visit(pattern);
self.write_indent();
self.output.push_str("Value:\n");
self.visit(value);
self.indent -= 1;
}
BoundKind::If {
cond,
then_br,
+13 -8
View File
@@ -186,12 +186,13 @@ impl<E: MacroEvaluator> MacroExpander<E> {
})
}
UntypedKind::Def { name, value } => {
UntypedKind::Def { target, value } => {
let target = self.expand_recursive(*target)?;
let value = self.expand_recursive(*value)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Def {
name,
target: Box::new(target),
value: Box::new(value),
},
ty: (),
@@ -379,13 +380,13 @@ impl<E: MacroEvaluator> MacroExpander<E> {
Ok(self.value_to_node(val, node.identity))
}
UntypedKind::Def { mut name, value } => {
name.context = Some(state.expansion_id.clone());
UntypedKind::Def { target, value } => {
let target = self.expand_template(*target, state)?;
let val = self.expand_template(*value, state)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Def {
name,
target: Box::new(target),
value: Box::new(val),
},
ty: (),
@@ -613,9 +614,13 @@ mod tests {
call,
} = &exprs[1].kind
{
if let UntypedKind::Def { name, .. } = &result.kind {
assert_eq!(name.context, Some(call.identity.clone()));
assert_eq!(name.name.as_ref(), "y");
if let UntypedKind::Def { target, .. } = &result.kind {
if let UntypedKind::Parameter(sym) = &target.kind {
assert_eq!(sym.context, Some(call.identity.clone()));
assert_eq!(sym.name.as_ref(), "y");
} else {
panic!("Expected Parameter target, got {:?}", target.kind);
}
} else {
panic!("Expected Def result, got {:?}", result.kind);
}
+4
View File
@@ -118,6 +118,10 @@ impl TCO {
global_index: *global_index,
value: Box::new(Self::transform(Rc::new((**value).clone()), false)),
},
BoundKind::DefDestructure { pattern, value } => BoundKind::DefDestructure {
pattern: Box::new(Self::transform(Rc::new((**pattern).clone()), false)),
value: Box::new(Self::transform(Rc::new((**value).clone()), false)),
},
BoundKind::Record { fields } => {
let new_fields = fields
.iter()
+34
View File
@@ -144,6 +144,27 @@ impl TypeChecker {
ctx.set_local_type(slot, specialized_ty.clone());
(BoundKind::Parameter { name, slot }, specialized_ty.clone())
}
BoundKind::DefGlobal {
name,
global_index,
..
} => {
self.global_types
.borrow_mut()
.insert(global_index, specialized_ty.clone());
(
BoundKind::DefGlobal {
name,
global_index,
value: Box::new(Node {
identity: node.identity.clone(),
kind: BoundKind::Nop,
ty: StaticType::Void,
}),
},
specialized_ty.clone(),
)
}
BoundKind::Tuple { elements } => {
let mut typed_elements = Vec::new();
let mut elem_types = Vec::new();
@@ -263,6 +284,19 @@ impl TypeChecker {
)
}
BoundKind::DefDestructure { pattern, value } => {
let val_typed = self.check_node(*value, ctx)?;
let pat_typed = self.check_params(*pattern, &val_typed.ty, ctx)?;
let ty = val_typed.ty.clone();
(
BoundKind::DefDestructure {
pattern: Box::new(pat_typed),
value: Box::new(val_typed),
},
ty,
)
}
BoundKind::If {
cond,
then_br,
+2 -2
View File
@@ -58,10 +58,10 @@ impl UpvalueAnalyzer {
}
}
}
UntypedKind::Def { name, value } => {
UntypedKind::Def { target, value } => {
Self::visit(value, scopes, capture_map, current_lambda.clone());
if let Some(current) = scopes.last_mut() {
current.insert(name.clone(), node.identity.clone());
Self::visit_params(target, current);
}
}
UntypedKind::Lambda { params, body } => {