diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index e3c6348d..9403b9e4 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -539,6 +539,13 @@ define_opcodes! { DEFINE_ACCESSOR_ATTRS = 0x91 => "DEFINE_ACCESSOR_ATTRS", def = None, uses = [SlotSpec::Slot(Slot::Rd), SlotSpec::Slot(Slot::A), SlotSpec::Slot(Slot::B)], pure = false, jump = false, term = false, ic = false, + // 隐式全局写:ext 字 = 键常量池下标(u16)、a 槽 = 值寄存器;全局对象由 VM 运行期 + // 从 session 取(不依赖 this——函数体内隐式全局写时 regs[254] 不一定是全局对象)。 + // 数据属性可写/可枚举/可配置(未声明标识符 PutValue 与 eval 脚本声明共用); + // 全局对象不可扩展时 strict/sloppy 均抛 TypeError。 + DEFINE_GLOBAL_PROP_C = 0x98 => "DEFINE_GLOBAL_PROP_C", + def = None, uses = [SlotSpec::Slot(Slot::A)], + pure = false, jump = false, term = false, ic = false, // ── 成员复合赋值:位/移位 (0x64-0x69) ── COMPOUND_MEMBER_BIT_AND = 0x64 => "COMPOUND_MEMBER_BIT_AND", diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 04a76544..5dcd742f 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -207,6 +207,11 @@ pub struct CompileCtx { /// LOAD_GLOBAL(运行期查 global object 属性,缺失抛 ReferenceError)。 /// 按寄存器而非名字记录:块作用域同名新绑定持不同槽位,不会被误判为隐式全局。 pub(crate) implicit_global_reads: HashSet, + /// 未声明标识符写所登记的全局槽寄存器集合:`lookup_or_global` 未命中任何作用域 + /// 时登记全局作用域绑定并记录其寄存器,写调用点据此补全局对象属性写(sloppy) + /// 或抛 ReferenceError(strict)。子函数 ctx 从父继承——继承绑定命中同一寄存器, + /// 补写/抛错判定跨嵌套函数一致。 + pub(crate) implicit_global_writes: HashSet, /// 函数 `length` 属性值:首个带默认值形参之前的形参数(rest 不计)。 /// emit_params_prologue 前由编译入口从 param_specs 计算。 pub(crate) function_length: u32, @@ -332,6 +337,7 @@ impl CompileCtx { captured_bindings: BTreeMap::new(), upvalue_const_flags: HashSet::new(), implicit_global_reads: HashSet::new(), + implicit_global_writes: HashSet::new(), function_length: 0, const_overflow: false, with_stack: Vec::new(), @@ -461,6 +467,9 @@ impl CompileCtx { return reg; } let reg = self.alloc_reg(); + // 未声明标识符写:登记全局作用域绑定,记录寄存器供写调用点补全局对象属性 + // 写(sloppy)或抛 ReferenceError(strict)。 + self.implicit_global_writes.insert(reg); self.scopes.symbols.lookup_or_global(name, reg) } @@ -1096,6 +1105,9 @@ impl Emitter { ); inherited_reg_start = inherited_reg_start.max(binding.reg.saturating_add(1)); } + // 隐式全局写集合随继承绑定传入:父层未声明写已登记全局作用域,子层解析命中 + // 继承绑定时须同样补全局对象属性写(或严格模式抛错)。 + ctx.implicit_global_writes = parent_ctx.implicit_global_writes.clone(); for (name, reg) in extra_bindings { ctx.scopes.symbols.scopes[0].bindings.insert( (*name).to_string(), @@ -1488,6 +1500,29 @@ impl Emitter { } } + /// 隐式全局写(sloppy 未声明标识符写):把值寄存器写到全局对象可写/可枚举/ + /// 可配置数据属性(未解析引用上的 PutValue 语义)。全局对象由 VM 运行期从 + /// session 解析,不依赖 this。 + /// + /// # 边界与前提 + /// - 仅 `implicit_global_writes` 命中的寄存器调用(未声明标识符写)。 + /// + /// # 副作用 + /// - 定义全局对象数据属性(可写/可枚举/可配置),属性缺失时新建。 + pub(crate) fn emit_implicit_global_write(&self, name: &str, val_reg: u32, ctx: &mut CompileCtx) { + let idx = ctx.add_constant(Constant::String(name.to_string())); + ctx.inst(Inst::define_global_prop_c(Operand::Reg(val_reg), idx)); + } + + /// 严格模式未声明写:发射 ReferenceError 抛错指令序列(未解析引用不可 put, + /// 值无关,编译期拦截)。错误消息格式与读侧 LOAD_GLOBAL 运行期消息一致。 + /// + /// # 副作用 + /// - 发射 THROW 指令序列,其后控制流不可达,dummy 值保持寄存器良定义。 + pub(crate) fn emit_strict_undeclared_write(&self, name: &str, ctx: &mut CompileCtx) -> Result { + self.emit_throw_error("ReferenceError", &format!("{name} is not defined"), ctx) + } + /// 把脚本顶层 var/function 绑定的当前值同步写入全局对象属性,使顶层声明 /// 可经 `globalThis` 反射(脚本环境记录的 var 绑定全局对象属性)。 /// diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index fe72159b..fd7b0672 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -72,7 +72,15 @@ impl Emitter { Ok(val_reg) } else { let var_reg = ctx.lookup_or_global(name); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明复合写:未解析引用不可 put,值无关抛 ReferenceError。 + return self.emit_strict_undeclared_write(name, ctx); + } ctx.inst(Inst::new(op, Operand::Reg(var_reg), Operand::Reg(rhs), Operand::None)); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } Ok(var_reg) } } @@ -368,12 +376,23 @@ impl Emitter { let is_const = ctx.lookup_const_flag(name); let const_flag = if is_const { 1 } else { 0 }; let var_reg = ctx.lookup_or_global(name); - ctx.inst(Inst::new( - OpCode::STORE_VAR, - Operand::Reg(var_reg), - Operand::Reg(val_reg), - Operand::Imm(const_flag), - )); + // 隐式全局判定以寄存器集合为准:读侧(旧值解析)已登记绑定, + // 写侧二次解析命中集合而非"新登记"。 + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,后续 LOAD_VAR 不可达。 + let _ = self.emit_strict_undeclared_write(name, ctx)?; + } else { + ctx.inst(Inst::new( + OpCode::STORE_VAR, + Operand::Reg(var_reg), + Operand::Reg(val_reg), + Operand::Imm(const_flag), + )); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } + } } ctx.inst(Inst::new( OpCode::LOAD_VAR, diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index 5de025c5..b3358046 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -164,12 +164,21 @@ impl Emitter { return; } let var_reg = ctx.lookup_or_global(name); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明写:发射 ReferenceError 抛错,跳过寄存器写(值无关)。 + let _ = self.emit_strict_undeclared_write(name, ctx); + return; + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(val_reg), Operand::Imm(const_flag), )); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } } /// with 内动态写入:对象有该属性则写对象,否则回退静态写入。 diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 00caea1e..545b8cdb 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -599,6 +599,11 @@ impl Emitter { Ok(if update.prefix { new_reg } else { old_reg }) } else { let var_reg = ctx.lookup_or_global(name); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明更新写:值无关抛 ReferenceError。 + return self.emit_strict_undeclared_write(name, ctx); + } let result_reg = ctx.alloc_reg(); let op = match (update.operator, update.prefix) { (UpdateOperator::Increment, true) => OpCode::INC_PRE, @@ -607,6 +612,9 @@ impl Emitter { (UpdateOperator::Decrement, false) => OpCode::DEC_POST, }; ctx.inst(Inst::new(op, Operand::Reg(var_reg), Operand::Reg(result_reg), Operand::Reg(result_reg))); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } Ok(result_reg) } } diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index 1b721312..070fcd4f 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -115,12 +115,21 @@ impl Emitter { self.emit_identifier_tdz_guard(name, ctx)?; self.emit_const_write_guard(name, ctx)?; let var_reg = ctx.lookup_or_global(name); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 + self.emit_strict_undeclared_write(name, ctx)?; + return Ok(()); + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(src_reg), Operand::Imm(if ctx.lookup_const_flag(name) { 1 } else { 0 }), )); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } Ok(()) } AssignmentTarget::ArrayAssignmentTarget(ap) => self.emit_array_assignment(ap, src_reg, ctx), @@ -300,12 +309,21 @@ impl Emitter { self.emit_identifier_tdz_guard(name, ctx)?; self.emit_const_write_guard(name, ctx)?; let var_reg = ctx.lookup_or_global(name); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 + self.emit_strict_undeclared_write(name, ctx)?; + return Ok(()); + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(src_reg), Operand::Imm(if ctx.lookup_const_flag(name) { 1 } else { 0 }), )); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } Ok(()) } _ => Err("assignment target not supported".into()), @@ -367,12 +385,21 @@ impl Emitter { self.emit_identifier_tdz_guard(name, ctx)?; self.emit_const_write_guard(name, ctx)?; let var_reg = ctx.lookup_or_global(name); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 + self.emit_strict_undeclared_write(name, ctx)?; + return Ok(()); + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(prop_reg), Operand::Imm(if ctx.lookup_const_flag(name) { 1 } else { 0 }), )); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } } AssignmentTargetProperty::AssignmentTargetPropertyProperty(prop) => { let (prop_reg, static_key, key_reg) = diff --git a/crates/oxide_emit/src/stmt/iteration/for_in.rs b/crates/oxide_emit/src/stmt/iteration/for_in.rs index 3b5f6155..354c7422 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_in.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_in.rs @@ -64,7 +64,17 @@ impl Emitter { ForStatementLeft::AssignmentTargetIdentifier(id_ref) => { let name = id_ref.name.as_str(); let var_reg = ctx.lookup_or_global(name); - ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(key_reg), Operand::None)); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + // 未声明写仅在本迭代实际产生键后才发生(for-in NEXT 之后),空集合不抛。 + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 + self.emit_strict_undeclared_write(name, ctx)?; + } else { + ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(key_reg), Operand::None)); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } + } } _ => return Err("unsupported for-in left-hand side".into()), } diff --git a/crates/oxide_emit/src/stmt/iteration/for_of.rs b/crates/oxide_emit/src/stmt/iteration/for_of.rs index 3ad5cb75..8712e7db 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_of.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_of.rs @@ -108,7 +108,17 @@ impl Emitter { ForStatementLeft::AssignmentTargetIdentifier(id_ref) => { let name = id_ref.name.as_str(); let var_reg = ctx.lookup_or_global(name); - ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(val_reg), Operand::None)); + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + // 未声明写仅在本迭代实际产生值后才发生(for-of NEXT 之后),空集合不抛。 + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 + self.emit_strict_undeclared_write(name, ctx)?; + } else { + ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(val_reg), Operand::None)); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } + } } ForStatementLeft::ArrayAssignmentTarget(ap) => { self.emit_array_assignment(ap, val_reg, ctx)?; diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index 1e5edc4a..3e08203c 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -740,6 +740,7 @@ mod tests { assert_group(&[OpCode::NEW_OBJECT, OpCode::NEW_ARRAY], Some(1), &[], true); // define 语义属性写入 / arguments / rest assert_group(&[OpCode::DEFINE_PROP, OpCode::DEFINE_GLOBAL_PROP], None, &[1, 2, 3], false); + assert_contract(OpCode::DEFINE_GLOBAL_PROP_C, None, &[2], false); assert_contract(OpCode::DEFINE_PROP_ATTRS, None, &[1, 2, 3], false); assert_contract(OpCode::DEFINE_ACCESSOR_ATTRS, None, &[1, 2, 3], false); assert_group(&[OpCode::CREATE_ARGUMENTS, OpCode::CREATE_REST_ARRAY], Some(1), &[], false); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index a4abaf27..042c8aa8 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -229,6 +229,13 @@ impl Inst { Self::new(OpCode::DEFINE_GLOBAL_PROP, target, value, key) } + /// 定义隐式全局数据属性:ext = [key_idx](键常量池下标),value 放 a 槽。 + /// 全局对象由 VM 运行期从 session 解析(不依赖 this 寄存器),属性可写/可枚举/ + /// 可配置(未声明标识符写与 eval 脚本 var/函数声明的属性描述符)。 + pub fn define_global_prop_c(value: Operand, key_idx: u16) -> Self { + Self::with_ext(OpCode::DEFINE_GLOBAL_PROP_C, Operand::None, value, Operand::None, &[key_idx as u32]) + } + /// 静态删除属性:obj 同时放 rd/a 槽,const_idx 为属性名常量下标。 pub fn delete_prop_static(obj: Operand, const_idx: u32) -> Self { Self::with_ext(OpCode::DELETE_PROP_STATIC, obj, obj, Operand::None, &[const_idx]) diff --git a/crates/oxide_vm/src/ic_helper.rs b/crates/oxide_vm/src/ic_helper.rs index d59c1777..ce9d2b79 100644 --- a/crates/oxide_vm/src/ic_helper.rs +++ b/crates/oxide_vm/src/ic_helper.rs @@ -66,6 +66,7 @@ fn ext_word_count(bytecode: &[Instr], pc: usize) -> usize { | OpCode::DEFINE_ACCESSOR | OpCode::DEFINE_ACCESSOR_DYNAMIC | OpCode::DEFINE_PROP_ATTRS + | OpCode::DEFINE_GLOBAL_PROP_C | OpCode::DELETE_PROP_STATIC | OpCode::REST_OBJECT | OpCode::INIT_PRIVATE => 1, diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 1bbeb54a..26d9c4f4 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1834,6 +1834,12 @@ impl Vm { self.dispatch_define_global_prop(rd, a, b)?; } + OpCode::DEFINE_GLOBAL_PROP_C => { + let key_idx = self.bytecode[self.pc] as u16; + self.pc += 1; + self.dispatch_define_global_prop_c(a, key_idx)?; + } + OpCode::DEFINE_PROP_ATTRS => { let attrs = self.bytecode[self.pc] as u8; self.pc += 1; diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 273971e9..0bcae93b 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -1,7 +1,7 @@ use crate::vm::{Completion, FrameArgs, FrameContinuation, TryHandler, Vm}; use crate::vm_trace; use oxide_bytecode::opcode; -use oxide_types::object::PropAttributes; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::make_private_name_id; use oxide_types::value::JsValue; @@ -294,6 +294,42 @@ impl Vm { Ok(()) } + /// 定义隐式全局数据属性:ext 字 = 键常量池下标(u16),a 槽 = 值寄存器。 + /// 全局对象由 session 直接解析——函数体内隐式全局写不依赖 this(regs[254] + /// 不一定是全局对象)。属性可写/可枚举/可配置(未声明标识符 PutValue 与 + /// eval 脚本 var/函数声明的属性描述符)。 + /// + /// # 边界与前提 + /// - 键常量必须是字符串(emit 侧保证);非字符串按错误返回。 + /// - 全局对象不可扩展且属性缺失 → TypeError(两模式均抛,CreateGlobalVarBinding 语义); + /// 已有不可写属性时 sloppy PutValue 静默 no-op,严格模式抛错。 + pub(crate) fn dispatch_define_global_prop_c(&mut self, a: usize, key_idx: u16) -> Result<(), String> { + let idx = key_idx as usize; + vm_trace!("DEFINE_GLOBAL_PROP_C value={} idx={}", a, idx); + let key_val = self.immutables().get(idx).copied().unwrap_or(JsValue::undefined()); + if !key_val.is_string() { + return Err(format!("DEFINE_GLOBAL_PROP_C constant index {idx} is not a string key")); + } + let si = self.property_key_si(key_val)?; + let value = self.regs[a]; + let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; + // SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。 + let obj = unsafe { &mut *global_ptr }; + match self.define_data_property(obj, si, value, PropAttributes::new(true, true, true)) { + Ok(()) => Ok(()), + Err(msg) => { + // 不可扩展 + 新属性:规范两模式均抛;已有不可写属性:sloppy 静默、strict 抛。 + let non_extensible_new = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), si).is_none() + && !obj.is_extensible(); + if non_extensible_new || self.current_strict() { + self.raise_error_kind("TypeError", &msg) + } else { + Ok(()) + } + } + } + } + /// break 完成:`crossed`(rd 槽)为 emit 词法算出的逃出 finally 域数。 /// 逐个穿越 finally 后跳转到目标;crossed 为 0 时直接跳转。 /// ext 字携带逃出的迭代器层数:无 finally 穿越时立即关闭后跳转,有 finally diff --git a/crates/oxide_vm/tests/implicit_global_write_tests.rs b/crates/oxide_vm/tests/implicit_global_write_tests.rs new file mode 100644 index 00000000..eb6317c9 --- /dev/null +++ b/crates/oxide_vm/tests/implicit_global_write_tests.rs @@ -0,0 +1,163 @@ +//! sloppy 未声明标识符写(隐式全局创建)语义测试:sloppy 未解析引用写在全局对象 +//! 建可写/可枚举/可配置数据属性;严格模式同一写抛 ReferenceError(编译期拦截)。 +//! 覆盖脚本顶层、函数体(全局对象经 session 解析,不依赖 this)、解构目标、 +//! 更新式/逻辑赋值、for-in/for-of 左侧与不可扩展全局对象分支。 + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + let mut vm = Vm::new(); + vm.run(&module) +} + +fn eval_many(lines: &[&str]) -> Result { + eval(&lines.join("; ")) +} + +fn assert_err_contains(result: Result, expected: &str) { + match result { + Ok(_) => panic!("expected error containing '{}', got Ok", expected), + Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e), + } +} + +/// 求值并把结果按字符串提取(`JsValue` Display 不携带字符串内容)。 +fn eval_str(source: &str) -> String { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&module).expect("run"); + vm.lookup_str(result).unwrap_or_else(|| format!("{result}")) +} + +// ── sloppy 顶层未声明写:全局对象新建可写/可枚举/可配置数据属性 ── +#[test] +fn sloppy_undeclared_write_creates_global_property() { + let r = eval_many(&["x = 1", "globalThis.x === 1"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "sloppy 未声明写应建全局属性,实际 {:?}", r); +} + +#[test] +fn sloppy_undeclared_write_descriptor_writable_enumerable_configurable() { + let s = eval_str( + "y = 2; var d = Object.getOwnPropertyDescriptor(globalThis, 'y'); \ + d.writable + ':' + d.enumerable + ':' + d.configurable", + ); + assert_eq!(s, "true:true:true", "隐式全局属性描述符应全 true,实际 {s}"); +} + +// ── 可配置:隐式全局属性经 Object.defineProperty 可再描述(var 绑定不可配置) ── +#[test] +fn sloppy_undeclared_write_property_reconfigurable() { + let r = + eval_many(&["x = 1", "Object.defineProperty(globalThis, 'x', { value: 9 })", "globalThis.x === 9"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "可配置隐式全局属性应可再定义,实际 {:?}", r); +} + +// ── 函数体内未声明写:全局对象经 session 解析(不依赖 this) ── +#[test] +fn sloppy_function_body_undeclared_write_lands_on_global() { + let r = eval_many(&["function f() { x = 7 }", "f()", "globalThis.x === 7"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "函数体内未声明写应落全局对象,实际 {:?}", r); +} + +// ── 解构赋值目标:未声明标识符走同一隐式全局路径 ── +#[test] +fn sloppy_destructuring_undeclared_write_lands_on_global() { + let r = eval_many(&["({ q } = { q: 5 })", "globalThis.q === 5"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "解构未声明目标应落全局对象,实际 {:?}", r); +} + +// ── 更新式赋值:未声明名 RMW 旧值 undefined,落全局 NaN(undefined + 1) ── +#[test] +fn sloppy_update_operator_undeclared_write_lands_on_global() { + let r = eval_many(&["z++", "Number.isNaN(globalThis.z)"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "z++ 未声明写应落全局 NaN(undefined+1),实际 {:?}", r); +} + +#[test] +fn sloppy_compound_operator_undeclared_write_is_nan() { + let s = eval_str("w += 1; typeof w"); + assert_eq!(s, "number", "w += 1 未声明名应为 NaN(typeof number),实际 {s}"); +} + +// ── 逻辑赋值:未声明名 ||= 短路建全局 ── +#[test] +fn sloppy_logical_assign_undeclared_write_lands_on_global() { + let r = eval_many(&["p ||= 5", "globalThis.p === 5"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "p ||= 5 未声明写应落全局为 5,实际 {:?}", r); +} + +// ── for-in 左侧未声明标识符:首个键后建全局 ── +#[test] +fn sloppy_for_in_undeclared_write_lands_on_global() { + let r = eval_many(&["for (k in { a: 1 }) {}", "globalThis.k === 'a'"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "for-in 未声明左侧应落全局为 'a',实际 {:?}", r); +} + +// ── for-of 左侧未声明标识符:末次迭代值落全局 ── +#[test] +fn sloppy_for_of_undeclared_write_lands_on_global() { + let r = eval_many(&["for (v of [1, 2]) {}", "globalThis.v === 2"]).unwrap(); + assert!(r.is_bool() && r.as_bool(), "for-of 未声明左侧应落全局为 2,实际 {:?}", r); +} + +// ── 严格模式:顶层未声明写抛 ReferenceError(消息与读侧一致) ── +#[test] +fn strict_top_level_undeclared_write_throws() { + let result = eval("'use strict'; x = 1"); + assert_err_contains(result, "x is not defined"); +} + +// ── 严格模式:函数体内未声明写抛 ReferenceError ── +#[test] +fn strict_function_body_undeclared_write_throws() { + let result = eval("function f() { 'use strict'; y = 2 }; f()"); + assert_err_contains(result, "y is not defined"); +} + +// ── 严格 for-in:空对象无键不抛(PutValue 仅首键后发生) ── +#[test] +fn strict_for_in_empty_object_does_not_throw() { + let r = eval("'use strict'; for (k in {}) {}; true").unwrap(); + assert!(r.is_bool() && r.as_bool(), "严格空 for-in 不应抛错,实际 {:?}", r); +} + +// ── 严格 for-of:空迭代器不抛 ── +#[test] +fn strict_for_of_empty_does_not_throw() { + let r = eval("'use strict'; for (v of []) {}; true").unwrap(); + assert!(r.is_bool() && r.as_bool(), "严格空 for-of 不应抛错,实际 {:?}", r); +} + +// ── 全局对象不可扩展 + 属性缺失:sloppy 未声明写抛 TypeError ── +#[test] +fn sloppy_undeclared_write_on_non_extensible_global_throws() { + let result = eval("Object.preventExtensions(globalThis); t = 1"); + assert_err_contains(result, "not extensible"); +} + +// ── 全局对象不可扩展 + 属性缺失:严格未声明写先抛 ReferenceError ── +#[test] +fn strict_undeclared_write_on_non_extensible_global_throws_reference() { + let result = eval("'use strict'; Object.preventExtensions(globalThis); t = 1"); + assert_err_contains(result, "t is not defined"); +} + +// ── 既有不可写非可配置全局属性:sloppy 未声明写静默 no-op 不抛错 ── +#[test] +fn sloppy_undeclared_write_to_readonly_global_property_silent() { + let r = eval_many(&[ + "Object.defineProperty(globalThis, 'gw', { value: 0, writable: false, configurable: false })", + "gw = 1", + "globalThis.gw === 0", + ]) + .unwrap(); + assert!(r.is_bool() && r.as_bool(), "sloppy 对既有不可写全局属性写应静默 no-op,实际 {:?}", r); +}