Introduce Value::Cell for mutable upvalues, allowing closures to

modify captured variables.
Implement `capture_upvalue` and `get_value`/`set_value` methods in the
`VM` to handle these mutable upvalues.

Refactor number parsing to support integers and floats

Separate the `TokenKind::Number` into `TokenKind::Integer` and
`TokenKind::Float`.
Update the lexer to distinguish between integer and float literals.
Update the parser to correctly map these new token kinds to their
respective `Value` types.

Add integration tests for parsing integers and floats, and for closure
modification of captured variables.
This commit is contained in:
Michael Schimmel
2026-02-17 12:35:39 +01:00
parent 12791a7f8f
commit ce166f39e3
6 changed files with 289 additions and 16 deletions
+16 -3
View File
@@ -11,7 +11,8 @@ pub enum TokenKind {
Quote, Backtick, Tilde, At,
Identifier(Arc<str>),
Keyword(Arc<str>),
Number(f64),
Integer(i64),
Float(f64),
String(Arc<str>),
EOF,
}
@@ -69,8 +70,20 @@ impl<'a> Lexer<'a> {
'"' => TokenKind::String(Arc::from(self.read_string()?)),
c if c.is_ascii_digit() || (c == '-' && self.input.peek().is_some_and(|p| p.is_ascii_digit())) => {
let mut num_str = String::from(c);
num_str.push_str(&self.read_while(|c| c.is_ascii_digit() || c == '.'));
TokenKind::Number(num_str.parse().map_err(|_| "Invalid number format")?)
while let Some(&next) = self.peek() {
if next.is_ascii_digit() || next == '.' {
num_str.push(self.input.next().unwrap());
self.col += 1;
} else {
break;
}
}
if num_str.contains('.') {
TokenKind::Float(num_str.parse().map_err(|_| "Invalid float format")?)
} else {
TokenKind::Integer(num_str.parse().map_err(|_| "Invalid integer format")?)
}
}
c if is_ident_start(c) => {
let mut id = String::from(c);
+2 -1
View File
@@ -51,7 +51,8 @@ impl<'a> Parser<'a> {
let identity = Arc::new(NodeIdentity { location: token.location });
let kind = match token.kind {
TokenKind::Number(n) => UntypedKind::Constant(Value::Float(n)),
TokenKind::Integer(n) => UntypedKind::Constant(Value::Int(n)),
TokenKind::Float(n) => UntypedKind::Constant(Value::Float(n)),
TokenKind::String(s) => UntypedKind::Constant(Value::Text(s)),
TokenKind::Keyword(k) => UntypedKind::Constant(Value::Keyword(Keyword::intern(&k))),
TokenKind::Identifier(id) => {
+4
View File
@@ -68,6 +68,7 @@ pub enum Value {
Record(Arc<HashMap<Keyword, Value>>),
Function(Arc<dyn Fn(Vec<Value>) -> Value + Send + Sync>),
Object(Arc<dyn Object>), // For compiled Closures and other opaque types
Cell(Arc<Mutex<Value>>), // Boxed value for captures
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -93,6 +94,7 @@ impl Value {
match self {
Value::Void => false,
Value::Bool(b) => *b,
Value::Cell(c) => c.lock().unwrap().is_truthy(),
_ => true,
}
}
@@ -109,6 +111,7 @@ impl Value {
Value::Record(_) => StaticType::Record(Arc::new(BTreeMap::new())), // Empty for now
Value::Function { .. } => StaticType::Any, // Dynamic function
Value::Object(o) => StaticType::Object(o.type_name()),
Value::Cell(c) => c.lock().unwrap().static_type(),
}
}
}
@@ -126,6 +129,7 @@ impl fmt::Display for Value {
Value::Record(r) => write!(f, "<record[{}]>", r.len()),
Value::Function(_) => write!(f, "<native fn>"),
Value::Object(o) => write!(f, "<{}>", o.type_name()),
Value::Cell(c) => write!(f, "{}", c.lock().unwrap()),
}
}
}
+174 -12
View File
@@ -7,7 +7,7 @@ use crate::ast::types::{Value, Object, StaticType};
#[derive(Debug, Clone)]
pub struct Closure {
pub function_node: Arc<Node<BoundKind, StaticType>>,
pub upvalues: Vec<Value>,
pub upvalues: Vec<Arc<Mutex<Value>>>,
}
impl Object for Closure {
@@ -102,7 +102,7 @@ impl VM {
BoundKind::Lambda { param_count: _, upvalues, body } => {
let mut captured = Vec::with_capacity(upvalues.len());
for addr in upvalues {
captured.push(self.get_value(*addr)?);
captured.push(self.capture_upvalue(*addr)?);
}
let closure = Closure {
@@ -151,13 +151,53 @@ impl VM {
}
}
fn capture_upvalue(&mut self, addr: Address) -> Result<Arc<Mutex<Value>>, String> {
match addr {
Address::Local(idx) => {
let frame = self.frames.last().ok_or("No call frame")?;
let abs_index = frame.stack_base + (idx as usize);
if abs_index < self.stack.len() {
// Check if already boxed
if let Value::Cell(cell) = &self.stack[abs_index] {
Ok(cell.clone())
} else {
// Box it
let val = self.stack[abs_index].clone();
let cell = Arc::new(Mutex::new(val));
self.stack[abs_index] = Value::Cell(cell.clone());
Ok(cell)
}
} else {
Err(format!("Stack underflow capture local {}", idx))
}
},
Address::Upvalue(idx) => {
let frame = self.frames.last().ok_or("No call frame")?;
if let Some(closure) = &frame.closure {
let idx = idx as usize;
if idx < closure.upvalues.len() {
Ok(closure.upvalues[idx].clone())
} else {
Err(format!("Upvalue access out of bounds capture {}", idx))
}
} else {
Err("Current frame has no closure".to_string())
}
},
Address::Global(_) => Err("Cannot capture global directly".to_string()),
}
}
fn get_value(&self, addr: Address) -> Result<Value, String> {
match addr {
Address::Local(idx) => {
let frame = self.frames.last().ok_or("No call frame")?;
let abs_index = frame.stack_base + (idx as usize);
if abs_index < self.stack.len() {
Ok(self.stack[abs_index].clone())
match &self.stack[abs_index] {
Value::Cell(cell) => Ok(cell.lock().unwrap().clone()),
val => Ok(val.clone()),
}
} else {
Err(format!("Stack underflow access local {}", idx))
}
@@ -176,7 +216,7 @@ impl VM {
if let Some(closure) = &frame.closure {
let idx = idx as usize;
if idx < closure.upvalues.len() {
Ok(closure.upvalues[idx].clone())
Ok(closure.upvalues[idx].lock().unwrap().clone())
} else {
Err(format!("Upvalue access out of bounds {}", idx))
}
@@ -192,14 +232,16 @@ impl VM {
Address::Local(idx) => {
let frame = self.frames.last().ok_or("No call frame")?;
let abs_index = frame.stack_base + (idx as usize);
if abs_index >= self.stack.len() {
if abs_index == self.stack.len() {
self.stack.push(value);
} else {
return Err(format!("Stack gap write local {}", idx));
}
if abs_index < self.stack.len() {
if let Value::Cell(cell) = &self.stack[abs_index] {
*cell.lock().unwrap() = value;
} else {
self.stack[abs_index] = value;
}
} else if abs_index == self.stack.len() {
self.stack.push(value);
} else {
self.stack[abs_index] = value;
return Err(format!("Stack gap write local {}", idx));
}
Ok(())
},
@@ -212,7 +254,127 @@ impl VM {
globals[idx] = value;
Ok(())
},
Address::Upvalue(_) => Err("Cannot assign to upvalue (Closure Upvalues are immutable)".to_string()),
Address::Upvalue(idx) => {
let frame = self.frames.last().ok_or("No call frame")?;
if let Some(closure) = &frame.closure {
let idx = idx as usize;
if idx < closure.upvalues.len() {
*closure.upvalues[idx].lock().unwrap() = value;
Ok(())
} else {
Err(format!("Upvalue assignment out of bounds {}", idx))
}
} else {
Err("Current frame has no closure".to_string())
}
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::types::{SourceLocation, NodeIdentity};
fn make_dummy_identity() -> Arc<NodeIdentity> {
Arc::new(NodeIdentity {
location: SourceLocation { line: 0, col: 0 },
})
}
#[test]
fn test_capture_boxing_modification() {
// Goal:
// var x = 10; (Local 0)
// var f = lambda { x = 20; }; (Local 1)
// f();
// return x; -> Should be 20
let id = make_dummy_identity();
// 1. Define Lambda Body: x = 20 (Upvalue 0)
let lambda_body = Node {
identity: id.clone(),
ty: StaticType::Void,
kind: BoundKind::Set {
addr: Address::Upvalue(0),
value: Box::new(Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Constant(Value::Int(20)),
}),
},
};
// 2. Define Main Script
let root = Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Block {
exprs: vec![
// x = 10 (Local 0) - simulate definition by assignment to stack gap
Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Set {
addr: Address::Local(0),
value: Box::new(Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Constant(Value::Int(10)),
}),
},
},
// f = lambda capturing x (Local 1)
Node {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Set {
addr: Address::Local(1),
value: Box::new(Node {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Lambda {
param_count: 0,
upvalues: vec![Address::Local(0)], // Capture x
body: Arc::new(lambda_body),
},
}),
},
},
// f()
Node {
identity: id.clone(),
ty: StaticType::Void,
kind: BoundKind::Call {
callee: Box::new(Node {
identity: id.clone(),
ty: StaticType::Any,
kind: BoundKind::Get(Address::Local(1)),
}),
args: vec![],
},
},
// return x (Local 0)
Node {
identity: id.clone(),
ty: StaticType::Int,
kind: BoundKind::Get(Address::Local(0)),
},
],
},
};
let globals = Arc::new(Mutex::new(Vec::new()));
let mut vm = VM::new(globals);
let result = vm.run(&root);
match result {
Ok(Value::Int(val)) => assert_eq!(val, 20, "Captured variable should be modified to 20"),
Ok(val) => panic!("Expected Int(20), got {:?}", val),
Err(e) => panic!("VM Error: {}", e),
}
}
}
+91
View File
@@ -0,0 +1,91 @@
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use crate::ast::parser::Parser;
use crate::ast::compiler::binder::Binder;
use crate::ast::vm::VM;
use crate::ast::types::Value;
use crate::ast::nodes::UntypedKind;
#[test]
fn test_parse_integer_constant() {
let source = "123";
let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Int(val)) = ast.kind {
assert_eq!(val, 123);
} else {
panic!("Expected Integer constant, got {:?}", ast.kind);
}
}
#[test]
fn test_parse_negative_integer() {
let source = "-42";
let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Int(val)) = ast.kind {
assert_eq!(val, -42);
} else {
panic!("Expected Integer constant, got {:?}", ast.kind);
}
}
#[test]
fn test_parse_float_constant() {
let source = "123.45";
let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Float(val)) = ast.kind {
assert_eq!(val, 123.45);
} else {
panic!("Expected Float constant, got {:?}", ast.kind);
}
}
#[test]
fn test_parse_negative_float() {
let source = "-10.5";
let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Float(val)) = ast.kind {
assert_eq!(val, -10.5);
} else {
panic!("Expected Float constant, got {:?}", ast.kind);
}
}
#[test]
fn test_closure_modification_from_source() {
let source = r#"
(do
(def x 10)
(def f (fn [] (assign x 20)))
(f)
x
)
"#;
let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse");
let globals = Arc::new(Mutex::new(std::collections::HashMap::new()));
let mut binder = Binder::new(globals.clone());
let bound_ast = binder.bind(&ast).expect("Failed to bind");
let vm_globals = Arc::new(Mutex::new(Vec::new()));
let mut vm = VM::new(vm_globals);
let result = vm.run(&bound_ast);
match result {
Ok(Value::Int(val)) => assert_eq!(val, 20, "Closure should modify outer variable"),
Ok(val) => panic!("Expected Int(20), got {:?}", val),
Err(e) => panic!("VM Error: {}", e),
}
}
}
+2
View File
@@ -2,6 +2,8 @@ use eframe::egui;
use crate::ast::environment::Environment;
pub mod ast;
#[cfg(test)]
mod integration_test;
fn main() -> eframe::Result {
let options = eframe::NativeOptions {