fix: eval 代码顶层函数声明撞不可写全局内置——sloppy 编译期首指令 TypeError、strict 函数声明不落全局

This commit is contained in:
2026-09-13 09:05:54 +00:00
parent a98d5251e1
commit 1fdb7aca2d
6 changed files with 252 additions and 2 deletions
Generated
+1
View File
@@ -1342,6 +1342,7 @@ dependencies = [
"oxide_builtins", "oxide_builtins",
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
"oxide_emit",
"oxide_ir", "oxide_ir",
"oxide_kernel", "oxide_kernel",
"oxide_log", "oxide_log",
+20
View File
@@ -1660,6 +1660,26 @@ impl Emitter {
ctx.is_eval_script = is_eval_script; ctx.is_eval_script = is_eval_script;
// 脚本顶层严格模式由源码 "use strict" directive 决定(嵌套函数经父 ctx 继承)。 // 脚本顶层严格模式由源码 "use strict" directive 决定(嵌套函数经父 ctx 继承)。
ctx.is_strict = program.has_use_strict_directive(); ctx.is_strict = program.has_use_strict_directive();
// eval 代码顶层函数声明撞不可写全局内置(三常量的全局绑定在任何符合规范的
// 实现中均不可配置,声明实例化无法建立全局绑定):规范在建立全局 var 绑定
// 之前抛 TypeError(step 8 的 abrupt 先于绑定实例化),故 throw 发为程序首
// 指令,其后预声明/序言/声明发射均不可达(寄存器保持良定义,运行期零开销)。
// 严格 eval 代码函数声明绑定 eval 自身 lexical 环境、不触全局、不抛,门禁
// 随 !is_strict 关闭,其写点抑制在声明发射处。
if ctx.is_eval_script && !ctx.is_strict {
for name in self.collect_top_level_function_names(&program.body) {
if NON_WRITABLE_GLOBAL_BUILTINS.contains(&name.as_str()) {
let _ = self.emit_throw_error(
"TypeError",
&format!("Cannot declare function '{name}': global property is not configurable"),
&mut ctx,
)?;
break;
}
}
}
self.predeclare_function_declarations(&program.body, &mut ctx); self.predeclare_function_declarations(&program.body, &mut ctx);
// 预注册 builtin 引用(先于任何临时寄存器),builtin 槽不进入临时寄存器池。 // 预注册 builtin 引用(先于任何临时寄存器),builtin 槽不进入临时寄存器池。
@@ -70,7 +70,9 @@ impl Emitter {
} }
// 脚本顶层(非块内)函数声明:同步写全局对象,使 globalThis 可反射函数名。 // 脚本顶层(非块内)函数声明:同步写全局对象,使 globalThis 可反射函数名。
// 块内函数声明是块级绑定,不得落全局对象(作用域隔离,块外不可见)。 // 块内函数声明是块级绑定,不得落全局对象(作用域隔离,块外不可见)。
if ctx.is_global_scope && ctx.scopes.symbols.scopes.len() == 1 { // 严格 eval 代码函数声明绑定 eval 自身 lexical 环境、不落全局对象:抑制 A
// 侧写(局部绑定读面未暴露),同时避免不可写全局内置在 0x98 上误抛 strict。
if ctx.is_global_scope && ctx.scopes.symbols.scopes.len() == 1 && !(ctx.is_eval_script && ctx.is_strict) {
self.emit_global_prop_write(&name, var_reg, ctx); self.emit_global_prop_write(&name, var_reg, ctx);
} }
Ok(None) Ok(None)
+1 -1
View File
@@ -490,7 +490,7 @@ fn eval_family_excluded(path: &str) -> Option<&'static str> {
let is_direct = path.contains("eval-code/direct/"); let is_direct = path.contains("eval-code/direct/");
let common = [ let common = [
"declare-arguments", // 直接 eval 的 arguments 语义族(档 3) "declare-arguments", // 直接 eval 的 arguments 语义族(档 3)
"non-definable", // 与既有不可配置全局属性冲突(DEFINE_GLOBAL_PROP 静默跳过,不抛) "non-definable-global-var", // CanDeclareGlobalVar(var 面)c:false 语义与全局描述符面裁决绑定
"this-value-func", // 调用者 this 传递(档 3) "this-value-func", // 调用者 this 传递(档 3)
"new.target", // new.target 语义(档 3) "new.target", // new.target 语义(档 3)
"strict-caller", // 严格调用者传播(档 3) "strict-caller", // 严格调用者传播(档 3)
+1
View File
@@ -32,6 +32,7 @@ num-traits = "0.2"
[dev-dependencies] [dev-dependencies]
proptest.workspace = true proptest.workspace = true
oxide_ir = { path = "../oxide_ir" } oxide_ir = { path = "../oxide_ir" }
oxide_emit = { path = "../oxide_emit" }
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
[[bench]] [[bench]]
@@ -0,0 +1,226 @@
//! 顶层函数声明撞不可写全局内置(undefined/NaN/Infinity):脚本面声明不更新绑定
//! (A 侧保留原常量,读/调/typeof 面收敛全局对象,用户名与模块面不受影响);
//! eval 代码面——sloppy 函数声明在建立全局绑定前抛 TypeError(程序首指令 abrupt,
//! 无部分创建,A 侧保留),strict 函数声明为局部绑定不抛、不落全局。
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_emit::module::{ModuleSourceLoader, ResolvedModule};
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<JsValue, String> {
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(&Arc::new(module))
}
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
match result {
Ok(_) => panic!("expected error containing '{expected}', got Ok"),
Err(e) => assert!(e.contains(expected), "expected error containing '{expected}', got: {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(&Arc::new(module)).expect("run");
vm.lookup_str(result).unwrap_or_else(|| format!("{result}"))
}
/// 同一 VM 续跑脚本读 A 侧并提取结果字符串(模块完成值非末语句值,局部
/// 绑定面经 global 属性旁路观测)。
fn script_str(vm: &mut Vm, source: &str) -> Option<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 r = vm.run(&Arc::new(module)).expect("run");
vm.lookup_str(r)
}
/// 无依赖模块加载器:本组测试模块源无 import,resolve 不被调用。
struct NoopLoader;
impl ModuleSourceLoader for NoopLoader {
fn resolve(
&mut self, _base_dir: &str, _specifier: &str, _attributes: &[(&str, &str)],
) -> Result<ResolvedModule, String> {
Err("test module has no imports".into())
}
}
// ── 脚本面:三常量函数声明静默 no-op,绑定不更新 ──
#[test]
fn script_fn_decl_three_constants_sloppy_typeof_keeps_original_type() {
assert_eq!(eval_str("function undefined(){} typeof undefined"), "undefined");
assert_eq!(eval_str("function NaN(){} typeof NaN"), "number");
assert_eq!(eval_str("function Infinity(){} typeof Infinity"), "number");
}
#[test]
fn script_fn_decl_three_constants_strict_no_throw_typeof_keeps_original_type() {
assert_eq!(eval_str("'use strict'; function undefined(){} typeof undefined"), "undefined");
assert_eq!(eval_str("'use strict'; function NaN(){} typeof NaN"), "number");
assert_eq!(eval_str("'use strict'; function Infinity(){} typeof Infinity"), "number");
}
#[test]
fn script_fn_decl_nan_infinity_self_equality_preserved() {
let r = eval("'use strict'; function NaN(){}; NaN !== NaN").unwrap();
assert!(r.is_bool() && r.as_bool(), "声明后 NaN 自反不等性应保留,实际 {r:?}");
let r = eval("'use strict'; function Infinity(){}; Infinity === Infinity").unwrap();
assert!(r.is_bool() && r.as_bool(), "声明后 Infinity 自相等性应保留,实际 {r:?}");
}
#[test]
fn script_fn_decl_global_descriptor_value_and_writability_preserved() {
// undefined:值保留原常量(=== undefined),writable 保持 false。
let r = eval(
"function undefined(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'undefined'); d.value === undefined && d.writable === false",
)
.unwrap();
assert!(r.is_bool() && r.as_bool(), "undefined 描述符应保留原常量值与不可写,实际 {r:?}");
// NaN:值保留原常量(自反不等),writable 保持 false。
let r = eval(
"function NaN(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'NaN'); d.value !== d.value && d.writable === false",
)
.unwrap();
assert!(r.is_bool() && r.as_bool(), "NaN 描述符应保留原常量值与不可写,实际 {r:?}");
// Infinity:值保留原常量,writable 保持 false。
let r = eval(
"function Infinity(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'Infinity'); d.value === Infinity && d.writable === false",
)
.unwrap();
assert!(r.is_bool() && r.as_bool(), "Infinity 描述符应保留原常量值与不可写,实际 {r:?}");
// 声明不改变描述符:三常量属性不可枚举,Object.keys 不新增名字。
let r = eval(
"function undefined(){} function NaN(){} function Infinity(){} \
Object.keys(globalThis).filter(function (k) { return k === 'undefined' || k === 'NaN' || k === 'Infinity'; }).length === 0",
)
.unwrap();
assert!(r.is_bool() && r.as_bool(), "声明不应新增可枚举全局名,实际 {r:?}");
}
#[test]
fn script_fn_decl_undefined_call_throws_type_error() {
// 裸调用经 A 侧原常量(不可调用)抛 TypeError,不进入声明函数体返回 1。
let result = eval("function undefined(){ return 1; } undefined()");
assert_err_contains(result, "TypeError");
}
#[test]
fn script_fn_decl_nested_capture_call_throws_type_error() {
// 嵌套捕获面不改变调用面:声明体内闭包不暴露,裸调用仍解析全局原常量
//(不可调用)抛 TypeError。
let result = eval("function undefined(){ const g = () => undefined; return g; } undefined()");
assert_err_contains(result, "TypeError");
}
#[test]
fn script_fn_decl_user_name_binding_still_works() {
assert_eq!(eval_str("function foo(){ return 42; } typeof foo"), "function");
let r = eval("function foo(){ return 42; } foo()").unwrap();
assert!(r.is_int() && r.as_int() == 42, "用户名函数声明应可调用,实际 {r:?}");
let r = eval("function foo(){ return 42; } globalThis.foo === foo").unwrap();
assert!(r.is_bool() && r.as_bool(), "用户名函数声明应同步 A 侧,实际 {r:?}");
}
#[test]
fn module_fn_decl_undefined_keeps_global_constant() {
// 模块内函数声明是模块环境绑定:不命中全局门禁,模块内 typeof 是函数本体。
// 模块完成值非末语句值,局部面经 global 属性旁路观测;全局对象 undefined
// 仍是原常量。
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse_module(
&allocator,
"function undefined(){ return 1; } globalThis.__probe = typeof undefined; 0",
)
.expect("parse module");
let module = Compiler::new()
.compile_module(&program, "test.mjs", &mut NoopLoader)
.expect("compile module");
let mut vm = Vm::new();
vm.run(&Arc::new(module)).expect("module run");
assert_eq!(
script_str(&mut vm, "globalThis.__probe"),
Some("function".to_string()),
"模块内 typeof undefined 应为模块环境绑定(function)"
);
assert_eq!(
script_str(&mut vm, "typeof globalThis.undefined"),
Some("undefined".to_string()),
"模块声明不应改变全局 undefined 常量"
);
}
// ── eval 代码面:sloppy 函数声明撞不可写全局绑定抛 TypeError ──
#[test]
fn eval_fn_decl_sloppy_throws_type_error_a_side_preserved() {
// 撞名 eval 整体抛 TypeError(首指令 abrupt,无 var 绑定副作用),抛后
// typeof undefined 仍是原常量(A 侧保留)。
let s = eval_str(
"var r = 'no-throw'; try { eval('function undefined(){}'); } \
catch (e) { r = e instanceof TypeError ? 'TypeError' : 'other'; } r + ':' + typeof undefined",
);
assert_eq!(s, "TypeError:undefined");
// generator 声明同属该门禁(同 AST 节点):同样抛 TypeError。
let s = eval_str(
"var r = 'no-throw'; try { eval('function* undefined(){}'); } \
catch (e) { r = e instanceof TypeError ? 'TypeError' : 'other'; } r",
);
assert_eq!(s, "TypeError");
}
#[test]
fn eval_fn_decl_with_variable_no_partial_binding_creation() {
// 带 var 形:throw 先于 GDI 序言到达,shouldNotBeDefined 无部分创建。
let s = eval_str(
"var r = 'no-throw'; try { eval('var shouldNotBeDefined; function NaN(){}'); } \
catch (e) { r = e instanceof TypeError ? 'TypeError' : 'other'; } r + ':' + \
(Object.getOwnPropertyDescriptor(globalThis, 'shouldNotBeDefined') === undefined ? 'absent' : 'created')",
);
assert_eq!(s, "TypeError:absent");
}
#[test]
fn eval_fn_decl_with_function_no_partial_binding_creation() {
// 带函数形:撞名抛错后另一声明的 A 侧写不可达,shouldNotBeDefined 不存在。
let s = eval_str(
"var r = 'no-throw'; try { eval('function shouldNotBeDefined(){} function NaN(){}'); } \
catch (e) { r = e instanceof TypeError ? 'TypeError' : 'other'; } r + ':' + \
(Object.getOwnPropertyDescriptor(globalThis, 'shouldNotBeDefined') === undefined ? 'absent' : 'created')",
);
assert_eq!(s, "TypeError:absent");
}
#[test]
fn eval_fn_decl_a_side_value_and_descriptor_preserved() {
// 抛后 A 侧保留:Infinity 仍为原常量(自相等),描述符不可写位不变。
let s = eval_str(
"try { eval('function Infinity(){}'); } catch (e) {} \
(Infinity === Infinity) + ':' + \
(Object.getOwnPropertyDescriptor(globalThis, 'Infinity').writable === false ? 'w-false' : 'w-true')",
);
assert_eq!(s, "true:w-false");
}
#[test]
fn eval_fn_decl_strict_no_throw_a_side_preserved() {
// strict eval 代码函数声明是局部绑定:不抛、不落全局对象,typeof 仍读全局
// 原常量。
let s = eval_str(
"var r = 'threw'; try { eval(\"'use strict'; function undefined(){}\"); \
r = 'no-throw'; } catch (e) { r = 'threw:' + e; } r + ':' + typeof undefined",
);
assert_eq!(s, "no-throw:undefined");
}