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
+1
View File
@@ -37,6 +37,7 @@ Wichtig: zentraler Einstiegspunkt ist Delphi/Myc.Ast.Environment.pasund die dort
* Ich kann Rust noch nicht so gut. Erkläre mir, was du machst und welche Konzepte du nutzt. Ich will was dazulernen.
* Wenn clippy etwas vorschlägt, von dem du glaubst, dass es nicht stabil ist, dann überprüfe die aktuelle Version. Wahrscheinlich ist das Feature mittlerweile im trunk. Wir gehen immer davon aus, dass clippy recht hat.
* Keine Interaktion mit GIT. Kein Commit, oder ähnliches. Schlage auch nichts dergleichen vor.
* Gehe immer schrittweise vor. Zeige mir, was du vor hast, bevor du Code erzeugst.
## Testing & Debugging
+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 } => {
+1 -1
View File
@@ -71,7 +71,7 @@ pub enum UntypedKind {
else_br: Option<Box<Node<UntypedKind>>>,
},
Def {
name: Symbol,
target: Box<Node<UntypedKind>>,
value: Box<Node<UntypedKind>>,
},
Assign {
+41 -41
View File
@@ -185,17 +185,12 @@ impl<'a> Parser<'a> {
}
fn parse_def(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
let name_node = self.parse_expression()?;
let name = match name_node.kind {
UntypedKind::Identifier(sym) => sym,
_ => return Err("Expected identifier for def name".to_string()),
};
let target = Box::new(self.parse_pattern()?);
let value = Box::new(self.parse_expression()?);
Ok(Node {
identity,
kind: UntypedKind::Def { name, value },
kind: UntypedKind::Def { target, value },
ty: (),
})
}
@@ -264,44 +259,49 @@ impl<'a> Parser<'a> {
self.peek()
));
}
let token = self.advance()?;
let identity = Rc::new(NodeIdentity {
location: token.location,
});
self.parse_pattern()
}
let mut elements = Vec::new();
while *self.peek() != TokenKind::RightBracket {
let next_peek = self.peek();
match next_peek {
TokenKind::Identifier(name) => {
let name = name.clone();
let token = self.advance()?;
let p_identity = Rc::new(NodeIdentity {
fn parse_pattern(&mut self) -> Result<Node<UntypedKind>, String> {
let next = self.peek();
match next {
TokenKind::Identifier(_) => {
let token = self.advance()?;
let sym: Symbol = match token.kind {
TokenKind::Identifier(s) => s.into(),
_ => unreachable!(),
};
Ok(Node {
identity: Rc::new(NodeIdentity {
location: token.location,
});
elements.push(Node {
identity: p_identity,
kind: UntypedKind::Parameter(name.into()),
ty: (),
});
}
TokenKind::LeftBracket => {
elements.push(self.parse_param_vector()?);
}
_ => {
return Err(format!(
"Expected identifier or nested parameter vector, found {:?}",
next_peek
));
}
}),
kind: UntypedKind::Parameter(sym),
ty: (),
})
}
TokenKind::LeftBracket => {
let token = self.advance()?;
let identity = Rc::new(NodeIdentity {
location: token.location,
});
let mut elements = Vec::new();
while *self.peek() != TokenKind::RightBracket {
elements.push(self.parse_pattern()?);
}
self.expect(TokenKind::RightBracket)?;
Ok(Node {
identity,
kind: UntypedKind::Tuple { elements },
ty: (),
})
}
_ => Err(format!(
"Expected identifier or pattern vector [...] for definition, found {:?}",
next
)),
}
self.expect(TokenKind::RightBracket)?;
Ok(Node {
identity,
kind: UntypedKind::Tuple { elements },
ty: (),
})
}
fn parse_call(
+25 -9
View File
@@ -281,6 +281,18 @@ impl VM {
globals[idx] = val.clone();
Ok(val)
}
BoundKind::DefDestructure { pattern, value } => {
let val = self.eval_internal(obs, value)?;
let mut offset = 0;
// Destructuring works on tuples/vectors, or single values wrapped in a slice
if let Some(vals) = val.as_slice() {
self.unpack(pattern, vals, &mut offset)?;
} else {
self.unpack(pattern, &[val.clone()], &mut offset)?;
}
Ok(val)
}
BoundKind::Get { addr, .. } => self.get_value(*addr),
BoundKind::Set { addr, value } => {
let val = self.eval_internal(obs, value)?;
@@ -622,9 +634,7 @@ impl VM {
fn flatten_value(val: Value, into: &mut Vec<Value>) {
if let Some(values) = val.as_slice() {
for item in values.iter() {
Self::flatten_value(item.clone(), into);
}
into.extend_from_slice(values);
} else {
into.push(val);
}
@@ -654,12 +664,7 @@ impl VM {
into: &mut Vec<Value>,
) -> Result<(), String> {
for e in elements {
match &e.kind {
BoundKind::Tuple { elements: sub } => {
self.eval_and_flatten_internal(obs, sub, into)?
}
_ => into.push(self.eval_internal(obs, e)?),
}
into.push(self.eval_internal(obs, e)?);
}
Ok(())
}
@@ -685,6 +690,17 @@ impl VM {
}
Ok(())
}
BoundKind::DefGlobal { global_index, .. } => {
let val = values.get(*offset).cloned().unwrap_or(Value::Void);
*offset += 1;
let idx = *global_index as usize;
let mut globals = self.globals.borrow_mut();
if idx >= globals.len() {
globals.resize(idx + 1, Value::Void);
}
globals[idx] = val;
Ok(())
}
BoundKind::Tuple { elements } => {
if let Some(sub_values) = values.get(*offset).and_then(|v| v.as_slice()) {
*offset += 1;
+24
View File
@@ -312,4 +312,28 @@ mod tests {
dump_record
);
}
#[test]
fn test_def_destructuring() {
let env = Environment::new();
// 1. Global destructuring
let source_global = "(do (def [a b] [1 2]) (+ a b))";
assert_eq!(format!("{}", env.run_script(source_global).unwrap()), "3");
// 2. Local nested destructuring inside a function
let source_local = "((fn [x] (do (def [a [[b c] d]] x) (+ a (+ b (+ c d))))) [1 [[2 3] 4]])";
assert_eq!(format!("{}", env.run_script(source_local).unwrap()), "10");
// 3. Verify 'def' returns the assigned value
let source_return = "(def [x y] [7 8])";
let res = env.run_script(source_return).unwrap();
if let Value::Tuple(vals) = res {
assert_eq!(vals.len(), 2);
assert_eq!(format!("{}", vals[0]), "7");
assert_eq!(format!("{}", vals[1]), "8");
} else {
panic!("Expected tuple return from def, got {:?}", res);
}
}
}