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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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, ..
|
||||
} => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user