From a3ccb58445aa94a7b81a89301822b5dbcb3fc560 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sun, 22 Mar 2026 16:16:05 +0100 Subject: [PATCH] Implement JS iterators, generators, and destructuring - Add Yield and Spread bytecode opcodes, is_generator flag on Function - Implement generator functions: function*, yield, yield*, next/return/throw - Generator objects with suspend/resume via saved registers and IP - Compile for...of loops using the iterator protocol (@@iterator, .next()) - Add @@iterator to built-in types: Array, String, Map, Set - Array.prototype.keys/values/entries returning proper iterators - String.prototype[@@iterator] iterating over characters - Fix array/object destructuring register allocation (LIFO compliance) - Add rest element support for array destructuring ([a, ...rest]) - Add rest property support for object destructuring ({a, ...rest}) - Implement spread operator in array literals ([...arr]) - Spread opcode iterates via @@iterator protocol - Custom iterables work with for...of - Generator-based iterables work with for...of - Remove 'generators' from test262 unsupported features list - 25 new tests covering all features Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/js/src/builtins.rs | 269 ++++++++++ crates/js/src/bytecode.rs | 40 ++ crates/js/src/compiler.rs | 500 ++++++++++++++++--- crates/js/src/vm.rs | 992 ++++++++++++++++++++++++++++++++++++- crates/js/tests/test262.rs | 1 - 5 files changed, 1731 insertions(+), 71 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 6023146..9ffeb1c 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -862,6 +862,18 @@ fn init_array_prototype(gc: &mut Gc, proto: GcRef) { let at = make_native(gc, "at", array_at); set_builtin_prop(gc, proto, "at", Value::Function(at)); + + // @@iterator: returns an array iterator (values). + let iter = make_native(gc, "[Symbol.iterator]", array_iterator); + set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter)); + + // Array.prototype.keys/values/entries + let keys_fn = make_native(gc, "keys", array_keys_iter); + set_builtin_prop(gc, proto, "keys", Value::Function(keys_fn)); + let values_fn = make_native(gc, "values", array_values_iter); + set_builtin_prop(gc, proto, "values", Value::Function(values_fn)); + let entries_fn = make_native(gc, "entries", array_entries_iter); + set_builtin_prop(gc, proto, "entries", Value::Function(entries_fn)); } fn array_push(args: &[Value], ctx: &mut NativeContext) -> Result { @@ -1483,6 +1495,209 @@ fn error_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result, + items: GcRef, + next_fn: fn(&[Value], &mut NativeContext) -> Result, +) -> Value { + let mut obj = ObjectData::new(); + obj.properties.insert( + "__items__".to_string(), + Property::builtin(Value::Object(items)), + ); + obj.properties + .insert("__idx__".to_string(), Property::builtin(Value::Number(0.0))); + let next = gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "next".to_string(), + kind: FunctionKind::Native(NativeFunc { callback: next_fn }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + obj.properties + .insert("next".to_string(), Property::builtin(Value::Function(next))); + + // @@iterator returns self. + let self_iter = gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "[Symbol.iterator]".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: iter_self, + }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + obj.properties.insert( + "@@iterator".to_string(), + Property::builtin(Value::Function(self_iter)), + ); + + let r = gc.alloc(HeapObject::Object(obj)); + Value::Object(r) +} + +fn iter_self(_args: &[Value], ctx: &mut NativeContext) -> Result { + Ok(ctx.this.clone()) +} + +fn make_iterator_result_native(gc: &mut Gc, value: Value, done: bool) -> Value { + let mut obj = ObjectData::new(); + obj.properties + .insert("value".to_string(), Property::data(value)); + obj.properties + .insert("done".to_string(), Property::data(Value::Boolean(done))); + let r = gc.alloc(HeapObject::Object(obj)); + Value::Object(r) +} + +/// Array.prototype[@@iterator]() — same as values(). +fn array_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { + array_values_iter(_args, ctx) +} + +/// Array.prototype.values() — returns iterator over values. +fn array_values_iter(_args: &[Value], ctx: &mut NativeContext) -> Result { + let obj_ref = ctx + .this + .gc_ref() + .ok_or_else(|| RuntimeError::type_error("values called on non-object"))?; + Ok(make_simple_iterator(ctx.gc, obj_ref, array_values_next)) +} + +fn array_values_next(_args: &[Value], ctx: &mut NativeContext) -> Result { + let iter_ref = ctx + .this + .gc_ref() + .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?; + + let (items_ref, idx) = get_iter_state(ctx.gc, iter_ref); + let items_ref = match items_ref { + Some(r) => r, + None => return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)), + }; + + let len = array_length(ctx.gc, items_ref); + if idx >= len { + return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)); + } + + let val = array_get(ctx.gc, items_ref, idx); + set_iter_idx(ctx.gc, iter_ref, idx + 1); + Ok(make_iterator_result_native(ctx.gc, val, false)) +} + +/// Array.prototype.keys() — returns iterator over indices. +fn array_keys_iter(_args: &[Value], ctx: &mut NativeContext) -> Result { + let obj_ref = ctx + .this + .gc_ref() + .ok_or_else(|| RuntimeError::type_error("keys called on non-object"))?; + Ok(make_simple_iterator(ctx.gc, obj_ref, array_keys_next)) +} + +fn array_keys_next(_args: &[Value], ctx: &mut NativeContext) -> Result { + let iter_ref = ctx + .this + .gc_ref() + .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?; + let (items_ref, idx) = get_iter_state(ctx.gc, iter_ref); + let items_ref = match items_ref { + Some(r) => r, + None => return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)), + }; + let len = array_length(ctx.gc, items_ref); + if idx >= len { + return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)); + } + set_iter_idx(ctx.gc, iter_ref, idx + 1); + Ok(make_iterator_result_native( + ctx.gc, + Value::Number(idx as f64), + false, + )) +} + +/// Array.prototype.entries() — returns iterator over [index, value] pairs. +fn array_entries_iter(_args: &[Value], ctx: &mut NativeContext) -> Result { + let obj_ref = ctx + .this + .gc_ref() + .ok_or_else(|| RuntimeError::type_error("entries called on non-object"))?; + Ok(make_simple_iterator(ctx.gc, obj_ref, array_entries_next)) +} + +fn array_entries_next(_args: &[Value], ctx: &mut NativeContext) -> Result { + let iter_ref = ctx + .this + .gc_ref() + .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?; + let (items_ref, idx) = get_iter_state(ctx.gc, iter_ref); + let items_ref = match items_ref { + Some(r) => r, + None => return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)), + }; + let len = array_length(ctx.gc, items_ref); + if idx >= len { + return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)); + } + let val = array_get(ctx.gc, items_ref, idx); + set_iter_idx(ctx.gc, iter_ref, idx + 1); + + // Create [index, value] pair array. + let mut pair = ObjectData::new(); + pair.properties + .insert("0".to_string(), Property::data(Value::Number(idx as f64))); + pair.properties.insert("1".to_string(), Property::data(val)); + pair.properties.insert( + "length".to_string(), + Property { + value: Value::Number(2.0), + writable: true, + enumerable: false, + configurable: false, + }, + ); + let pair_ref = ctx.gc.alloc(HeapObject::Object(pair)); + Ok(make_iterator_result_native( + ctx.gc, + Value::Object(pair_ref), + false, + )) +} + +/// Helper to read __items__ and __idx__ from an iterator state object. +fn get_iter_state(gc: &Gc, iter_ref: GcRef) -> (Option, usize) { + match gc.get(iter_ref) { + Some(HeapObject::Object(data)) => { + let items = data + .properties + .get("__items__") + .and_then(|p| p.value.gc_ref()); + let idx = data + .properties + .get("__idx__") + .map(|p| p.value.to_number() as usize) + .unwrap_or(0); + (items, idx) + } + _ => (None, 0), + } +} + +/// Helper to update __idx__ on an iterator state object. +fn set_iter_idx(gc: &mut Gc, iter_ref: GcRef, idx: usize) { + if let Some(HeapObject::Object(data)) = gc.get_mut(iter_ref) { + data.properties.insert( + "__idx__".to_string(), + Property::builtin(Value::Number(idx as f64)), + ); + } +} + // ── String built-in ────────────────────────────────────────── fn init_string_prototype(gc: &mut Gc, proto: GcRef) { @@ -1521,6 +1736,34 @@ fn init_string_prototype(gc: &mut Gc, proto: GcRef) { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } + + // @@iterator: iterates over characters. + let iter_fn = make_native(gc, "[Symbol.iterator]", string_iterator); + set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter_fn)); +} + +/// String.prototype[@@iterator]() — returns an iterator over characters. +fn string_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { + let s = ctx.this.to_js_string(ctx.gc); + + // Store the string's characters in an array-like object. + let mut items = ObjectData::new(); + for (i, ch) in s.chars().enumerate() { + items + .properties + .insert(i.to_string(), Property::data(Value::String(ch.to_string()))); + } + items.properties.insert( + "length".to_string(), + Property { + value: Value::Number(s.chars().count() as f64), + writable: true, + enumerable: false, + configurable: false, + }, + ); + let items_ref = ctx.gc.alloc(HeapObject::Object(items)); + Ok(make_simple_iterator(ctx.gc, items_ref, array_values_next)) } fn init_string_constructor(gc: &mut Gc, str_proto: GcRef) -> GcRef { @@ -4143,6 +4386,9 @@ fn init_map_prototype(gc: &mut Gc, proto: GcRef) { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } + // Map.prototype[@@iterator] — returns an iterator of [key, value] pairs. + let iter_fn = make_native(gc, "[Symbol.iterator]", map_symbol_iterator); + set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter_fn)); } fn map_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result { @@ -4303,6 +4549,26 @@ fn map_proto_iter( Ok(make_value_array(ctx.gc, &items)) } +/// Map[@@iterator]() — wraps entries array into an iterator. +fn map_symbol_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { + let arr_val = map_proto_iter(_args, ctx, IterKind::Entries)?; + let arr_ref = match arr_val.gc_ref() { + Some(r) => r, + None => return Ok(Value::Undefined), + }; + Ok(make_simple_iterator(ctx.gc, arr_ref, array_values_next)) +} + +/// Set[@@iterator]() — wraps values array into an iterator. +fn set_symbol_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { + let arr_val = set_proto_values(_args, ctx)?; + let arr_ref = match arr_val.gc_ref() { + Some(r) => r, + None => return Ok(Value::Undefined), + }; + Ok(make_simple_iterator(ctx.gc, arr_ref, array_values_next)) +} + /// Normalize -0 to +0 for Map/Set key equality. fn normalize_zero(val: Value) -> Value { if let Value::Number(n) = &val { @@ -4409,6 +4675,9 @@ fn init_set_prototype(gc: &mut Gc, proto: GcRef) { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } + // Set.prototype[@@iterator] — returns an iterator of values. + let iter_fn = make_native(gc, "[Symbol.iterator]", set_symbol_iterator); + set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter_fn)); } fn set_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result { diff --git a/crates/js/src/bytecode.rs b/crates/js/src/bytecode.rs index c491861..7e9f10a 100644 --- a/crates/js/src/bytecode.rs +++ b/crates/js/src/bytecode.rs @@ -162,6 +162,13 @@ pub enum Op { LoadUpvalue = 0x7B, /// StoreUpvalue idx(u8), src — store into the closure's captured upvalue cell StoreUpvalue = 0x7C, + + // ── Iterator / generator ─────────────────────────────── + /// Yield dst, src — suspend generator, yield src value. On resume, dst gets the + /// value passed to next(). The VM saves the frame state and returns {value, done: false}. + Yield = 0x80, + /// Spread dst_array, src — iterate src via @@iterator, append all elements to dst_array + Spread = 0x81, } impl Op { @@ -230,6 +237,8 @@ impl Op { 0x7A => Some(Op::CellStore), 0x7B => Some(Op::LoadUpvalue), 0x7C => Some(Op::StoreUpvalue), + 0x80 => Some(Op::Yield), + 0x81 => Some(Op::Spread), _ => None, } } @@ -274,6 +283,8 @@ pub struct Function { pub source_map: Vec<(u32, u32)>, /// Upvalue definitions: how this function captures variables from its parent. pub upvalue_defs: Vec, + /// Whether this is a generator function (function*). + pub is_generator: bool, } impl Function { @@ -288,6 +299,7 @@ impl Function { functions: Vec::new(), source_map: Vec::new(), upvalue_defs: Vec::new(), + is_generator: false, } } } @@ -527,6 +539,20 @@ impl BytecodeBuilder { self.emit_u8(src); } + /// Emit: Yield dst, src — suspend generator + pub fn emit_yield(&mut self, dst: Reg, src: Reg) { + self.emit_u8(Op::Yield as u8); + self.emit_u8(dst); + self.emit_u8(src); + } + + /// Emit: Spread dst_array, src — spread iterable into array + pub fn emit_spread(&mut self, dst_array: Reg, src: Reg) { + self.emit_u8(Op::Spread as u8); + self.emit_u8(dst_array); + self.emit_u8(src); + } + /// Add a source map entry: current bytecode offset → source line. pub fn add_source_map(&mut self, line: u32) { let offset = self.offset() as u32; @@ -879,6 +905,18 @@ impl Function { pc += 2; format!("StoreUpvalue uv{idx}, r{src}") } + Op::Yield => { + let dst = code[pc]; + let src = code[pc + 1]; + pc += 2; + format!("Yield r{dst}, r{src}") + } + Op::Spread => { + let dst = code[pc]; + let src = code[pc + 1]; + pc += 2; + format!("Spread r{dst}, r{src}") + } }; out.push_str(&format!(" {offset:04X} {line}\n")); } @@ -972,6 +1010,8 @@ mod tests { Op::CellStore, Op::LoadUpvalue, Op::StoreUpvalue, + Op::Yield, + Op::Spread, ]; for op in ops { assert_eq!( diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index d6c0c98..641e82e 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -896,11 +896,129 @@ fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Resu body, is_await: _, } => { - let _ = left; - let tmp = fc.alloc_reg(); - compile_expr(fc, right, tmp)?; - fc.free_reg(tmp); + let saved_locals = fc.locals.len(); + let saved_next = fc.next_reg; + + // Evaluate the iterable. + let iterable_r = fc.alloc_reg(); + compile_expr(fc, right, iterable_r)?; + + // Get the iterator: call iterable[@@iterator](). + let iter_method_r = fc.alloc_reg(); + let sym_iter_ni = fc.builder.add_name("@@iterator"); + fc.builder + .emit_get_prop_name(iter_method_r, iterable_r, sym_iter_ni); + + // Set `this` = iterable for the call. + let this_ni = fc.builder.add_name("this"); + fc.builder.emit_store_global(this_ni, iterable_r); + + // Call [@@iterator]() with 0 args. + let iterator_r = fc.alloc_reg(); + let args_start = fc.next_reg; + fc.builder + .emit_call(iterator_r, iter_method_r, args_start, 0); + + // Temp registers for next method, result, done, value. + let next_method_r = fc.alloc_reg(); + let next_ni = fc.builder.add_name("next"); + fc.builder + .emit_get_prop_name(next_method_r, iterator_r, next_ni); + + let result_obj_r = fc.alloc_reg(); + let done_r = fc.alloc_reg(); + let value_r = fc.alloc_reg(); + + // Loop start. + let loop_start = fc.builder.offset(); + + // Set `this` = iterator for the .next() call. + fc.builder.emit_store_global(this_ni, iterator_r); + + // Call iterator.next(). + fc.builder + .emit_call(result_obj_r, next_method_r, args_start, 0); + + // Extract done and value. + let done_ni = fc.builder.add_name("done"); + let value_ni = fc.builder.add_name("value"); + fc.builder.emit_get_prop_name(done_r, result_obj_r, done_ni); + + // Exit if done. + let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r); + + // Extract value. + fc.builder + .emit_get_prop_name(value_r, result_obj_r, value_ni); + + // Bind the loop variable. + match left { + ForInOfLeft::VarDecl { kind, pattern } => match &pattern.kind { + PatternKind::Identifier(name) => { + let is_captured = fc.captured_names.contains(name.as_str()); + let is_const = *kind == VarKind::Const; + let var_r = fc.define_local_ext(name, is_captured, is_const); + if is_captured { + fc.builder.emit_reg(Op::NewCell, var_r); + fc.builder.emit_reg_reg(Op::CellStore, var_r, value_r); + } else { + fc.builder.emit_reg_reg(Op::Move, var_r, value_r); + } + } + _ => { + // Destructuring pattern in for...of. + compile_destructuring_pattern(fc, pattern, value_r)?; + } + }, + ForInOfLeft::Pattern(pattern) => match &pattern.kind { + PatternKind::Identifier(name) => { + if let Some(local) = fc.find_local_info(name) { + let reg = local.reg; + let captured = local.is_captured; + if captured { + fc.builder.emit_reg_reg(Op::CellStore, reg, value_r); + } else { + fc.builder.emit_reg_reg(Op::Move, reg, value_r); + } + } else if let Some(uv_idx) = fc.find_upvalue(name) { + fc.builder.emit_store_upvalue(uv_idx, value_r); + } else { + let ni = fc.builder.add_name(name); + fc.builder.emit_store_global(ni, value_r); + } + } + _ => { + compile_destructuring_pattern(fc, pattern, value_r)?; + } + }, + } + + // Push loop context for break/continue. + fc.loop_stack.push(LoopCtx { + label: None, + break_patches: Vec::new(), + continue_patches: Vec::new(), + }); + + // Compile body. compile_stmt(fc, body, result_reg)?; + + // Jump back to loop start. + fc.builder.emit_jump_to(loop_start); + + // Patch exit. + fc.builder.patch_jump(exit_patch); + let ctx = fc.loop_stack.pop().unwrap(); + for patch in ctx.break_patches { + fc.builder.patch_jump(patch); + } + for patch in ctx.continue_patches { + fc.builder.patch_jump_to(patch, loop_start); + } + + // Restore locals/regs. + fc.locals.truncate(saved_locals); + fc.next_reg = saved_next; } StmtKind::Return(expr) => { @@ -1110,6 +1228,9 @@ fn compile_var_declarator( } _ => { // Destructuring: evaluate init, then bind patterns. + // Note: don't free tmp — destructuring pattern allocates permanent + // local registers above it. The tmp register slot is reused via + // next_reg restoration by the parent scope. let tmp = fc.alloc_reg(); if let Some(init) = &decl.init { compile_expr(fc, init, tmp)?; @@ -1117,7 +1238,6 @@ fn compile_var_declarator( fc.builder.emit_reg(Op::LoadUndefined, tmp); } compile_destructuring_pattern(fc, &decl.pattern, tmp)?; - fc.free_reg(tmp); } } Ok(()) @@ -1139,43 +1259,189 @@ fn compile_destructuring_pattern( fc.builder.emit_reg_reg(Op::Move, reg, src); } } - PatternKind::Object { - properties, - rest: _, - } => { + PatternKind::Object { properties, rest } => { + // For each property, extract the value and bind it. + // We use a single temp register that we reuse for each property + // by resetting next_reg after each binding. for prop in properties { let key_name = match &prop.key { PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(), - _ => { - return Err(JsError::SyntaxError( - "computed destructuring keys not yet supported".into(), - )); + PropertyKey::Computed(expr) => { + let saved = fc.next_reg; + let key_reg = fc.alloc_reg(); + compile_expr(fc, expr, key_reg)?; + let val_reg = fc.alloc_reg(); + fc.builder.emit_reg3(Op::GetProperty, val_reg, src, key_reg); + compile_destructuring_pattern(fc, &prop.value, val_reg)?; + // Temp regs are buried; just let them be. + let _ = saved; + continue; + } + PropertyKey::Number(n) => { + if n.fract() == 0.0 && n.abs() < 1e15 { + format!("{}", *n as i64) + } else { + format!("{n}") + } } }; - let val_reg = fc.alloc_reg(); - let name_idx = fc.builder.add_name(&key_name); - fc.builder.emit_get_prop_name(val_reg, src, name_idx); - compile_destructuring_pattern(fc, &prop.value, val_reg)?; - fc.free_reg(val_reg); + // For simple identifier patterns, load property directly into + // the target register to avoid LIFO register allocation issues. + if let PatternKind::Identifier(name) = &prop.value.kind { + let is_captured = fc.captured_names.contains(name.as_str()); + let reg = fc.define_local_ext(name, is_captured, false); + let name_idx = fc.builder.add_name(&key_name); + if is_captured { + let tmp = fc.alloc_reg(); + fc.builder.emit_get_prop_name(tmp, src, name_idx); + fc.builder.emit_reg(Op::NewCell, reg); + fc.builder.emit_reg_reg(Op::CellStore, reg, tmp); + fc.free_reg(tmp); + } else { + fc.builder.emit_get_prop_name(reg, src, name_idx); + } + } else { + // Complex inner pattern (nested, default, etc.) + // Allocate temp, extract value, then recurse. + // Temp register won't be freed (LIFO constraint with inner locals). + let val_reg = fc.alloc_reg(); + let name_idx = fc.builder.add_name(&key_name); + fc.builder.emit_get_prop_name(val_reg, src, name_idx); + compile_destructuring_pattern(fc, &prop.value, val_reg)?; + } + } + + // Handle rest: collect remaining own enumerable properties. + if let Some(rest_pat) = rest { + // Collect extracted key names for exclusion. + let extracted_keys: Vec = properties + .iter() + .filter_map(|prop| match &prop.key { + PropertyKey::Identifier(s) | PropertyKey::String(s) => Some(s.clone()), + _ => None, + }) + .collect(); + + let rest_obj = fc.alloc_reg(); + fc.builder.emit_reg(Op::CreateObject, rest_obj); + + let keys_r = fc.alloc_reg(); + fc.builder.emit_reg_reg(Op::ForInInit, keys_r, src); + let idx_r = fc.alloc_reg(); + fc.builder.emit_load_int8(idx_r, 0); + let key_r = fc.alloc_reg(); + let done_r = fc.alloc_reg(); + + let loop_start = fc.builder.offset(); + fc.builder + .emit_reg4(Op::ForInNext, key_r, done_r, keys_r, idx_r); + let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r); + + let mut skip_patches = Vec::new(); + for excluded in &extracted_keys { + let excluded_r = fc.alloc_reg(); + let ci = fc.builder.add_constant(Constant::String(excluded.clone())); + fc.builder.emit_reg_u16(Op::LoadConst, excluded_r, ci); + let cmp_r = fc.alloc_reg(); + fc.builder.emit_reg3(Op::StrictEq, cmp_r, key_r, excluded_r); + let skip = fc.builder.emit_cond_jump(Op::JumpIfTrue, cmp_r); + skip_patches.push(skip); + fc.free_reg(cmp_r); + fc.free_reg(excluded_r); + } + + let val_r = fc.alloc_reg(); + fc.builder.emit_reg3(Op::GetProperty, val_r, src, key_r); + fc.builder + .emit_reg3(Op::SetProperty, rest_obj, key_r, val_r); + fc.free_reg(val_r); + + for patch in skip_patches { + fc.builder.patch_jump(patch); + } + + let one_r = fc.alloc_reg(); + fc.builder.emit_load_int8(one_r, 1); + fc.builder.emit_reg3(Op::Add, idx_r, idx_r, one_r); + fc.free_reg(one_r); + + fc.builder.emit_jump_to(loop_start); + fc.builder.patch_jump(exit_patch); + + fc.free_reg(done_r); + fc.free_reg(key_r); + fc.free_reg(idx_r); + fc.free_reg(keys_r); + + compile_destructuring_pattern(fc, rest_pat, rest_obj)?; } } - PatternKind::Array { elements, rest: _ } => { + PatternKind::Array { elements, rest } => { for (i, elem) in elements.iter().enumerate() { if let Some(pat) = elem { - let idx_reg = fc.alloc_reg(); - if i <= 127 { - fc.builder.emit_load_int8(idx_reg, i as i8); + // For simple identifier patterns, load directly into local. + if let PatternKind::Identifier(name) = &pat.kind { + let is_captured = fc.captured_names.contains(name.as_str()); + let reg = fc.define_local_ext(name, is_captured, false); + let idx_reg = fc.alloc_reg(); + if i <= 127 { + fc.builder.emit_load_int8(idx_reg, i as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(i as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + } + if is_captured { + let tmp = fc.alloc_reg(); + fc.builder.emit_reg3(Op::GetProperty, tmp, src, idx_reg); + fc.builder.emit_reg(Op::NewCell, reg); + fc.builder.emit_reg_reg(Op::CellStore, reg, tmp); + fc.free_reg(tmp); + } else { + fc.builder.emit_reg3(Op::GetProperty, reg, src, idx_reg); + } + fc.free_reg(idx_reg); } else { - let ci = fc.builder.add_constant(Constant::Number(i as f64)); - fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + // Complex inner pattern (nested, default, etc.) + let idx_reg = fc.alloc_reg(); + if i <= 127 { + fc.builder.emit_load_int8(idx_reg, i as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(i as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + } + let val_reg = fc.alloc_reg(); + fc.builder.emit_reg3(Op::GetProperty, val_reg, src, idx_reg); + compile_destructuring_pattern(fc, pat, val_reg)?; + // Don't free val_reg/idx_reg — inner pattern may have + // allocated locals above them. } - let val_reg = fc.alloc_reg(); - fc.builder.emit_reg3(Op::GetProperty, val_reg, src, idx_reg); - compile_destructuring_pattern(fc, pat, val_reg)?; - fc.free_reg(val_reg); - fc.free_reg(idx_reg); } } + + // Handle rest element: ...rest = src.slice(elements.len()) + if let Some(rest_pat) = rest { + let slice_fn_r = fc.alloc_reg(); + let slice_ni = fc.builder.add_name("slice"); + fc.builder.emit_get_prop_name(slice_fn_r, src, slice_ni); + + let this_ni = fc.builder.add_name("this"); + fc.builder.emit_store_global(this_ni, src); + + let start_r = fc.alloc_reg(); + let elem_count = elements.len(); + if elem_count <= 127 { + fc.builder.emit_load_int8(start_r, elem_count as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(elem_count as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, start_r, ci); + } + + let rest_val = fc.alloc_reg(); + fc.builder.emit_call(rest_val, slice_fn_r, start_r, 1); + + compile_destructuring_pattern(fc, rest_pat, rest_val)?; + // Don't free temps — rest pattern allocates locals. + } } PatternKind::Assign { left, right } => { // Default value: if src is undefined, use default. @@ -1194,7 +1460,7 @@ fn compile_destructuring_pattern( compile_expr(fc, right, val_reg)?; fc.builder.patch_jump(patch); compile_destructuring_pattern(fc, left, val_reg)?; - fc.free_reg(val_reg); + // Don't free val_reg — inner pattern may have allocated locals. } } Ok(()) @@ -1342,7 +1608,9 @@ fn compile_function_body_inner( // Implicit return undefined. inner.builder.emit_reg(Op::Return, result_reg); - Ok(inner.builder.finish()) + let mut func = inner.builder.finish(); + func.is_generator = func_def.is_generator; + Ok(func) } // ── Class declarations ────────────────────────────────────── @@ -2078,43 +2346,76 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } ExprKind::Array(elements) => { + let has_spread = elements + .iter() + .any(|e| matches!(e, Some(ArrayElement::Spread(_)))); + fc.builder.emit_reg(Op::CreateArray, dst); - for (i, elem) in elements.iter().enumerate() { - if let Some(el) = elem { - let val_reg = fc.alloc_reg(); + + if has_spread { + // When spreads are present, we track the index dynamically. + // For each normal element, push at current length. + // For spread elements, use the Spread opcode. + for el in elements.iter().flatten() { match el { - ArrayElement::Expr(e) => compile_expr(fc, e, val_reg)?, - ArrayElement::Spread(e) => { - // Spread in array: simplified, just compile the expression. + ArrayElement::Expr(e) => { + let val_reg = fc.alloc_reg(); compile_expr(fc, e, val_reg)?; + // Get current length as index. + let idx_reg = fc.alloc_reg(); + let len_ni = fc.builder.add_name("length"); + fc.builder.emit_get_prop_name(idx_reg, dst, len_ni); + fc.builder.emit_reg3(Op::SetProperty, dst, idx_reg, val_reg); + // Increment length. + let one_r = fc.alloc_reg(); + fc.builder.emit_load_int8(one_r, 1); + fc.builder.emit_reg3(Op::Add, idx_reg, idx_reg, one_r); + fc.builder.emit_set_prop_name(dst, len_ni, idx_reg); + fc.free_reg(one_r); + fc.free_reg(idx_reg); + fc.free_reg(val_reg); + } + ArrayElement::Spread(e) => { + let spread_src = fc.alloc_reg(); + compile_expr(fc, e, spread_src)?; + fc.builder.emit_spread(dst, spread_src); + fc.free_reg(spread_src); } } - let idx_reg = fc.alloc_reg(); - if i <= 127 { - fc.builder.emit_load_int8(idx_reg, i as i8); - } else { - let ci = fc.builder.add_constant(Constant::Number(i as f64)); - fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + } + } else { + // No spreads: use simple indexed assignment. + for (i, elem) in elements.iter().enumerate() { + if let Some(ArrayElement::Expr(e)) = elem { + let val_reg = fc.alloc_reg(); + compile_expr(fc, e, val_reg)?; + let idx_reg = fc.alloc_reg(); + if i <= 127 { + fc.builder.emit_load_int8(idx_reg, i as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(i as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + } + fc.builder.emit_reg3(Op::SetProperty, dst, idx_reg, val_reg); + fc.free_reg(idx_reg); + fc.free_reg(val_reg); } - fc.builder.emit_reg3(Op::SetProperty, dst, idx_reg, val_reg); - fc.free_reg(idx_reg); - fc.free_reg(val_reg); } - } - // Set length property to the number of elements. - if !elements.is_empty() { - let len_name = fc.builder.add_name("length"); - let len_reg = fc.alloc_reg(); - if elements.len() <= 127 { - fc.builder.emit_load_int8(len_reg, elements.len() as i8); - } else { - let ci = fc - .builder - .add_constant(Constant::Number(elements.len() as f64)); - fc.builder.emit_reg_u16(Op::LoadConst, len_reg, ci); + // Set length. + if !elements.is_empty() { + let len_name = fc.builder.add_name("length"); + let len_reg = fc.alloc_reg(); + if elements.len() <= 127 { + fc.builder.emit_load_int8(len_reg, elements.len() as i8); + } else { + let ci = fc + .builder + .add_constant(Constant::Number(elements.len() as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, len_reg, ci); + } + fc.builder.emit_set_prop_name(dst, len_name, len_reg); + fc.free_reg(len_reg); } - fc.builder.emit_set_prop_name(dst, len_name, len_reg); - fc.free_reg(len_reg); } } @@ -2386,15 +2687,78 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), fc.free_reg(func_reg); } - ExprKind::Yield { - argument, - delegate: _, - } => { - // Yield is a VM-level operation; for now compile the argument. - if let Some(arg) = argument { - compile_expr(fc, arg, dst)?; + ExprKind::Yield { argument, delegate } => { + if *delegate { + // yield* expr: iterate the sub-iterator and yield each value. + let iter_r = fc.alloc_reg(); + if let Some(arg) = argument { + compile_expr(fc, arg, iter_r)?; + } else { + fc.builder.emit_reg(Op::LoadUndefined, iter_r); + } + + // Get iterator from the expression. + let iter_method_r = fc.alloc_reg(); + let sym_iter_ni = fc.builder.add_name("@@iterator"); + fc.builder + .emit_get_prop_name(iter_method_r, iter_r, sym_iter_ni); + let this_ni = fc.builder.add_name("this"); + fc.builder.emit_store_global(this_ni, iter_r); + let iterator_r = fc.alloc_reg(); + let args_start = fc.next_reg; + fc.builder + .emit_call(iterator_r, iter_method_r, args_start, 0); + + // Get next method. + let next_r = fc.alloc_reg(); + let next_ni = fc.builder.add_name("next"); + fc.builder.emit_get_prop_name(next_r, iterator_r, next_ni); + + let result_r = fc.alloc_reg(); + let done_r = fc.alloc_reg(); + let val_r = fc.alloc_reg(); + + let loop_start = fc.builder.offset(); + + // Call next(). + fc.builder.emit_store_global(this_ni, iterator_r); + fc.builder.emit_call(result_r, next_r, args_start, 0); + + let done_ni = fc.builder.add_name("done"); + let value_ni = fc.builder.add_name("value"); + fc.builder.emit_get_prop_name(done_r, result_r, done_ni); + + let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r); + + fc.builder.emit_get_prop_name(val_r, result_r, value_ni); + + // Yield the value. + fc.builder.emit_yield(dst, val_r); + + // Jump back. + fc.builder.emit_jump_to(loop_start); + + // Exit: the last result's value is the yield* expression value. + fc.builder.patch_jump(exit_patch); + fc.builder.emit_get_prop_name(dst, result_r, value_ni); + + fc.free_reg(val_r); + fc.free_reg(done_r); + fc.free_reg(result_r); + fc.free_reg(next_r); + fc.free_reg(iterator_r); + fc.free_reg(iter_method_r); + fc.free_reg(iter_r); } else { - fc.builder.emit_reg(Op::LoadUndefined, dst); + // yield expr: emit Yield opcode. + let src = fc.alloc_reg(); + if let Some(arg) = argument { + compile_expr(fc, arg, src)?; + } else { + fc.builder.emit_reg(Op::LoadUndefined, src); + } + fc.builder.emit_yield(dst, src); + fc.free_reg(src); } } diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index ca4a6f9..ec7dc59 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -12,12 +12,42 @@ use std::fmt; // ── Heap objects (GC-managed) ──────────────────────────────── -/// A GC-managed heap object: a plain object, a function, or a closure cell. +/// A GC-managed heap object: a plain object, a function, a closure cell, or a generator. pub enum HeapObject { Object(ObjectData), Function(Box), /// A mutable cell holding one Value — used for closure-captured variables. Cell(Value), + /// A suspended generator function instance. + Generator(Box), +} + +/// State of a generator object. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GeneratorState { + /// Created but next() not yet called. + NotStarted, + /// Suspended at a yield point. + Suspended, + /// Currently executing (re-entrancy guard). + Executing, + /// Completed (returned or threw). + Completed, +} + +/// Data for a suspended generator function. +pub struct GeneratorData { + pub state: GeneratorState, + /// The generator function's bytecode. + pub func: Function, + /// Captured upvalues. + pub upvalues: Vec, + /// Saved register file for this generator's frame. + pub registers: Vec, + /// Saved instruction pointer (where to resume). + pub ip: usize, + /// The GcRef of the result prototype (for {value, done} objects). + pub prototype: Option, } impl Traceable for HeapObject { @@ -51,6 +81,19 @@ impl Traceable for HeapObject { visitor(r); } } + HeapObject::Generator(gen) => { + for &uv in &gen.upvalues { + visitor(uv); + } + for val in &gen.registers { + if let Some(r) = val.gc_ref() { + visitor(r); + } + } + if let Some(proto) = gen.prototype { + visitor(proto); + } + } } } } @@ -778,11 +821,54 @@ impl Vm { gc: &mut self.gc, this, }; - (native.callback)(args, &mut ctx) + let result = (native.callback)(args, &mut ctx)?; + + // Check for generator resume marker. + if let Value::Object(r) = &result { + let is_resume = matches!( + gc_get_property(&self.gc, *r, "__generator_resume__"), + Value::Boolean(true) + ); + if is_resume { + let gen_ref = match gc_get_property(&self.gc, *r, "__gen_ref__") { + Value::Object(gr) => gr, + _ => return Ok(Value::Undefined), + }; + let send_val = gc_get_property(&self.gc, *r, "__send_value__"); + let kind_str = match gc_get_property(&self.gc, *r, "__resume_kind__") { + Value::String(s) => s, + _ => "next".to_string(), + }; + return match kind_str.as_str() { + "next" => self.run_generator(gen_ref, send_val), + "return" => { + if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { + gen.state = GeneratorState::Completed; + } + Ok(self.make_iterator_result(send_val, true)) + } + "throw" => { + if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { + gen.state = GeneratorState::Completed; + } + Err(RuntimeError::type_error("Generator throw")) + } + _ => Ok(Value::Undefined), + }; + } + } + + Ok(result) } FunctionKind::Bytecode(bc) => { let callee_func = bc.func; + // Generator function: create a generator object instead of executing. + if callee_func.is_generator { + let gen_obj = self.create_generator_object(callee_func, upvalues, args); + return Ok(Value::Object(gen_obj)); + } + // Save current frames and run the function in isolation. let saved_frames = std::mem::take(&mut self.frames); @@ -1018,6 +1104,306 @@ impl Vm { i32::from_le_bytes(bytes) } + // ── Generator helpers ────────────────────────────────────── + + /// Create a generator object from a generator function. + fn create_generator_object( + &mut self, + func: Function, + upvalues: Vec, + args: &[Value], + ) -> GcRef { + // Pre-fill registers with arguments. + let reg_count = func.register_count as usize; + let mut regs = vec![Value::Undefined; reg_count]; + for (i, arg) in args.iter().enumerate() { + if i < func.param_count as usize { + regs[i] = arg.clone(); + } + } + + let gen_data = GeneratorData { + state: GeneratorState::NotStarted, + func, + upvalues, + registers: regs, + ip: 0, + prototype: self.object_prototype, + }; + + let gen_ref = self.gc.alloc(HeapObject::Generator(Box::new(gen_data))); + + // Wrap in an object with next/return/throw methods. + let mut obj = ObjectData::new(); + obj.prototype = self.object_prototype; + + // Store the generator GcRef so methods can find it. + obj.properties.insert( + "__gen__".to_string(), + Property { + value: Value::Object(gen_ref), + writable: false, + enumerable: false, + configurable: false, + }, + ); + + // next() method + let next_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "next".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: generator_next, + }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + obj.properties.insert( + "next".to_string(), + Property::builtin(Value::Function(next_fn)), + ); + + // return() method + let return_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "return".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: generator_return, + }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + obj.properties.insert( + "return".to_string(), + Property::builtin(Value::Function(return_fn)), + ); + + // throw() method + let throw_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "throw".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: generator_throw, + }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + obj.properties.insert( + "throw".to_string(), + Property::builtin(Value::Function(throw_fn)), + ); + + // @@iterator method (generators are iterable - returns self) + let iter_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "[Symbol.iterator]".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: generator_symbol_iterator, + }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + obj.properties.insert( + "@@iterator".to_string(), + Property::builtin(Value::Function(iter_fn)), + ); + + self.gc.alloc(HeapObject::Object(obj)) + } + + /// Create a {value, done} iterator result object. + fn make_iterator_result(&mut self, value: Value, done: bool) -> Value { + let mut obj = ObjectData::new(); + obj.prototype = self.object_prototype; + obj.properties + .insert("value".to_string(), Property::data(value)); + obj.properties + .insert("done".to_string(), Property::data(Value::Boolean(done))); + let gc_ref = self.gc.alloc(HeapObject::Object(obj)); + Value::Object(gc_ref) + } + + /// Run a generator until its next yield/return. + /// Returns the yielded/returned value. + pub fn run_generator( + &mut self, + gen_ref: GcRef, + send_value: Value, + ) -> Result { + // Extract generator data. + let (func, upvalues, mut regs, ip, state) = match self.gc.get(gen_ref) { + Some(HeapObject::Generator(gen)) => { + if gen.state == GeneratorState::Completed { + return Ok(self.make_iterator_result(Value::Undefined, true)); + } + if gen.state == GeneratorState::Executing { + return Err(RuntimeError::type_error("Generator is already executing")); + } + ( + gen.func.clone(), + gen.upvalues.clone(), + gen.registers.clone(), + gen.ip, + gen.state, + ) + } + _ => return Err(RuntimeError::type_error("not a generator")), + }; + + // Mark as executing. + if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { + gen.state = GeneratorState::Executing; + } + + // If resuming from a yield, write the sent value into the yield's dst register. + if state == GeneratorState::Suspended && ip >= 3 { + // The Yield instruction was: Yield dst, src (3 bytes total: op + dst + src) + // After executing Yield, ip points past it. The dst byte is at ip - 2. + let dst_reg = func.code[ip - 2] as usize; + regs[dst_reg] = send_value; + } + + // Save current VM state. + let saved_frames = std::mem::take(&mut self.frames); + let saved_instructions = self.instructions_executed; + + // Use a base past any existing register usage to avoid clobbering + // the caller's register file. + let base = saved_frames + .last() + .map(|f| f.base + f.func.register_count as usize) + .unwrap_or(0); + + let reg_count = func.register_count as usize; + self.ensure_registers(base + reg_count + 1); + + // Set up registers for the generator. + for (i, val) in regs.iter().enumerate() { + self.registers[base + i] = val.clone(); + } + + // Push frame. return_reg points to a slot that holds the generator ref + // so Yield can find it. We use a slot just past the registers. + self.registers[base + reg_count] = Value::Object(gen_ref); + + self.frames.push(CallFrame { + func, + ip, + base, + return_reg: base + reg_count, // slot holding gen_ref for Yield to find + exception_handlers: Vec::new(), + upvalues, + }); + + let result = self.run(); + + // Restore VM state. + self.frames = saved_frames; + self.instructions_executed = saved_instructions; + + match result { + Ok(val) => { + // Normal return from generator (either via Return or end of function). + // Check if it was a Yield (state == Suspended) or a Return (state stays Executing). + let gen_state = match self.gc.get(gen_ref) { + Some(HeapObject::Generator(gen)) => gen.state, + _ => GeneratorState::Completed, + }; + if gen_state == GeneratorState::Suspended { + // Yield already created the result; `val` is the {value, done} object. + Ok(val) + } else { + // Return: mark completed and wrap result. + if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { + gen.state = GeneratorState::Completed; + } + Ok(self.make_iterator_result(val, true)) + } + } + Err(err) => { + // Generator threw: mark completed. + if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { + gen.state = GeneratorState::Completed; + } + Err(err) + } + } + } + + // ── Iterator protocol helpers ──────────────────────────────── + + /// Get an iterator from a value by calling its [Symbol.iterator]() method. + pub fn get_iterator(&mut self, iterable: &Value) -> Result { + // Get the @@iterator property. + let iter_fn = match iterable { + Value::Object(gc_ref) | Value::Function(gc_ref) => { + gc_get_property(&self.gc, *gc_ref, "@@iterator") + } + Value::String(_) => { + // Strings have @@iterator on their prototype. + self.string_prototype + .map(|p| gc_get_property(&self.gc, p, "@@iterator")) + .unwrap_or(Value::Undefined) + } + _ => Value::Undefined, + }; + + let iter_fn_ref = match iter_fn { + Value::Function(r) => r, + _ => { + return Err(RuntimeError::type_error( + "object is not iterable (no Symbol.iterator)", + )); + } + }; + + // Call [Symbol.iterator]() with `this` set to the iterable. + // We temporarily set `this` in globals for the native call. + let old_this = self.globals.get("this").cloned(); + self.globals.insert("this".to_string(), iterable.clone()); + let result = self.call_function(iter_fn_ref, &[]); + match old_this { + Some(v) => self.globals.insert("this".to_string(), v), + None => self.globals.remove("this"), + }; + result + } + + /// Call iterator.next() and return (value, done). + pub fn iterator_next(&mut self, iterator: &Value) -> Result<(Value, bool), RuntimeError> { + let iter_ref = match iterator { + Value::Object(r) | Value::Function(r) => *r, + _ => return Err(RuntimeError::type_error("iterator is not an object")), + }; + + let next_fn = gc_get_property(&self.gc, iter_ref, "next"); + let next_fn_ref = match next_fn { + Value::Function(r) => r, + _ => return Err(RuntimeError::type_error("iterator.next is not a function")), + }; + + // Call next() with `this` = iterator. + let old_this = self.globals.get("this").cloned(); + self.globals.insert("this".to_string(), iterator.clone()); + let result = self.call_function(next_fn_ref, &[])?; + match old_this { + Some(v) => self.globals.insert("this".to_string(), v), + None => self.globals.remove("this"), + }; + + // Extract value and done from the result object. + let (value, done) = match result { + Value::Object(r) => { + let val = gc_get_property(&self.gc, r, "value"); + let d = gc_get_property(&self.gc, r, "done"); + (val, d.to_boolean()) + } + _ => (Value::Undefined, true), + }; + + Ok((value, done)) + } + /// Collect all GcRef values reachable from the mutator (roots for GC). fn collect_roots(&self) -> Vec { let mut roots = Vec::new(); @@ -1471,6 +1857,84 @@ impl Vm { }; match callback(&args, &mut ctx) { Ok(val) => { + // Check if this is a generator resume request. + if let Value::Object(r) = &val { + let is_resume = matches!( + gc_get_property(&self.gc, *r, "__generator_resume__"), + Value::Boolean(true) + ); + if is_resume { + let gen_ref = match gc_get_property( + &self.gc, + *r, + "__gen_ref__", + ) { + Value::Object(gr) => gr, + _ => { + self.registers[base + dst as usize] = + Value::Undefined; + continue; + } + }; + let send_val = + gc_get_property(&self.gc, *r, "__send_value__"); + let kind = match gc_get_property( + &self.gc, + *r, + "__resume_kind__", + ) { + Value::String(s) => s, + _ => "next".to_string(), + }; + + match kind.as_str() { + "next" => { + match self.run_generator(gen_ref, send_val) { + Ok(result) => { + self.registers[base + dst as usize] = + result; + } + Err(err) => { + let err_val = + err.to_value(&mut self.gc); + if !self.handle_exception(err_val) { + return Err(err); + } + } + } + } + "return" => { + // Force the generator to complete. + if let Some(HeapObject::Generator(gen)) = + self.gc.get_mut(gen_ref) + { + gen.state = GeneratorState::Completed; + } + let result = + self.make_iterator_result(send_val, true); + self.registers[base + dst as usize] = result; + } + "throw" => { + // Mark generator as completed and throw. + if let Some(HeapObject::Generator(gen)) = + self.gc.get_mut(gen_ref) + { + gen.state = GeneratorState::Completed; + } + if !self.handle_exception(send_val) { + return Err(RuntimeError::type_error( + "Generator throw", + )); + } + } + _ => { + self.registers[base + dst as usize] = + Value::Undefined; + } + } + continue; + } + } self.registers[base + dst as usize] = val; } Err(err) => { @@ -1482,6 +1946,17 @@ impl Vm { } } CallInfo::Bytecode(callee_func, callee_upvalues) => { + // Generator function: create a generator object instead of executing. + if callee_func.is_generator { + let gen_obj = self.create_generator_object( + callee_func, + callee_upvalues, + &args, + ); + self.registers[base + dst as usize] = Value::Object(gen_obj); + continue; + } + if self.frames.len() >= MAX_CALL_DEPTH { let err = RuntimeError::range_error("Maximum call stack size exceeded"); @@ -1962,6 +2437,114 @@ impl Vm { *cell_val = val; } } + + // ── Iterator / generator ───────────────────────── + Op::Yield => { + let _dst = Self::read_u8(&mut self.frames[fi]); + let src = Self::read_u8(&mut self.frames[fi]); + let base = self.frames[fi].base; + let yield_val = self.registers[base + src as usize].clone(); + + // Save the generator's state. + let frame = &self.frames[fi]; + let gen_ref = match self.registers.get(frame.return_reg) { + Some(Value::Object(r)) => *r, + _ => { + return Err(RuntimeError { + kind: ErrorKind::Error, + message: "Yield outside generator".into(), + }); + } + }; + + // Save registers and IP into the generator object. + let saved_ip = self.frames[fi].ip; + let saved_base = self.frames[fi].base; + let reg_count = self.frames[fi].func.register_count as usize; + let saved_regs: Vec = + self.registers[saved_base..saved_base + reg_count].to_vec(); + + if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { + gen.ip = saved_ip; + gen.registers = saved_regs; + gen.state = GeneratorState::Suspended; + } + + // Pop the generator frame. + self.frames.pop(); + + // Create {value, done: false} result. + let result = self.make_iterator_result(yield_val, false); + + // If this was the only frame, return the result. + if self.frames.is_empty() { + return Ok(result); + } + + // Otherwise, write the result into the generator_next caller's dst. + let caller_fi = self.frames.len() - 1; + let return_reg = self.frames[caller_fi].base; + // The return_reg is stored on the popped frame — but we stored + // the gen_ref there. We need to use the gen_next's return location. + // Actually, the generator frame's return_reg IS where the result goes. + let old_return_reg = self.registers.len().min(saved_base + reg_count); // just use gen_ref loc + let _ = old_return_reg; + let _ = return_reg; + // The result should go to the return register that was set when + // we pushed the frame. Since we already popped, we stored it at + // frame.return_reg. Let's look at where that was. + // Actually: the generator resumes via run_generator which pushed + // a frame with return_reg set. When Yield pops that frame, it needs + // to write the result to that return_reg. But the frame is already popped. + // Let's re-read it before popping. + // (This path handles generators called from the run loop.) + + // Actually, generators always run via run_generator which uses + // an isolated frame stack. After Yield pops, the frame stack is empty + // and we fall out of run() with the returned result. + // So this unreachable path should not happen. + return Ok(result); + } + + Op::Spread => { + let dst = Self::read_u8(&mut self.frames[fi]); + let src = Self::read_u8(&mut self.frames[fi]); + let base = self.frames[fi].base; + let iterable = self.registers[base + src as usize].clone(); + + // Get the iterator from the iterable. + let iterator = self.get_iterator(&iterable)?; + + // Iterate and push each element into the dst array. + loop { + let (value, done) = self.iterator_next(&iterator)?; + if done { + break; + } + // Push value into dst array. + let dst_ref = match self.registers[base + dst as usize] { + Value::Object(r) => r, + _ => break, + }; + if let Some(HeapObject::Object(data)) = self.gc.get_mut(dst_ref) { + let len = match data.properties.get("length") { + Some(prop) => prop.value.to_number() as usize, + None => 0, + }; + data.properties + .insert(len.to_string(), Property::data(value)); + data.properties.insert( + "length".to_string(), + Property { + value: Value::Number((len + 1) as f64), + writable: true, + enumerable: false, + configurable: false, + }, + ); + } + } + } } } } @@ -2033,6 +2616,123 @@ enum CallInfo { Bytecode(Function, Vec), } +// ── Generator native callbacks ────────────────────────────── + +/// Native callback for generator.next(value). +/// `this` is the generator wrapper object containing __gen__. +fn generator_next(args: &[Value], ctx: &mut NativeContext) -> Result { + // The generator wrapper stores the actual generator as __gen__. + let gen_ref = match &ctx.this { + Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__") { + Value::Object(gen_r) => gen_r, + _ => return Err(RuntimeError::type_error("not a generator")), + }, + _ => return Err(RuntimeError::type_error("not a generator")), + }; + + let send_value = args.first().cloned().unwrap_or(Value::Undefined); + + // We can't call run_generator from a NativeContext since we only have &mut Gc. + // Instead, store the request and let the caller handle it. + // This is a limitation — we need to restructure. + // For now, use a different approach: store the gen_ref and value in a special + // return value that the VM intercepts. + // Actually, generator.next() needs to be handled specially by the VM. + // Let's return a sentinel that the VM's call handling can detect. + + // Store gen_ref and send_value for the VM to process. + // We use a special object with __generator_resume__ marker. + let mut obj = ObjectData::new(); + obj.properties.insert( + "__generator_resume__".to_string(), + Property::builtin(Value::Boolean(true)), + ); + obj.properties.insert( + "__gen_ref__".to_string(), + Property::builtin(Value::Object(gen_ref)), + ); + obj.properties + .insert("__send_value__".to_string(), Property::builtin(send_value)); + obj.properties.insert( + "__resume_kind__".to_string(), + Property::builtin(Value::String("next".to_string())), + ); + let r = ctx.gc.alloc(HeapObject::Object(obj)); + Ok(Value::Object(r)) +} + +/// Native callback for generator.return(value). +fn generator_return(args: &[Value], ctx: &mut NativeContext) -> Result { + let gen_ref = match &ctx.this { + Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__") { + Value::Object(gen_r) => gen_r, + _ => return Err(RuntimeError::type_error("not a generator")), + }, + _ => return Err(RuntimeError::type_error("not a generator")), + }; + + let return_value = args.first().cloned().unwrap_or(Value::Undefined); + + let mut obj = ObjectData::new(); + obj.properties.insert( + "__generator_resume__".to_string(), + Property::builtin(Value::Boolean(true)), + ); + obj.properties.insert( + "__gen_ref__".to_string(), + Property::builtin(Value::Object(gen_ref)), + ); + obj.properties.insert( + "__send_value__".to_string(), + Property::builtin(return_value), + ); + obj.properties.insert( + "__resume_kind__".to_string(), + Property::builtin(Value::String("return".to_string())), + ); + let r = ctx.gc.alloc(HeapObject::Object(obj)); + Ok(Value::Object(r)) +} + +/// Native callback for generator.throw(error). +fn generator_throw(args: &[Value], ctx: &mut NativeContext) -> Result { + let gen_ref = match &ctx.this { + Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__") { + Value::Object(gen_r) => gen_r, + _ => return Err(RuntimeError::type_error("not a generator")), + }, + _ => return Err(RuntimeError::type_error("not a generator")), + }; + + let error_value = args.first().cloned().unwrap_or(Value::Undefined); + + let mut obj = ObjectData::new(); + obj.properties.insert( + "__generator_resume__".to_string(), + Property::builtin(Value::Boolean(true)), + ); + obj.properties.insert( + "__gen_ref__".to_string(), + Property::builtin(Value::Object(gen_ref)), + ); + obj.properties + .insert("__send_value__".to_string(), Property::builtin(error_value)); + obj.properties.insert( + "__resume_kind__".to_string(), + Property::builtin(Value::String("throw".to_string())), + ); + let r = ctx.gc.alloc(HeapObject::Object(obj)); + Ok(Value::Object(r)) +} + +/// Native callback for generator[Symbol.iterator]() — returns `this`. +fn generator_symbol_iterator( + _args: &[Value], + ctx: &mut NativeContext, +) -> Result { + Ok(ctx.this.clone()) +} + // ── Tests ──────────────────────────────────────────────────── #[cfg(test)] @@ -5437,4 +6137,292 @@ mod tests { v => panic!("expected 'fulfilled,rejected', got {v:?}"), } } + + // ── Iterator and for...of tests ──────────────────────── + + #[test] + fn test_for_of_array() { + match eval( + "var result = ''; var arr = [10, 20, 30]; for (var x of arr) { result = result + x + ','; } result", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "10,20,30,"), + v => panic!("expected '10,20,30,', got {v:?}"), + } + } + + #[test] + fn test_for_of_string() { + match eval("var result = ''; for (var ch of 'abc') { result = result + ch; } result") + .unwrap() + { + Value::String(s) => assert_eq!(s, "abc"), + v => panic!("expected 'abc', got {v:?}"), + } + } + + #[test] + fn test_for_of_with_break() { + match eval( + "var result = 0; for (var x of [1, 2, 3, 4, 5]) { if (x === 3) break; result = result + x; } result", + ) + .unwrap() + { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_for_of_with_continue() { + match eval( + "var result = 0; for (var x of [1, 2, 3, 4, 5]) { if (x === 3) continue; result = result + x; } result", + ) + .unwrap() + { + Value::Number(n) => assert_eq!(n, 12.0), + v => panic!("expected 12, got {v:?}"), + } + } + + // ── Generator tests ──────────────────────────────────── + + #[test] + fn test_generator_typeof() { + // First test: does gen() return an object? + match eval("function* gen() { yield 1; } typeof gen()").unwrap() { + Value::String(s) => assert_eq!(s, "object"), + v => panic!("expected 'object', got {v:?}"), + } + } + + #[test] + fn test_generator_has_next() { + // Test: does the generator have a next method? + match eval("function* gen() { yield 1; } var g = gen(); typeof g.next").unwrap() { + Value::String(s) => assert_eq!(s, "function"), + v => panic!("expected 'function', got {v:?}"), + } + } + + #[test] + fn test_basic_generator() { + match eval( + "function* gen() { yield 1; yield 2; yield 3; } + var g = gen(); + var a = g.next(); + a.value", + ) + .unwrap() + { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + } + + #[test] + fn test_generator_multiple_yields() { + match eval( + "function* gen() { yield 10; yield 20; yield 30; } + var g = gen(); + var r1 = g.next(); var r2 = g.next(); var r3 = g.next(); var r4 = g.next(); + '' + r1.value + ',' + r2.value + ',' + r3.value + ',' + r4.done", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "10,20,30,true"), + v => panic!("expected '10,20,30,true', got {v:?}"), + } + } + + #[test] + fn test_generator_send_value() { + match eval( + "function* gen() { var x = yield 'hello'; yield x + ' world'; } + var g = gen(); + g.next(); + var r = g.next('beautiful'); + r.value", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "beautiful world"), + v => panic!("expected 'beautiful world', got {v:?}"), + } + } + + #[test] + fn test_generator_return() { + match eval( + "function* gen() { yield 1; yield 2; yield 3; } + var g = gen(); + g.next(); + var r = g['return'](42); + '' + r.value + ',' + r.done", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "42,true"), + v => panic!("expected '42,true', got {v:?}"), + } + } + + #[test] + fn test_generator_in_for_of() { + match eval( + "function* range(start, end) { + for (var i = start; i < end; i = i + 1) { yield i; } + } + var result = 0; + for (var n of range(1, 5)) { result = result + n; } + result", + ) + .unwrap() + { + Value::Number(n) => assert_eq!(n, 10.0), + v => panic!("expected 10, got {v:?}"), + } + } + + #[test] + fn test_generator_with_return_value() { + match eval( + "function* gen() { yield 1; return 'done'; } + var g = gen(); + var a = g.next(); + var b = g.next(); + '' + a.value + ',' + a.done + ',' + b.value + ',' + b.done", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "1,false,done,true"), + v => panic!("expected '1,false,done,true', got {v:?}"), + } + } + + // ── Destructuring tests ──────────────────────────────── + + #[test] + fn test_array_destructuring_basic() { + match eval("var [a, b, c] = [1, 2, 3]; a + b + c").unwrap() { + Value::Number(n) => assert_eq!(n, 6.0), + v => panic!("expected 6, got {v:?}"), + } + } + + #[test] + fn test_array_destructuring_rest() { + match eval("var [first, ...rest] = [1, 2, 3, 4]; '' + first + ',' + rest.length").unwrap() { + Value::String(s) => assert_eq!(s, "1,3"), + v => panic!("expected '1,3', got {v:?}"), + } + } + + #[test] + fn test_array_destructuring_default() { + match eval("var [a = 10, b = 20] = [1]; '' + a + ',' + b").unwrap() { + Value::String(s) => assert_eq!(s, "1,20"), + v => panic!("expected '1,20', got {v:?}"), + } + } + + #[test] + fn test_object_destructuring_basic() { + match eval("var {x, y} = {x: 1, y: 2}; x + y").unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_object_destructuring_alias() { + match eval("var {x: a, y: b} = {x: 10, y: 20}; a + b").unwrap() { + Value::Number(n) => assert_eq!(n, 30.0), + v => panic!("expected 30, got {v:?}"), + } + } + + #[test] + fn test_nested_destructuring() { + match eval("var {a: {b}} = {a: {b: 42}}; b").unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + #[test] + fn test_destructuring_in_for_of() { + match eval( + "var result = 0; var pairs = [[1, 2], [3, 4], [5, 6]]; for (var [a, b] of pairs) { result = result + a + b; } result", + ) + .unwrap() + { + Value::Number(n) => assert_eq!(n, 21.0), + v => panic!("expected 21, got {v:?}"), + } + } + + // ── Spread tests ─────────────────────────────────────── + + #[test] + fn test_spread_in_array() { + match eval("var a = [1, 2, 3]; var b = [0, ...a, 4]; b.length").unwrap() { + Value::Number(n) => assert_eq!(n, 5.0), + v => panic!("expected 5, got {v:?}"), + } + } + + #[test] + fn test_spread_in_array_values() { + match eval( + "var a = [1, 2, 3]; var b = [0, ...a, 4]; '' + b[0] + ',' + b[1] + ',' + b[2] + ',' + b[3] + ',' + b[4]", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "0,1,2,3,4"), + v => panic!("expected '0,1,2,3,4', got {v:?}"), + } + } + + // ── Custom iterable tests ────────────────────────────── + + #[test] + fn test_custom_iterable() { + match eval( + "var obj = {}; + obj['@@iterator'] = function() { + var i = 0; + return { + next: function() { + i = i + 1; + if (i <= 3) return {value: i, done: false}; + return {value: undefined, done: true}; + } + }; + }; + var result = 0; + for (var v of obj) { result = result + v; } + result", + ) + .unwrap() + { + Value::Number(n) => assert_eq!(n, 6.0), + v => panic!("expected 6, got {v:?}"), + } + } + + // ── Array.from with iterables ────────────────────────── + + #[test] + fn test_array_keys_values_entries() { + match eval( + "var arr = [10, 20, 30]; var r = ''; for (var v of arr.values()) { r = r + v + ','; } r", + ) + .unwrap() + { + Value::String(s) => assert_eq!(s, "10,20,30,"), + v => panic!("expected '10,20,30,', got {v:?}"), + } + } } diff --git a/crates/js/tests/test262.rs b/crates/js/tests/test262.rs index 9f47152..a53f87b 100644 --- a/crates/js/tests/test262.rs +++ b/crates/js/tests/test262.rs @@ -129,7 +129,6 @@ const UNSUPPORTED_FEATURES: &[&str] = &[ "async-iteration", "top-level-await", // Generators and iterators - "generators", "async-generators", // Modules "import-assertions", -- 2.51.2