mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: C 风格 for 词法头建立独立每迭代环境
This commit is contained in:
@@ -130,6 +130,11 @@ pub struct CompileCtx {
|
||||
/// let/const 循环变量每迭代新分配一个 cell,update 写寄存器(不污染本迭代
|
||||
/// 闭包捕获的 cell),下一迭代把这个寄存器值拷入新分配的 cell。
|
||||
pub(crate) register_update_names: Vec<String>,
|
||||
/// 正在发射的 C 风格 for 头词法绑定名(含解构叶):这些头名在 init 中经覆盖
|
||||
/// cell 建 `MAKE_CELL`,但循环体/test/update 读的是绑定寄存器、每迭代 fresh
|
||||
/// 拷贝的源也是寄存器,故 `emit_bind_target` 对集合内名字在 cell 写之外补写
|
||||
/// 绑定寄存器。init 发射完毕后清空。
|
||||
pub(crate) for_head_store_registers: HashSet<String>,
|
||||
/// 模块编译上下文:当前模块命名空间对象寄存器(`__moduleObject` 返回值)。
|
||||
pub(crate) module_ns_reg: Option<u32>,
|
||||
/// 已求值依赖模块的命名空间对象寄存器(按 import/export source 字符串索引)。
|
||||
@@ -216,6 +221,7 @@ impl CompileCtx {
|
||||
with_stack: Vec::new(),
|
||||
open_try_handlers: Vec::new(),
|
||||
register_update_names: Vec::new(),
|
||||
for_head_store_registers: HashSet::new(),
|
||||
module_ns_reg: None,
|
||||
module_dep_ns_regs: HashMap::new(),
|
||||
module_self_import_specs: HashSet::new(),
|
||||
|
||||
@@ -89,6 +89,16 @@ impl Emitter {
|
||||
if let Some(&cell_idx) = ctx.captured_bindings.get(name) {
|
||||
let op = if fresh_cell { OpCode::MAKE_CELL_FRESH } else { OpCode::MAKE_CELL };
|
||||
ctx.inst(Inst::new(op, Operand::Reg(src_reg), Operand::Imm(cell_idx as u16), Operand::None));
|
||||
// C 风格 for 头绑定:cell 之外补写绑定寄存器——循环体/test/update 读寄存器,
|
||||
// 每迭代 fresh 拷贝的源也是寄存器(见 `CompileCtx::for_head_store_registers`)。
|
||||
if ctx.for_head_store_registers.contains(name) {
|
||||
ctx.inst(Inst::new(
|
||||
OpCode::STORE_VAR,
|
||||
Operand::Reg(target_reg),
|
||||
Operand::Reg(src_reg),
|
||||
Operand::Imm(0),
|
||||
));
|
||||
}
|
||||
} else if !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name) {
|
||||
// with 内 var 初始化:对象有该属性则写对象,否则写提升槽(动态解析)。
|
||||
// ponytail: with 内顶层 var 不写全局对象属性,with 语句本身已是稀见用例。
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
//! for 语句 emit:`emit_for_statement` 含 init/test/update 三段与循环跳转。
|
||||
//!
|
||||
//! C 风格 for 的词法头按规范建模为独立环境:init 求值前头名绑定未初始化 cell
|
||||
//! (直读与捕获均抛 ReferenceError),被闭包引用或与顶层 var/函数同名的头名保留
|
||||
//! 独立 cell 贯穿循环,其余 init 后撤出覆盖回退寄存器循环。
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::for_in::ForHeadEnv;
|
||||
use crate::{CompileCtx, Emitter};
|
||||
use oxide_bytecode::module::Constant;
|
||||
use oxide_bytecode::opcode::OpCode;
|
||||
@@ -42,13 +49,32 @@ impl Emitter {
|
||||
let end_label = ctx.next_label_id();
|
||||
ctx.push_loop(end_label, update_label, crate::emit_ctx::LoopKind::Plain);
|
||||
let n_labeled = ctx.take_pending_loop_labels(end_label, update_label);
|
||||
// 循环头声明中被嵌套函数捕获的 let/const 绑定:每迭代 fresh cell。
|
||||
let mut fresh_bindings: Vec<(String, u8)> = Vec::new();
|
||||
// 循环头 let/const 声明名:update 段是 per-iteration 可变绑定(规范
|
||||
// §14.7.4.4 CreatePerIterationEnvironment 用 CreateMutableBinding)——
|
||||
// 规范允许 update 段写 let/const 循环变量,每迭代新建一个可变绑定;
|
||||
// 编译期 const 写检查对 update 段豁免,故记录全部声明名(含未被捕获者)。
|
||||
let mut update_names: Vec<String> = Vec::new();
|
||||
// 词法头独立环境:init 求值前把头名全部覆盖为未初始化 cell,使 init 内的
|
||||
// 直读与该区创建闭包的捕获命中 TDZ cell(不穿透外层同名已初始化绑定)。
|
||||
// init 后仅「被闭包引用」或「与顶层 var/函数同名」的头名保留覆盖贯穿循环
|
||||
// (体/测试/update 走 cell),其余撤出覆盖回退寄存器循环——普通 for-let
|
||||
// 不付每迭代 cell 代价。循环收尾恢复捕获映射,兄弟语句仍见外层同名绑定的
|
||||
// 原 cell。
|
||||
let mut head_keep: HashSet<String> = HashSet::new();
|
||||
let mut head_names: Option<Vec<String>> = None;
|
||||
let mut head_env: Option<ForHeadEnv> = None;
|
||||
if let Some(init) = &fr.init {
|
||||
if let Some(names) = self.collect_for_init_lexical_names(init) {
|
||||
for name in &names {
|
||||
if ctx.captured_bindings.contains_key(name) || self.is_global_tier_name(ctx, name) {
|
||||
head_keep.insert(name.clone());
|
||||
}
|
||||
}
|
||||
head_env = Some(self.begin_for_head_env(names.clone(), ctx)?);
|
||||
ctx.for_head_store_registers = names.iter().cloned().collect();
|
||||
head_names = Some(names);
|
||||
}
|
||||
}
|
||||
if let Some(init) = &fr.init {
|
||||
if let Some(expr) = init.as_expression() {
|
||||
self.emit_expression(expr, ctx)?;
|
||||
@@ -68,26 +94,9 @@ impl Emitter {
|
||||
let is_const = matches!(decl.kind, VariableDeclarationKind::Const);
|
||||
if let Some(init_expr) = &d.init {
|
||||
let val_reg = self.emit_expression(init_expr, ctx)?;
|
||||
// 捕获头名的寄存器补写在 `emit_bind_target` 经
|
||||
// `for_head_store_registers` 完成(解构叶用各自的叶值,不用整头 RHS)。
|
||||
self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, false, ctx)?;
|
||||
// 被捕获绑定:fresh 从寄存器读当前值,须把 init 值写入寄存器槽。
|
||||
// emit_binding_pattern 对捕获绑定只建 cell,不写寄存器。
|
||||
let mut names = Vec::new();
|
||||
self.collect_pattern_binding_names(&d.id, &mut names);
|
||||
for name in &names {
|
||||
if ctx.captured_bindings.contains_key(name) {
|
||||
if let Some(reg) = ctx.scopes.symbols.lookup_any(name) {
|
||||
// 全局不可写内置的捕获写被声明路径拦截,同步写一并跳过。
|
||||
if !ctx.targets_readonly_builtin(name, reg) {
|
||||
ctx.inst(Inst::new(
|
||||
OpCode::STORE_VAR,
|
||||
Operand::Reg(reg),
|
||||
Operand::Reg(val_reg),
|
||||
Operand::None,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let BindingPattern::BindingIdentifier(bi) = &d.id {
|
||||
let idx = ctx.add_constant(Constant::Undefined);
|
||||
let tmp = ctx.alloc_reg();
|
||||
@@ -112,24 +121,49 @@ impl Emitter {
|
||||
Operand::None,
|
||||
));
|
||||
}
|
||||
// 词法头无初始化声明:覆盖 cell 须置为已初始化(undefined),
|
||||
// 否则后续 init 表达式内创建、捕获该头名的闭包读时误报 TDZ。
|
||||
if !matches!(decl.kind, VariableDeclarationKind::Var) {
|
||||
if let Some(&cell_idx) = ctx.captured_bindings.get(bi.name.as_str()) {
|
||||
ctx.inst(Inst::new(
|
||||
OpCode::MAKE_CELL,
|
||||
Operand::Reg(tmp),
|
||||
Operand::Imm(cell_idx as u16),
|
||||
Operand::None,
|
||||
));
|
||||
}
|
||||
}
|
||||
ctx.init_var(bi.name.as_str());
|
||||
}
|
||||
// 记录 let/const 循环头声明名(update 段写豁免所需)与被捕获的绑定
|
||||
// (解构 pattern 递归收集绑定名,被捕获者每迭代 fresh cell)。
|
||||
// 记录 let/const 循环头声明名(update 段写豁免所需);fresh cell 判定
|
||||
// 在 init 后按保留集统一重算(见下方)。
|
||||
if !matches!(decl.kind, VariableDeclarationKind::Var) {
|
||||
let mut names = Vec::new();
|
||||
self.collect_pattern_binding_names(&d.id, &mut names);
|
||||
for name in &names {
|
||||
update_names.push(name.clone());
|
||||
if let Some(&cell_idx) = ctx.captured_bindings.get(name) {
|
||||
fresh_bindings.push((name.clone(), cell_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.pending_for_head_names = saved_pending;
|
||||
}
|
||||
}
|
||||
ctx.for_head_store_registers.clear();
|
||||
// 撤出非保留头名的覆盖:普通 for-let 回退寄存器循环,只有被闭包引用或与顶层
|
||||
// var/函数同名的头名保留独立 cell 贯穿循环。
|
||||
for name in head_names.iter().flatten() {
|
||||
if !head_keep.contains(name) {
|
||||
ctx.captured_bindings.remove(name);
|
||||
}
|
||||
}
|
||||
// 每迭代 fresh cell 的绑定:保留覆盖者(仍含 cell 下标);已撤出覆盖的头名
|
||||
// 走寄存器,不含在内。
|
||||
let mut fresh_bindings: Vec<(String, u8)> = Vec::new();
|
||||
for name in &update_names {
|
||||
if let Some(&cell_idx) = ctx.captured_bindings.get(name) {
|
||||
fresh_bindings.push((name.clone(), cell_idx));
|
||||
}
|
||||
}
|
||||
ctx.labels.set_label_pos(start_label, ctx.insts.len());
|
||||
// 每迭代 fresh cell:被捕获的 let/const 循环变量在迭代开始拷贝当前值到新 cell,
|
||||
// 本迭代闭包捕获新 cell(规范 CreatePerIterationEnvironment)。
|
||||
@@ -159,6 +193,11 @@ impl Emitter {
|
||||
self.emit_expression(update, ctx)?;
|
||||
ctx.register_update_names = prev;
|
||||
}
|
||||
// 恢复捕获映射:循环后的兄弟语句仍见外层同名绑定的原 cell(词法头环境
|
||||
// 作用是语句局部的)。
|
||||
if let Some(env) = head_env {
|
||||
self.restore_for_head_env(env, ctx);
|
||||
}
|
||||
ctx.inst(Inst::jmp(start_label));
|
||||
ctx.labels.set_label_pos(end_label, ctx.insts.len());
|
||||
ctx.pop_label_scopes(n_labeled);
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{CompileCtx, Emitter};
|
||||
use oxide_bytecode::opcode::OpCode;
|
||||
use oxide_ir::inst::Inst;
|
||||
use oxide_ir::operand::Operand;
|
||||
use oxide_parser::{ForStatementLeft, Statement, VariableDeclarationKind};
|
||||
use oxide_parser::{ForStatementInit, ForStatementLeft, Statement, VariableDeclarationKind};
|
||||
|
||||
/// for-in/for-of 词法头的捕获映射覆盖:每头名一条记录,依次为名字、旧条目
|
||||
/// (无旧条目为 None)、右值区 TDZ cell、体区 fresh cell(切换后填入)。
|
||||
@@ -22,6 +22,8 @@ pub(crate) struct ForHeadEnv {
|
||||
impl Emitter {
|
||||
/// 收集 for-in/for-of 词法声明头(let/const/using,含解构 pattern 叶)的
|
||||
/// 头名(排序保证稳定);var 头与赋值头无 TDZ 环境,返回 None。
|
||||
/// C 风格 for 的 init 布尔头同用 `ForHeadEnv`,其头名收集见
|
||||
/// [`Emitter::collect_for_init_lexical_names`]。
|
||||
pub(crate) fn collect_for_head_lexical_names(&self, left: &ForStatementLeft) -> Option<Vec<String>> {
|
||||
let ForStatementLeft::VariableDeclaration(decl) = left else {
|
||||
return None;
|
||||
@@ -38,6 +40,27 @@ impl Emitter {
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// 收集 C 风格 for 头词法声明(let/const,含解构 pattern 叶)的头名(排序
|
||||
/// 保证稳定);var 头与表达式头无 TDZ 环境,返回 None。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 无;纯 AST 收集。
|
||||
pub(crate) fn collect_for_init_lexical_names(&self, init: &ForStatementInit) -> Option<Vec<String>> {
|
||||
let ForStatementInit::VariableDeclaration(decl) = init else {
|
||||
return None;
|
||||
};
|
||||
if matches!(decl.kind, VariableDeclarationKind::Var) {
|
||||
return None;
|
||||
}
|
||||
let mut names = HashSet::new();
|
||||
for d in &decl.declarations {
|
||||
collect_binding_pattern_names(&d.id, &mut names);
|
||||
}
|
||||
let mut v: Vec<String> = names.into_iter().collect();
|
||||
v.sort();
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// 把词法头名覆盖到捕获映射(右值区阶段):保存旧条目、分配 TDZ cell 并
|
||||
/// 发射未初始化 MAKE_CELL、头名映射 TDZ cell。右值区内对头名的直读与
|
||||
/// 该区内创建闭包的捕获均命中未初始化 cell(读抛 ReferenceError)。
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//! C 风格 for 词法头独立环境钉:头名与同函数外层绑定同名时的 cell 覆盖碰撞、头名
|
||||
//! 与顶层 var/函数同名时的捕获剔除、头初始化表达式与无 init 声明的 TDZ 建模缺失
|
||||
//! 三面的引擎侧形态锁。
|
||||
//!
|
||||
//! 完成值一律收敛为布尔(JsValue 的 Display 只暴露 number/bool,不暴露字符串内容),
|
||||
//! 字符串比较在引擎内完成。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let program = match oxide_parser::parse(&allocator, source) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return format!("parse error: {}", e[0].message),
|
||||
};
|
||||
let module = match Compiler::new().compile(&program) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return format!("compile error: {e}"),
|
||||
};
|
||||
let mut vm = Vm::new();
|
||||
match vm.run(&Arc::new(module)) {
|
||||
Ok(result) => format!("{result}"),
|
||||
Err(e) => format!("vm error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── for 头 let/const 与同函数外层同名绑定的 cell 覆盖碰撞 ──
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_same_name_as_outer_let_keeps_outer_closure() {
|
||||
// 头名与外层 let 同名且外层被闭包捕获:头 MAKE_CELL 不得原地覆写外层 cell,
|
||||
// 头环境结束后外层闭包仍读外层值。
|
||||
assert_eq!(
|
||||
eval("let x = \"out\"; let p = () => x; for (let x = \"in\"; false;) {} p() === \"out\""),
|
||||
"true",
|
||||
"for-let head must not clobber the same-named outer captured cell"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_same_name_as_function_scope_let_keeps_outer_closure() {
|
||||
// 函数域同名外层 let 的孪生形:碰撞面不限于脚本顶层。
|
||||
assert_eq!(
|
||||
eval("function f(){ let x = \"out\"; let p = () => x; for (let x = \"in\"; false;) {} return p(); } f() === \"out\""),
|
||||
"true",
|
||||
"function-scope outer let closure keeps its cell across the for head"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_same_name_as_outer_var_keeps_outer_closure() {
|
||||
// 外层 var 同名:头绑定独立 cell,闭包捕获的外层 var 槽值不变。
|
||||
assert_eq!(
|
||||
eval(
|
||||
"function f(){ var x = \"out\"; let p = () => x; for (let x = \"in\"; false;) {} return p(); } f() === \"out\""
|
||||
),
|
||||
"true",
|
||||
"outer var closure keeps its value when a for-let head shadows it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_const_head_same_name_as_outer_let_keeps_outer_closure() {
|
||||
// const 头同形:不可变绑定的声明路径同样建独立 cell。
|
||||
assert_eq!(
|
||||
eval("let x = \"out\"; let p = () => x; for (const x = \"in\"; false;) {} p() === \"out\""),
|
||||
"true",
|
||||
"for-const head builds its own cell and leaves the outer captured cell intact"
|
||||
);
|
||||
}
|
||||
|
||||
// ── 四位置头名可见 + 循环后恢复外层环境(scope-head-lex-open/close)──
|
||||
|
||||
#[test]
|
||||
fn c_for_head_lex_env_visible_in_all_four_positions_then_restored() {
|
||||
// init/test/body/update 四位置的闭包都读头绑定('inside'),循环前后外层读
|
||||
// 仍读外层绑定('outside')——对应 scope-head-lex-open/close 两文件断言。
|
||||
assert_eq!(
|
||||
eval(
|
||||
"let x = 'outside'; var probeBefore = function(){ return x; }; \
|
||||
var probeDecl, probeTest, probeIncr, probeBody; var run = true; \
|
||||
for (let x = 'inside', _ = probeDecl = function(){ return x; }; \
|
||||
run && (probeTest = function(){ return x; }); \
|
||||
probeIncr = function(){ return x; }) \
|
||||
probeBody = function(){ return x; }, run = false; \
|
||||
probeBefore() === 'outside' && probeDecl() === 'inside' && probeTest() === 'inside' \
|
||||
&& probeBody() === 'inside' && probeIncr() === 'inside' && x === 'outside'"
|
||||
),
|
||||
"true",
|
||||
"head lexical environment is visible in init/test/body/update and removed after the loop"
|
||||
);
|
||||
}
|
||||
|
||||
// ── 头名与顶层 var/函数同名:顶层剔除后体/测试闭包须捕获头绑定 ──
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_same_name_as_top_var_body_closure_reads_head() {
|
||||
assert_eq!(
|
||||
eval("var x = \"top\"; var p; for (let x = 0; x < 2; x++) { p = () => x; } p() === 1"),
|
||||
"true",
|
||||
"body closure reads the per-iteration let head, not the same-named top-level var"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_same_name_as_top_var_test_closure_reads_head() {
|
||||
assert_eq!(
|
||||
eval("var x = \"top\"; var p; for (let x = 0; x < 1 && (p = () => x); x++) {} p() === 0"),
|
||||
"true",
|
||||
"test-position closure reads the let head binding, not the top-level var"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_same_name_as_top_function_reads_head() {
|
||||
assert_eq!(
|
||||
eval("function x(){}; var p; for (let x = 0; x < 2; x++) { p = () => x; } typeof p() === \"number\""),
|
||||
"true",
|
||||
"body closure reads the let head when it shadows a top-level function declaration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_nested_same_name_heads_capture_inner_head() {
|
||||
assert_eq!(
|
||||
eval(
|
||||
"var x = \"top\"; var p; for (let x = 0; x < 1; x++) { for (let x = 0; x < 2; x++) { p = () => x; } } p() === 1"
|
||||
),
|
||||
"true",
|
||||
"inner head shadows the outer head; inner closure reads the inner per-iteration binding"
|
||||
);
|
||||
}
|
||||
|
||||
// ── 头初始化表达式 / 无 init 声明的 TDZ 建模 ──
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_self_reference_throws_reference_error() {
|
||||
// 头自引用:TDZ 读抛 ReferenceError,不穿透外层同名已初始化绑定;无外层时
|
||||
// 同样抛 ReferenceError。
|
||||
assert_eq!(
|
||||
eval(
|
||||
"let a = 1; var r1 = 0; \
|
||||
try { for (let a = a; false;) {} } catch (e) { r1 = (e instanceof ReferenceError) ? 1 : 2; } \
|
||||
var r2 = 0; \
|
||||
try { for (let b = b; false;) {} } catch (e) { r2 = (e instanceof ReferenceError) ? 1 : 2; } \
|
||||
r1 === 1 && r2 === 1"
|
||||
),
|
||||
"true",
|
||||
"self-reference in the for head initializer reads the uninitialized head binding"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_second_declarator_self_reference_throws_reference_error() {
|
||||
assert_eq!(
|
||||
eval(
|
||||
"var r = 0; \
|
||||
try { for (let x = 1, y = y; false;) {} } catch (e) { r = (e instanceof ReferenceError) ? 1 : 2; } r === 1"
|
||||
),
|
||||
"true",
|
||||
"second declarator reading itself in the for head throws ReferenceError"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_without_init_initializes_undefined_for_closure() {
|
||||
// 无 init 的头声明:头绑定已初始化为 undefined,后续 init 内创建、捕获头名的
|
||||
// 闭包读 undefined(不得误报 TDZ)。
|
||||
assert_eq!(
|
||||
eval("var p; for (let x, _ = (p = () => x); false;) {} p() === undefined"),
|
||||
"true",
|
||||
"a for-let head without initializer is initialized to undefined for its closures"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_self_reference_with_top_var_throws_reference_error() {
|
||||
assert_eq!(
|
||||
eval(
|
||||
"var x = \"top\"; var r = 0; \
|
||||
try { for (let x = x; false;) {} } catch (e) { r = (e instanceof ReferenceError) ? 1 : 2; } r === 1"
|
||||
),
|
||||
"true",
|
||||
"for head self-reference throws even when a same-named top-level var exists"
|
||||
);
|
||||
}
|
||||
|
||||
// ── 绿守卫:非捕获遮蔽与解构头 ──
|
||||
|
||||
#[test]
|
||||
fn c_for_let_head_non_captured_shadow_restores_outer_and_destructures() {
|
||||
// 非捕获头名回退寄存器循环:循环后外层 let 读回原值;解构头叶绑定寄存器
|
||||
// 写的是叶值(不是整头右值)。
|
||||
assert_eq!(
|
||||
eval(
|
||||
"let x = \"out\"; for (let x = \"in\"; false;) {} \
|
||||
let value; for (let [v] = [23]; ;) { value = v; break; } \
|
||||
x === \"out\" && value === 23"
|
||||
),
|
||||
"true",
|
||||
"non-captured shadow restores the outer binding and destructuring heads bind leaf values"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user