Skip to content

Commit fcbbe36

Browse files
Chore: Add integration tests and fix.
1 parent 7f585d2 commit fcbbe36

7 files changed

Lines changed: 292 additions & 17 deletions

File tree

compiler/src/modules/parser/expr.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,28 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
418418
}
419419
self.eat(TokenType::Colon);
420420

421+
// Capture outer scope versions so free variables (e.g. `n` from enclosing
422+
// function) resolve correctly inside the lambda body.
423+
let outer_versions = self.ssa_versions.clone();
424+
421425
let body = self.with_fresh_chunk(|s| {
426+
// Seed with outer scope first, then params override (params shadow outer names).
427+
s.ssa_versions = outer_versions;
422428
for p in &params { s.ssa_versions.insert(p.clone(), 0); }
423429
s.expr();
424430
s.chunk.emit(OpCode::ReturnValue, 0);
425431
});
432+
// Ensure free variables used inside the lambda appear in the outer chunk's
433+
// name table so exec_make_function can find their slot indices for capture.
434+
let param_slots: alloc::collections::BTreeSet<String> = params.iter()
435+
.map(|p| format!("{}_0", p.trim_start_matches('*')))
436+
.collect();
437+
for name in &body.names {
438+
if !param_slots.contains(name.as_str()) {
439+
self.chunk.push_name(name);
440+
}
441+
}
442+
426443
let fi = self.chunk.functions.len() as u16;
427444
self.chunk.functions.push((params, body, defaults, u16::MAX));
428445
self.chunk.emit(OpCode::MakeFunction, fi);

compiler/src/modules/vm/builtins.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ impl<'a> VM<'a> {
117117
pub fn call_float(&mut self) -> Result<(), VmErr> {
118118
let o = self.pop()?;
119119
let f = if o.is_float() { o.as_float() }
120+
else if o.is_bool() { o.as_bool() as i64 as f64 }
120121
else if o.is_int() { o.as_int() as f64 }
121122
else if o.is_heap() { match self.heap.get(o) {
122123
HeapObj::Str(s) => s.trim().parse().map_err(|_| VmErr::Value("float(): invalid literal"))?,
@@ -273,6 +274,16 @@ impl<'a> VM<'a> {
273274

274275
pub fn call_list(&mut self) -> Result<(), VmErr> {
275276
let o = self.pop()?;
277+
// Str needs char allocation — handle before extract_iterable_full.
278+
if o.is_heap() {
279+
if let HeapObj::Str(s) = self.heap.get(o) {
280+
let s = s.clone();
281+
let items = self.str_to_char_vals(&s)?;
282+
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
283+
self.push(val);
284+
return Ok(());
285+
}
286+
}
276287
let items = self.extract_iterable_full(o)?;
277288
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
278289
self.push(val); Ok(())
@@ -430,6 +441,15 @@ impl<'a> VM<'a> {
430441
else { while cur > end { v.push(Val::int(cur)); cur += step; } }
431442
v
432443
}
444+
HeapObj::Str(s) => {
445+
let s = s.clone();
446+
drop(s);
447+
let s = match self.heap.get(o) { HeapObj::Str(s) => s.clone(), _ => unreachable!() };
448+
s.chars().map(|c| {
449+
// Can't alloc here — caller must handle
450+
Val::int(c as i64)
451+
}).collect()
452+
}
433453
_ => return Err(VmErr::Type("list() argument must be iterable")),
434454
})
435455
}

compiler/src/modules/vm/handlers/attr.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ impl<'a> VM<'a> {
3636
("dict", "get") => BuiltinMethodId::DictGet,
3737
("dict", "update") => BuiltinMethodId::DictUpdate,
3838
("dict", "pop") => BuiltinMethodId::DictPop,
39+
("dict", "setdefault") => BuiltinMethodId::DictSetDefault,
40+
("str", "lstrip") => BuiltinMethodId::StrLstrip,
41+
("str", "rstrip") => BuiltinMethodId::StrRstrip,
42+
("str", "isdigit") => BuiltinMethodId::StrIsDigit,
43+
("str", "isalpha") => BuiltinMethodId::StrIsAlpha,
44+
("str", "isalnum") => BuiltinMethodId::StrIsAlnum,
45+
("str", "capitalize") => BuiltinMethodId::StrCapitalize,
46+
("str", "title") => BuiltinMethodId::StrTitle,
47+
("str", "center") => BuiltinMethodId::StrCenter,
48+
("str", "zfill") => BuiltinMethodId::StrZfill,
49+
("list", "extend") => BuiltinMethodId::ListExtend,
50+
("list", "clear") => BuiltinMethodId::ListClear,
51+
("list", "copy") => BuiltinMethodId::ListCopy,
3952
(ty, attr) => {
4053
return Err(attr_not_found(ty, attr));
4154
}

compiler/src/modules/vm/handlers/function.rs

Lines changed: 175 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ impl<'a> VM<'a> {
1111
match op {
1212
// User functions
1313
OpCode::Call => self.exec_call(operand, chunk, slots),
14-
OpCode::MakeFunction | OpCode::MakeCoroutine => self.exec_make_function(operand, chunk),
14+
OpCode::MakeFunction | OpCode::MakeCoroutine => self.exec_make_function(operand, chunk, slots),
1515

1616
// Pure built-ins
1717
OpCode::CallLen => self.call_len(),
@@ -46,17 +46,35 @@ impl<'a> VM<'a> {
4646
}
4747
}
4848

49-
fn exec_make_function(&mut self, operand: u16, chunk: &SSAChunk) -> Result<(), VmErr> {
50-
// `operand` is the local index in `chunk.functions`. Translate to the
51-
// global id assigned during VM init so the resulting Func resolves
52-
// correctly even after escaping its defining scope.
49+
fn exec_make_function(&mut self, operand: u16, chunk: &SSAChunk, slots: &[Option<Val>]) -> Result<(), VmErr> {
5350
let global = self.fn_index
5451
.get(&(chunk as *const _))
5552
.and_then(|v| v.get(operand as usize).copied())
56-
.ok_or(VmErr::Runtime("MakeFunction: unknown function index"))?;
57-
let n_defaults = self.functions[global as usize].2 as usize;
53+
.ok_or(VmErr::Runtime("MakeFunction: unknown function index"))? as usize;
54+
55+
let n_defaults = self.functions[global].2 as usize;
5856
let defaults = if n_defaults > 0 { self.pop_n(n_defaults)? } else { vec![] };
59-
let val = self.heap.alloc(HeapObj::Func(global as usize, defaults))?;
57+
58+
// Build chunk name -> slot index map for capture lookup.
59+
let chunk_map: HashMap<&str, usize> = chunk.names.iter()
60+
.enumerate().map(|(i, n)| (n.as_str(), i)).collect();
61+
62+
// Capture free variables from the enclosing frame at function-creation time.
63+
let (params, body, _, _) = self.functions[global];
64+
let param_names: alloc::collections::BTreeSet<String> = params.iter()
65+
.map(|p| format!("{}_0", p.trim_start_matches('*')))
66+
.collect();
67+
let mut captures: Vec<(usize, Val)> = Vec::new();
68+
for (bi, bname) in body.names.iter().enumerate() {
69+
if param_names.contains(bname.as_str()) { continue; }
70+
if let Some(&si) = chunk_map.get(bname.as_str()) {
71+
if let Some(Some(v)) = slots.get(si) {
72+
captures.push((bi, *v));
73+
}
74+
}
75+
}
76+
77+
let val = self.heap.alloc(HeapObj::Func(global, defaults, captures))?;
6078
self.push(val);
6179
Ok(())
6280
}
@@ -89,8 +107,8 @@ impl<'a> VM<'a> {
89107
return self.exec_bound_method(recv, id, positional, kw_flat);
90108
}
91109

92-
let (fi, captured_defaults) = match self.heap.get(callee) {
93-
HeapObj::Func(i, d) => (*i, d.clone()),
110+
let (fi, captured_defaults, captured_env) = match self.heap.get(callee) {
111+
HeapObj::Func(i, d, c) => (*i, d.clone(), c.clone()),
94112
_ => return Err(VmErr::Type("object is not callable")),
95113
};
96114

@@ -168,6 +186,26 @@ impl<'a> VM<'a> {
168186
}
169187
}
170188

189+
// Apply captured environment from function-creation time (closures).
190+
// Skip nonlocal variables — they must always come from the live enclosing
191+
// scope (handled below) so that mutations between calls are visible.
192+
let nonlocal_body_slots: alloc::collections::BTreeSet<usize> = body.nonlocals.iter()
193+
.flat_map(|base| {
194+
body.names.iter().enumerate().filter_map(|(i, n)| {
195+
n.rfind('_').filter(|&p| n[p+1..].parse::<u32>().is_ok())
196+
.filter(|&p| &n[..p] == base.as_str())
197+
.map(|_| i)
198+
})
199+
})
200+
.collect();
201+
202+
for (bi, val) in &captured_env {
203+
if nonlocal_body_slots.contains(bi) { continue; }
204+
if *bi < fn_slots.len() && fn_slots[*bi].is_none() {
205+
fn_slots[*bi] = Some(*val);
206+
}
207+
}
208+
171209
// Captura del enclosing scope: nombres del frame que llama (`chunk`),
172210
// valores de sus slots. `.get()` defensivo si las longitudes divergen.
173211
for (si, sv) in slots.iter().enumerate() {
@@ -555,7 +593,7 @@ impl<'a> VM<'a> {
555593
self.push(Val::int(idx));
556594
Ok(())
557595
}
558-
ListCount => {
596+
ListCount => {
559597
if positional.len() != 1 {
560598
return Err(VmErr::Type("count() takes exactly one argument"));
561599
}
@@ -625,6 +663,132 @@ ListCount => {
625663
self.push(result);
626664
Ok(())
627665
}
666+
DictSetDefault => {
667+
if positional.is_empty() {
668+
return Err(VmErr::Type("setdefault() requires at least one argument"));
669+
}
670+
let key = positional[0];
671+
let default = if positional.len() > 1 { positional[1] } else { Val::none() };
672+
let result = match self.heap.get_mut(recv) {
673+
HeapObj::Dict(rc) => {
674+
let already = rc.borrow().get(&key).copied();
675+
if let Some(v) = already { v } else {
676+
rc.borrow_mut().insert(key, default);
677+
default
678+
}
679+
}
680+
_ => return Err(VmErr::Type("setdefault: receiver is not a dict")),
681+
};
682+
self.mark_impure();
683+
self.push(result);
684+
Ok(())
685+
}
686+
StrLstrip => {
687+
let s = self.recv_str(recv)?;
688+
let result = if positional.is_empty() { s.trim_start().to_string() }
689+
else { let p = self.val_to_str(positional[0])?; s.trim_start_matches(|c| p.contains(c)).to_string() };
690+
let val = self.heap.alloc(HeapObj::Str(result))?;
691+
self.push(val); Ok(())
692+
}
693+
StrRstrip => {
694+
let s = self.recv_str(recv)?;
695+
let result = if positional.is_empty() { s.trim_end().to_string() }
696+
else { let p = self.val_to_str(positional[0])?; s.trim_end_matches(|c| p.contains(c)).to_string() };
697+
let val = self.heap.alloc(HeapObj::Str(result))?;
698+
self.push(val); Ok(())
699+
}
700+
StrIsDigit => {
701+
let s = self.recv_str(recv)?;
702+
self.push(Val::bool(!s.is_empty() && s.chars().all(|c| c.is_ascii_digit())));
703+
Ok(())
704+
}
705+
StrIsAlpha => {
706+
let s = self.recv_str(recv)?;
707+
self.push(Val::bool(!s.is_empty() && s.chars().all(|c| c.is_alphabetic())));
708+
Ok(())
709+
}
710+
StrIsAlnum => {
711+
let s = self.recv_str(recv)?;
712+
self.push(Val::bool(!s.is_empty() && s.chars().all(|c| c.is_alphanumeric())));
713+
Ok(())
714+
}
715+
StrCapitalize => {
716+
let s = self.recv_str(recv)?;
717+
let result = if s.is_empty() { s } else {
718+
let mut cs = s.chars();
719+
cs.next().unwrap().to_uppercase().to_string() + &cs.as_str().to_lowercase()
720+
};
721+
let val = self.heap.alloc(HeapObj::Str(result))?;
722+
self.push(val); Ok(())
723+
}
724+
StrTitle => {
725+
let s = self.recv_str(recv)?;
726+
let result = s.split_whitespace()
727+
.map(|w| { let mut cs = w.chars(); cs.next().map(|c| c.to_uppercase().to_string() + cs.as_str()).unwrap_or_default() })
728+
.collect::<Vec<_>>().join(" ");
729+
let val = self.heap.alloc(HeapObj::Str(result))?;
730+
self.push(val); Ok(())
731+
}
732+
StrCenter => {
733+
if positional.is_empty() { return Err(VmErr::Type("center() requires at least one argument")); }
734+
let s = self.recv_str(recv)?;
735+
if !positional[0].is_int() { return Err(VmErr::Type("center() width must be an integer")); }
736+
let width = positional[0].as_int() as usize;
737+
let fill = if positional.len() > 1 { self.val_to_str(positional[1])?.chars().next().unwrap_or(' ') } else { ' ' };
738+
let pad = width.saturating_sub(s.len());
739+
let left = pad / 2;
740+
let right = pad - left;
741+
let result = fill.to_string().repeat(left) + &s + &fill.to_string().repeat(right);
742+
let val = self.heap.alloc(HeapObj::Str(result))?;
743+
self.push(val); Ok(())
744+
}
745+
StrZfill => {
746+
if positional.is_empty() || !positional[0].is_int() {
747+
return Err(VmErr::Type("zfill() requires an integer argument"));
748+
}
749+
let s = self.recv_str(recv)?;
750+
let width = positional[0].as_int() as usize;
751+
let result = if s.len() >= width { s } else {
752+
let pad = "0".repeat(width - s.len());
753+
if s.starts_with('+') || s.starts_with('-') {
754+
s[..1].to_string() + &pad + &s[1..]
755+
} else { pad + &s }
756+
};
757+
let val = self.heap.alloc(HeapObj::Str(result))?;
758+
self.push(val); Ok(())
759+
}
760+
ListExtend => {
761+
if positional.len() != 1 { return Err(VmErr::Type("extend() takes exactly one argument")); }
762+
let items: Vec<Val> = if positional[0].is_heap() {
763+
match self.heap.get(positional[0]) {
764+
HeapObj::List(rc) => rc.borrow().clone(),
765+
HeapObj::Tuple(v) => v.clone(),
766+
_ => return Err(VmErr::Type("extend() argument must be iterable")),
767+
}
768+
} else { return Err(VmErr::Type("extend() argument must be iterable")); };
769+
match self.heap.get_mut(recv) {
770+
HeapObj::List(rc) => rc.borrow_mut().extend_from_slice(&items),
771+
_ => return Err(VmErr::Type("extend: receiver is not a list")),
772+
}
773+
self.mark_impure();
774+
self.push(Val::none()); Ok(())
775+
}
776+
ListClear => {
777+
match self.heap.get_mut(recv) {
778+
HeapObj::List(rc) => rc.borrow_mut().clear(),
779+
_ => return Err(VmErr::Type("clear: receiver is not a list")),
780+
}
781+
self.mark_impure();
782+
self.push(Val::none()); Ok(())
783+
}
784+
ListCopy => {
785+
let items = match self.heap.get(recv) {
786+
HeapObj::List(rc) => rc.borrow().clone(),
787+
_ => return Err(VmErr::Type("copy: receiver is not a list")),
788+
};
789+
let val = self.heap.alloc(HeapObj::List(Rc::new(RefCell::new(items))))?;
790+
self.push(val); Ok(())
791+
}
628792
}
629793
}
630794

compiler/src/modules/vm/ops.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl<'a> VM<'a> {
5252
HeapObj::Set(s) => !s.borrow().is_empty(),
5353
HeapObj::Range(s,e,st) => if *st > 0 { s < e } else { s > e },
5454
HeapObj::Type(_) => true,
55-
HeapObj::Func(_, _) => true,
55+
HeapObj::Func(_, _, _) => true,
5656
HeapObj::Slice(..) => true,
5757
HeapObj::BoundMethod(..) => true,
5858
}
@@ -83,7 +83,7 @@ impl<'a> VM<'a> {
8383
HeapObj::Dict(_) => "dict",
8484
HeapObj::Set(_) => "set",
8585
HeapObj::Tuple(_) => "tuple",
86-
HeapObj::Func(_, _) => "function",
86+
HeapObj::Func(_, _, _) => "function",
8787
HeapObj::Type(_) => "type",
8888
HeapObj::Range(..) => "range",
8989
HeapObj::Slice(..) => "slice",
@@ -120,7 +120,7 @@ impl<'a> VM<'a> {
120120
HeapObj::Str(s) => s.clone(),
121121
HeapObj::BigInt(b) => b.to_decimal(),
122122
HeapObj::Type(name) => format!("<class '{}'>", name),
123-
HeapObj::Func(i, _) => format!("<function {}>", i),
123+
HeapObj::Func(i, _, _) => format!("<function {}>", i),
124124
HeapObj::BoundMethod(_, id) => match id {
125125
BuiltinMethodId::ListAppend => "<built-in method append>".into(),
126126
BuiltinMethodId::DictKeys => "<built-in method keys>".into(),
@@ -146,6 +146,19 @@ impl<'a> VM<'a> {
146146
BuiltinMethodId::DictGet => "<built-in method get>".into(),
147147
BuiltinMethodId::DictUpdate => "<built-in method update>".into(),
148148
BuiltinMethodId::DictPop => "<built-in method pop>".into(),
149+
BuiltinMethodId::DictSetDefault => "<built-in method setdefault>".into(),
150+
BuiltinMethodId::StrLstrip => "<built-in method lstrip>".into(),
151+
BuiltinMethodId::StrRstrip => "<built-in method rstrip>".into(),
152+
BuiltinMethodId::StrIsDigit => "<built-in method isdigit>".into(),
153+
BuiltinMethodId::StrIsAlpha => "<built-in method isalpha>".into(),
154+
BuiltinMethodId::StrIsAlnum => "<built-in method isalnum>".into(),
155+
BuiltinMethodId::StrCapitalize => "<built-in method capitalize>".into(),
156+
BuiltinMethodId::StrTitle => "<built-in method title>".into(),
157+
BuiltinMethodId::StrCenter => "<built-in method center>".into(),
158+
BuiltinMethodId::StrZfill => "<built-in method zfill>".into(),
159+
BuiltinMethodId::ListExtend => "<built-in method extend>".into(),
160+
BuiltinMethodId::ListClear => "<built-in method clear>".into(),
161+
BuiltinMethodId::ListCopy => "<built-in method copy>".into(),
149162
},
150163
HeapObj::Slice(s, e, st) => format!("slice({}, {}, {})",
151164
self.display(*s), self.display(*e), self.display(*st)),

0 commit comments

Comments
 (0)