diff --git a/Cargo.lock b/Cargo.lock index 34eadae1..d68943f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.13.20" +version = "0.13.21" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.13.20" +version = "0.13.21" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.13.20" +version = "0.13.21" dependencies = [ "ansi_term", "bumpalo", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.13.20" +version = "0.13.21" dependencies = [ "lru", "oxide_bytecode", @@ -1235,7 +1235,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.13.20" +version = "0.13.21" dependencies = [ "num-bigint", "oxc_allocator", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.13.20" +version = "0.13.21" dependencies = [ "bumpalo", "dashmap", @@ -1319,7 +1319,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.13.20" +version = "0.13.21" dependencies = [ "tracing", "tracing-appender", @@ -1340,7 +1340,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1371,7 +1371,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.13.20" +version = "0.13.21" dependencies = [ "num-bigint", "num-traits", @@ -1384,7 +1384,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.13.20" +version = "0.13.21" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1402,7 +1402,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.13.20" +version = "0.13.21" dependencies = [ "bumpalo", "dashmap", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.13.20" +version = "0.13.21" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 340605d1..23c11b5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.13.20" +version = "0.13.21" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index 446d52bb..1e4911af 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -116,6 +116,9 @@ pub struct CompileCtx { /// 本函数从父函数捕获的 const 绑定名:子 ctx 不继承父函数作用域符号表, /// 捕获 const 信息随 upvalue 收集一并快照,供 const 写检查(编译期拦截)使用。 pub(crate) upvalue_const_flags: HashSet, + /// 本函数从父函数捕获的函数名不可写绑定名:子 ctx 不继承父函数作用域符号表, + /// 捕获不可写信息随 upvalue 收集一并快照,供写路径 non_writable 守卫(编译期拦截)使用。 + pub(crate) upvalue_non_writable_flags: HashSet, /// 未声明标识符读所分配的全局槽寄存器集合:标识符首次读未命中任何作用域时, /// `lookup_or_builtin` 按隐式全局登记并记录其寄存器,后续读取据此发射 /// LOAD_GLOBAL(运行期查 global object 属性,缺失抛 ReferenceError)。 @@ -286,6 +289,7 @@ impl CompileCtx { pending_for_head_names: HashSet::new(), global_tier_names: HashSet::new(), upvalue_const_flags: HashSet::new(), + upvalue_non_writable_flags: HashSet::new(), implicit_global_reads: HashSet::new(), implicit_global_writes: HashSet::new(), function_length: 0, @@ -568,6 +572,12 @@ impl CompileCtx { self.scopes.symbols.lookup_is_const(name) || self.upvalue_const_flags.contains(name) } + /// 写路径 non_writable 守卫查询:名字是否为函数名不可写绑定(本作用域或 upvalue 捕获)。 + pub(crate) fn lookup_non_writable_flag(&self, name: &str) -> bool { + // upvalue 捕获的函数名:子 ctx 符号表不含父函数作用域绑定,查快照标志。 + self.scopes.symbols.lookup_non_writable(name) || self.upvalue_non_writable_flags.contains(name) + } + pub(crate) fn init_var(&mut self, name: &str) { self.scopes.symbols.init_var(name); } diff --git a/crates/oxide_emit/src/errors.rs b/crates/oxide_emit/src/errors.rs index db72802e..2dfb4abb 100644 --- a/crates/oxide_emit/src/errors.rs +++ b/crates/oxide_emit/src/errors.rs @@ -72,10 +72,16 @@ impl Emitter { /// const 写检查:已初始化的 const 绑定再赋值编译期抛 TypeError(与槽值无关)。 /// 简单赋值在 RHS 求值之后、复合/更新在读旧值之前调用;解构赋值在写目标时调用。 /// + /// # 边界与前提 + /// - 函数名不可写绑定同形拦截:strict 抛 TypeError;sloppy 不抛(put 静默失败), + /// 由调用点跳过写指令(值照算)。 + /// /// # 副作用 - /// - const 命中时发射 THROW 指令序列,其后写指令不可达但保持寄存器良定义。 + /// - const 或 strict 函数名不可写命中时发射 THROW 指令序列,其后写指令不可达但保持寄存器良定义。 pub(crate) fn emit_const_write_guard(&self, name: &str, ctx: &mut CompileCtx) -> Result<(), String> { - if ctx.lookup_const_flag(name) { + // const 与 strict 函数名不可写同形抛 TypeError(值无关);sloppy 函数名不可写不抛, + // 由调用点跳过写指令。 + if ctx.lookup_const_flag(name) || (ctx.lookup_non_writable_flag(name) && ctx.is_strict) { let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx)?; } Ok(()) diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index 6e792f71..bb5ea0ab 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -24,6 +24,32 @@ impl Emitter { // 循环 update 段的 let/const 循环变量是 per-iteration 可变绑定,复合写同样合法,豁免。 if !ctx.register_update_names.iter().any(|n| n == name) { self.emit_const_write_guard(name, ctx)?; + // 函数名不可写绑定:put 永不成功——sloppy 值照算(表达式值 = 运算结果), + // 跳过槽写;strict 已在 emit_const_write_guard 抛 TypeError(不可达)。 + if ctx.lookup_non_writable_flag(name) { + let val_reg = ctx.alloc_reg(); + let uv_idx = ctx.current_upvalue_captures.iter().position(|u| u.name == name); + if let Some(uv) = uv_idx { + ctx.inst(Inst::new( + OpCode::LOAD_UPVALUE, + Operand::Reg(val_reg), + Operand::Imm(uv as u16), + Operand::None, + )); + } else if let Some((cell_idx, binding_reg)) = ctx.visible_cell(name) { + ctx.inst(Inst::new( + OpCode::CELL_GET, + Operand::Reg(val_reg), + Operand::Reg(binding_reg), + Operand::Imm(cell_idx as u16), + )); + } else { + let var_reg = ctx.lookup_or_global(name); + ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(val_reg), Operand::Reg(var_reg), Operand::None)); + } + ctx.inst(Inst::new(op, Operand::Reg(val_reg), Operand::Reg(rhs), Operand::None)); + return Ok(val_reg); + } } // 循环 update 段:被捕获绑定走寄存器 RMW——C 风格 for 的 let/const // 循环变量每迭代新分配一个 cell,update 写寄存器供其拷入,不污染本迭代 @@ -433,6 +459,19 @@ impl Emitter { let val_reg = self.emit_expression(&assign.right, ctx)?; // 短路未通过才写:const 目标在此抛 TypeError(编译期拦截,值无关)。 self.emit_const_write_guard(name, ctx)?; + // 函数名不可写绑定(sloppy):put 永不成功,跳过写与命名空间写穿, + // 表达式值 = RHS(短路已通过故取 val_reg)。strict 已在 + // emit_const_write_guard 抛 TypeError(不可达)。 + if ctx.lookup_non_writable_flag(name) { + ctx.inst(Inst::new( + OpCode::LOAD_VAR, + Operand::Reg(result_reg), + Operand::Reg(val_reg), + Operand::None, + )); + ctx.labels.set_label_pos(end_label, ctx.insts.len()); + return Ok(result_reg); + } let const_flag = if ctx.lookup_const_flag(name) { 1 } else { 0 }; if let Some(uv) = uv_idx { ctx.inst(Inst::new( diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index 0c245da2..e9760024 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -181,6 +181,14 @@ impl Emitter { let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx)?; return Ok(()); } + // 函数名不可写绑定:put 永不成功——sloppy 静默跳过写指令(RHS 已求值,副作用保留), + // strict 抛 TypeError(与 const 路径同形,值无关)。with 回退写经本入口同形覆盖。 + if !in_loop_update && ctx.lookup_non_writable_flag(name) { + if ctx.is_strict { + let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx)?; + } + return Ok(()); + } // 循环 update 段:被捕获绑定走寄存器而非 cell——C 风格 for 的 let/const // 循环变量每迭代新分配一个 cell,update 写寄存器供其拷入,不污染本迭代 // 闭包捕获的 cell(机制见 `compile_ctx.rs` 的 `register_update_names` 字段文档)。 diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 43528459..4535fdf1 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -659,6 +659,41 @@ impl Emitter { self.emit_identifier_tdz_guard(name, ctx)?; if !ctx.register_update_names.iter().any(|n| n == name) { self.emit_const_write_guard(name, ctx)?; + // 函数名不可写绑定(sloppy):put 永不成功,值照算(前缀返回新值、后缀返回 + // 旧值),跳过槽写;strict 已在 emit_const_write_guard 抛 TypeError(不可达)。 + if ctx.lookup_non_writable_flag(name) { + let uv_idx = ctx.current_upvalue_captures.iter().position(|u| u.name == name); + let old_reg = ctx.alloc_reg(); + if let Some(uv) = uv_idx { + ctx.inst(Inst::new( + OpCode::LOAD_UPVALUE, + Operand::Reg(old_reg), + Operand::Imm(uv as u16), + Operand::None, + )); + } else if let Some((cell_idx, binding_reg)) = ctx.visible_cell(name) { + ctx.inst(Inst::new( + OpCode::CELL_GET, + Operand::Reg(old_reg), + Operand::Reg(binding_reg), + Operand::Imm(cell_idx as u16), + )); + } else { + let var_reg = ctx.lookup_or_global(name); + ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(old_reg), Operand::Reg(var_reg), Operand::None)); + } + let one_idx = ctx.add_constant(Constant::Int(1)); + let one_reg = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(one_reg), one_idx)); + let op = if update.operator == UpdateOperator::Increment { + OpCode::ADD + } else { + OpCode::SUB + }; + let new_reg = ctx.alloc_reg(); + ctx.inst(Inst::new(op, Operand::Reg(new_reg), Operand::Reg(old_reg), Operand::Reg(one_reg))); + return Ok(if update.prefix { new_reg } else { old_reg }); + } } let uv_idx = ctx.current_upvalue_captures.iter().position(|u| u.name == name); // 循环 update 段:被捕获绑定走寄存器 INC/DEC——C 风格 for 的 let/const diff --git a/crates/oxide_emit/src/function_body.rs b/crates/oxide_emit/src/function_body.rs index 43c1ca9b..174a5bdf 100644 --- a/crates/oxide_emit/src/function_body.rs +++ b/crates/oxide_emit/src/function_body.rs @@ -826,6 +826,22 @@ impl Emitter { }) .map(|u| u.name.clone()) .collect(); + // 捕获函数名不可写信息快照:与 const 快照同口径,供子函数写路径 + // non_writable 守卫(sloppy 静默 no-op / strict 抛 TypeError)使用。 + ctx.upvalue_non_writable_flags = ctx + .current_upvalue_captures + .iter() + .filter(|u| u.parent_uv_idx.is_none()) + .filter(|u| { + parent_ctx + .scopes + .symbols + .lookup_any_binding(u.name.as_str()) + .map(|(b, _)| b.non_writable) + .unwrap_or(false) + }) + .map(|u| u.name.clone()) + .collect(); // 类字段 computed key 数组等合成捕获:直接追加 upvalue(cell_idx 由父分配)。 for (name, cell_idx) in extra_upvalue_names { if !ctx.own_bindings.contains(*name) && !ctx.current_upvalue_captures.iter().any(|u| u.name == *name) { diff --git a/crates/oxide_vm/tests/module_namespace_tests.rs b/crates/oxide_vm/tests/module_namespace_tests.rs index 40431a58..5de92acf 100644 --- a/crates/oxide_vm/tests/module_namespace_tests.rs +++ b/crates/oxide_vm/tests/module_namespace_tests.rs @@ -391,8 +391,9 @@ fn cross_module_aliased_imports_read_live_value() { } /// 默认导出具名函数体的自引用重赋:函数名 `fn` 在函数作用域登记为不可写 -/// 绑定(15.2.10.1 步 m),遮蔽模块级同名绑定;体内容器 `fn = 2` 落在函数 -/// 作用域名绑定,模块级绑定不被改写,导入方读 default 仍得函数。 +/// 绑定(15.2.10.1 步 m),遮蔽模块级同名绑定;模块恒为严格模式,体内容器 +/// `fn = 2` 对不可写绑定抛 TypeError,模块级绑定不被改写,导入方读 default +/// 仍得函数。 #[test] fn cross_module_default_export_reads_live_value() { let cwd = std::env::current_dir().expect("cwd"); @@ -402,12 +403,16 @@ fn cross_module_default_export_reads_live_value() { std::fs::write( dir.join("main.mjs"), "import val from './dep.mjs';\n \ - const ret = val();\n \ - globalThis.__ns = [ret, typeof val].join('|');", + let threw = false;\n \ + try { val(); } catch (e) { threw = e instanceof TypeError; }\n \ + globalThis.__ns = [threw, typeof val].join('|');", ) .expect("write main"); let _cleanup = Cleanup(dir.clone()); let result = run_namespace_module(&dir); - assert_eq!(result, "1|function", "函数名不可写绑定遮蔽模块级绑定,自引用重赋静默失败"); + assert_eq!( + result, "true|function", + "严格模块内函数名不可写绑定自引用重赋抛 TypeError,模块级绑定不被改写" + ); } diff --git a/crates/oxide_vm/tests/switch_case_fn_tests.rs b/crates/oxide_vm/tests/switch_case_fn_tests.rs index 6937ee73..20f82e04 100644 --- a/crates/oxide_vm/tests/switch_case_fn_tests.rs +++ b/crates/oxide_vm/tests/switch_case_fn_tests.rs @@ -132,12 +132,13 @@ fn switch_case_function_captured_by_outer() { #[test] fn switch_case_function_binding_independent_of_outer_var() { - // 块绑定可变且与外层 var 独立:块内对 f 重新赋值不改写外层 var 绑定。 + // 函数名不可写绑定(15.2.10.1 步 m):体内容器对 f 重新赋值静默失败(sloppy), + // 块内 f 仍为函数对象;外层 var 绑定独立,不被改写。 let src = "var r1, r2; (function(){ var initialBV, currentBV; \ switch(1){case 1: function f(){ initialBV=f; f=123; currentBV=f; return 'decl'; }} \ var varBinding=f; f(); r1 = initialBV() === 'decl'; \ - r2 = currentBV === 123 && varBinding() === 'decl'; }()); r1 && r2"; - assert!(eval(src).unwrap().as_bool(), "块绑定应独立于外层 var 绑定"); + r2 = currentBV === f && varBinding() === 'decl'; }()); r1 && r2"; + assert!(eval(src).unwrap().as_bool(), "函数名不可写绑定自引用重赋静默失败,外层 var 绑定独立"); } #[test]