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. * 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. * 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. * 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 ## 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 } => { BoundKind::Call { callee, args } => {
let callee_m = self.visit(Rc::new((**callee).clone())); let callee_m = self.visit(Rc::new((**callee).clone()));
let args_m = self.visit(Rc::new((**args).clone())); let args_m = self.visit(Rc::new((**args).clone()));
+104 -56
View File
@@ -128,23 +128,8 @@ impl Binder {
)) ))
} }
UntypedKind::Parameter(sym) => { UntypedKind::Parameter(_) => {
// Parameters are ONLY allowed if we are inside a function (Binder stack > 1) Err("Unexpected 'Parameter' node in general binder context. This should be handled via 'bind_pattern'.".to_string())
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::If { UntypedKind::If {
@@ -169,52 +154,66 @@ impl Binder {
)) ))
} }
UntypedKind::Def { name, value } => { UntypedKind::Def { target, value } => {
// 1. Pre-declare name to support recursion // Special case: Single identifier (to support recursion)
let slot_or_idx = if self.functions.len() == 1 { if let UntypedKind::Parameter(ref name) = target.kind {
let mut globals = self.globals.borrow_mut(); let slot_or_idx = if self.functions.len() == 1 {
if globals.contains_key(name) { let mut globals = self.globals.borrow_mut();
return Err(format!( if globals.contains_key(name) {
"Global variable '{}' is already defined.", return Err(format!(
name.name "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 { } else {
let current_fn = self.functions.last_mut().unwrap(); // Complex Destructuring Pattern
current_fn.scope.define(name)? // 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( Ok(self.make_node(
node.identity.clone(), node.identity.clone(),
BoundKind::DefGlobal { BoundKind::DefDestructure {
name: name.clone(), pattern: Box::new(target_node),
global_index: slot_or_idx,
value: Box::new(val_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()); self.functions.push(FunctionCompiler::new());
// 1. Bind the parameter pattern/tuple // 1. Bind the parameter pattern/tuple
let params_bound = self.bind(params)?; let params_bound = self.bind_pattern(params)?;
// 2. Bind the body // 2. Bind the body
let body_bound = self.bind(body)?; let body_bound = self.bind(body)?;
@@ -405,6 +404,55 @@ impl Binder {
Err(format!("Undefined variable '{}'", sym.name)) 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 { fn make_node(&self, identity: Identity, kind: BoundKind<()>) -> BoundNode {
Node { Node {
identity, identity,
+17
View File
@@ -82,6 +82,12 @@ pub enum BoundKind<T = ()> {
value: Box<BoundNode<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 { Lambda {
params: Rc<BoundNode<T>>, params: Rc<BoundNode<T>>,
// The list of variables captured from enclosing scopes // The list of variables captured from enclosing scopes
@@ -187,6 +193,16 @@ where
value: vb, value: vb,
}, },
) => na == nb && ga == gb && va == 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 { BoundKind::Lambda {
params: pa, params: pa,
@@ -249,6 +265,7 @@ impl<T> BoundKind<T> {
BoundKind::DefGlobal { BoundKind::DefGlobal {
name, global_index, .. name, global_index, ..
} => format!("DEF_GLOBAL({}, Idx:{})", name.name, global_index), } => format!("DEF_GLOBAL({}, Idx:{})", name.name, global_index),
BoundKind::DefDestructure { .. } => "DEF_DESTRUCTURE".to_string(),
BoundKind::Lambda { BoundKind::Lambda {
params, upvalues, .. params, upvalues, ..
} => { } => {
+12
View File
@@ -120,6 +120,18 @@ impl Dumper {
self.indent -= 1; 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 { BoundKind::If {
cond, cond,
then_br, 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)?; let value = self.expand_recursive(*value)?;
Ok(Node { Ok(Node {
identity: node.identity, identity: node.identity,
kind: UntypedKind::Def { kind: UntypedKind::Def {
name, target: Box::new(target),
value: Box::new(value), value: Box::new(value),
}, },
ty: (), ty: (),
@@ -379,13 +380,13 @@ impl<E: MacroEvaluator> MacroExpander<E> {
Ok(self.value_to_node(val, node.identity)) Ok(self.value_to_node(val, node.identity))
} }
UntypedKind::Def { mut name, value } => { UntypedKind::Def { target, value } => {
name.context = Some(state.expansion_id.clone()); let target = self.expand_template(*target, state)?;
let val = self.expand_template(*value, state)?; let val = self.expand_template(*value, state)?;
Ok(Node { Ok(Node {
identity: node.identity, identity: node.identity,
kind: UntypedKind::Def { kind: UntypedKind::Def {
name, target: Box::new(target),
value: Box::new(val), value: Box::new(val),
}, },
ty: (), ty: (),
@@ -613,9 +614,13 @@ mod tests {
call, call,
} = &exprs[1].kind } = &exprs[1].kind
{ {
if let UntypedKind::Def { name, .. } = &result.kind { if let UntypedKind::Def { target, .. } = &result.kind {
assert_eq!(name.context, Some(call.identity.clone())); if let UntypedKind::Parameter(sym) = &target.kind {
assert_eq!(name.name.as_ref(), "y"); assert_eq!(sym.context, Some(call.identity.clone()));
assert_eq!(sym.name.as_ref(), "y");
} else {
panic!("Expected Parameter target, got {:?}", target.kind);
}
} else { } else {
panic!("Expected Def result, got {:?}", result.kind); panic!("Expected Def result, got {:?}", result.kind);
} }
+4
View File
@@ -118,6 +118,10 @@ impl TCO {
global_index: *global_index, global_index: *global_index,
value: Box::new(Self::transform(Rc::new((**value).clone()), false)), 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 } => { BoundKind::Record { fields } => {
let new_fields = fields let new_fields = fields
.iter() .iter()
+34
View File
@@ -144,6 +144,27 @@ impl TypeChecker {
ctx.set_local_type(slot, specialized_ty.clone()); ctx.set_local_type(slot, specialized_ty.clone());
(BoundKind::Parameter { name, 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 } => { BoundKind::Tuple { elements } => {
let mut typed_elements = Vec::new(); let mut typed_elements = Vec::new();
let mut elem_types = 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 { BoundKind::If {
cond, cond,
then_br, 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()); Self::visit(value, scopes, capture_map, current_lambda.clone());
if let Some(current) = scopes.last_mut() { if let Some(current) = scopes.last_mut() {
current.insert(name.clone(), node.identity.clone()); Self::visit_params(target, current);
} }
} }
UntypedKind::Lambda { params, body } => { UntypedKind::Lambda { params, body } => {
+1 -1
View File
@@ -71,7 +71,7 @@ pub enum UntypedKind {
else_br: Option<Box<Node<UntypedKind>>>, else_br: Option<Box<Node<UntypedKind>>>,
}, },
Def { Def {
name: Symbol, target: Box<Node<UntypedKind>>,
value: Box<Node<UntypedKind>>, value: Box<Node<UntypedKind>>,
}, },
Assign { Assign {
+41 -41
View File
@@ -185,17 +185,12 @@ impl<'a> Parser<'a> {
} }
fn parse_def(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> { fn parse_def(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
let name_node = self.parse_expression()?; let target = Box::new(self.parse_pattern()?);
let name = match name_node.kind {
UntypedKind::Identifier(sym) => sym,
_ => return Err("Expected identifier for def name".to_string()),
};
let value = Box::new(self.parse_expression()?); let value = Box::new(self.parse_expression()?);
Ok(Node { Ok(Node {
identity, identity,
kind: UntypedKind::Def { name, value }, kind: UntypedKind::Def { target, value },
ty: (), ty: (),
}) })
} }
@@ -264,44 +259,49 @@ impl<'a> Parser<'a> {
self.peek() self.peek()
)); ));
} }
let token = self.advance()?; self.parse_pattern()
let identity = Rc::new(NodeIdentity { }
location: token.location,
});
let mut elements = Vec::new(); fn parse_pattern(&mut self) -> Result<Node<UntypedKind>, String> {
while *self.peek() != TokenKind::RightBracket { let next = self.peek();
let next_peek = self.peek(); match next {
match next_peek { TokenKind::Identifier(_) => {
TokenKind::Identifier(name) => { let token = self.advance()?;
let name = name.clone(); let sym: Symbol = match token.kind {
let token = self.advance()?; TokenKind::Identifier(s) => s.into(),
let p_identity = Rc::new(NodeIdentity { _ => unreachable!(),
};
Ok(Node {
identity: Rc::new(NodeIdentity {
location: token.location, location: token.location,
}); }),
elements.push(Node { kind: UntypedKind::Parameter(sym),
identity: p_identity, ty: (),
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
));
}
} }
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( fn parse_call(
+25 -9
View File
@@ -281,6 +281,18 @@ impl VM {
globals[idx] = val.clone(); globals[idx] = val.clone();
Ok(val) 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::Get { addr, .. } => self.get_value(*addr),
BoundKind::Set { addr, value } => { BoundKind::Set { addr, value } => {
let val = self.eval_internal(obs, value)?; let val = self.eval_internal(obs, value)?;
@@ -622,9 +634,7 @@ impl VM {
fn flatten_value(val: Value, into: &mut Vec<Value>) { fn flatten_value(val: Value, into: &mut Vec<Value>) {
if let Some(values) = val.as_slice() { if let Some(values) = val.as_slice() {
for item in values.iter() { into.extend_from_slice(values);
Self::flatten_value(item.clone(), into);
}
} else { } else {
into.push(val); into.push(val);
} }
@@ -654,12 +664,7 @@ impl VM {
into: &mut Vec<Value>, into: &mut Vec<Value>,
) -> Result<(), String> { ) -> Result<(), String> {
for e in elements { for e in elements {
match &e.kind { into.push(self.eval_internal(obs, e)?);
BoundKind::Tuple { elements: sub } => {
self.eval_and_flatten_internal(obs, sub, into)?
}
_ => into.push(self.eval_internal(obs, e)?),
}
} }
Ok(()) Ok(())
} }
@@ -685,6 +690,17 @@ impl VM {
} }
Ok(()) 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 } => { BoundKind::Tuple { elements } => {
if let Some(sub_values) = values.get(*offset).and_then(|v| v.as_slice()) { if let Some(sub_values) = values.get(*offset).and_then(|v| v.as_slice()) {
*offset += 1; *offset += 1;
+24
View File
@@ -312,4 +312,28 @@ mod tests {
dump_record 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);
}
}
} }