From 2bc571e9267cb7f8e35ffaebec057a2b03d6b1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 18 Jul 2026 22:16:45 +0200 Subject: [PATCH] fix(hir): extend #6037/#6052 class-capture refresh to class expressions (#6604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A class EXPRESSION capturing an enclosing-function var assigned AFTER the class in source order — semver's shape in every bundled class file: var Comparator = class _Comparator { constructor(comp, options) { options = parseOptions(options); ... } }; module.exports = Comparator; var parseOptions = require_parse_options(); // AFTER the class replayed a stale declaration-time capture snapshot on DYNAMIC construction of the escaped class value: the end-of-body refresh machinery (#6037/#6052) scanned only `ast::Decl::Class` DECLARATION statements in both body twins (`lower_fn_body_block_stmt` and `lower_fn_expr`), so `var C = class ...` never got refresh statements. The captured var stayed `undefined` in the name-keyed CLASS_CAPTURE_VALUES snapshot forever, the per-evaluation `__perry_ctor_caps` array had snapshotted the same pre-assignment `undefined` (TDZ-suppressed, #6523), and the ctor prologue's param-or-snapshot rebind (#5437) found `undefined` on both sides → "TypeError: value is not a function" at pi-native init (wall #2 of the pi bring-up, #6564, directly behind #6593). Fix: `lower_class_expr` records every capturing class expression lowered in a function body — `(resolved_registration_name, captured_ids)`, sidestepping the expression-ident/binding-name and rename subtleties by recording the name AFTER dedup/rename resolution — in a new body-scoped `ctx.body_class_expr_captures` list. Both body twins mark the list length at entry and drain their own suffix into the existing re_regs/re_reg_capsets machinery, so class expressions get the same refresh-after-assignment, refresh-before-return, and end-of-body re-registration as declarations. The runtime construct replay then self-heals through the existing ctor-prologue param-or-snapshot rebind: the stale per-eval `undefined` cap param falls back to the now-live name-keyed snapshot. No codegen/runtime changes. Scoping: entries' LocalIds are only meaningful in their own function's numbering, so every body path drains/truncates back to its entry mark: the twins drain; arrows truncate on exit (mark at scope entry, covering expression bodies); the block twin truncates on its error path; and `get_param_default` self-truncates so a class expression in a default-param value — lowered BEFORE the callee body's twin takes its mark at fn-decl / ctor / method param sites — can never be drained by the wrong (enclosing) body. No `append_new_args_stmt` pass runs for expressions — their construct sites are either static (live locals appended at the site) or dynamic (replayed through the snapshot). Known limitation (noted in the PR): a MULTI-evaluation factory whose class captures a var assigned after the class still reads the LAST evaluation's refreshed snapshot for slots that were undefined at its own evaluation — same last-writer-wins semantics the name-keyed store always had (#5437/#685); pre-fix those slots were `undefined`, so this strictly improves the broken case. Per-evaluation isolation of the normal assigned-before-class shape is unchanged (per-eval `__perry_ctor_caps` values win in the param-first rebind). Tests: tests/test_class_expr_capture_refresh_6604.sh (var/let/const named + anonymous class expressions, argument-position, the esbuild __commonJS semver comparator.js layout, captured-var reassignment, multi-eval isolation) and node-suite parity fixture test-parity/node-suite/object/class-expr-capture-refresh.ts — all node-identical; failing (TypeError) before this fix. Fixes #6604 Claude-Session: https://claude.ai/code/session_01JuiiePQfrXhAFD9fuCygB9 --- crates/perry-hir/src/lower/context.rs | 1 + crates/perry-hir/src/lower/expr_function.rs | 42 +++++ .../src/lower/lower_expr/arm_class.rs | 17 ++ .../perry-hir/src/lower/lowering_context.rs | 17 ++ crates/perry-hir/src/lower_decl/block.rs | 30 +++ crates/perry-hir/src/lower_patterns.rs | 12 ++ .../object/class-expr-capture-refresh.ts | 155 ++++++++++++++++ tests/test_class_expr_capture_refresh_6604.sh | 173 ++++++++++++++++++ 8 files changed, 447 insertions(+) create mode 100644 test-parity/node-suite/object/class-expr-capture-refresh.ts create mode 100755 tests/test_class_expr_capture_refresh_6604.sh diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index 443c5e4aff..702bc587b5 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -169,6 +169,7 @@ impl LoweringContext { next_anon_shape_id: 0, class_method_return_types: Vec::new(), class_captures: Vec::new(), + body_class_expr_captures: Vec::new(), let_class_aliases: Vec::new(), global_this_aliases: HashSet::new(), prototype_aliases: HashMap::new(), diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index ef8d0732fc..842ba9dfc7 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -193,6 +193,10 @@ pub(super) fn lower_arrow(ctx: &mut LoweringContext, arrow: &ast::ArrowExpr) -> // #4101: retain source text for `fn.toString()`. capture_function_source(ctx, func_id, &arrow.span, arrow.is_async); let scope_mark = ctx.enter_scope(); + // #6604: truncate mark for capturing class expressions recorded while + // lowering THIS arrow — placed at scope entry so default-param + // expressions are covered too; see the truncate below the body match. + let body_class_expr_captures_mark = ctx.body_class_expr_captures.len(); let strict = ctx.current_strict_mode() || match &*arrow.body { ast::BlockStmtOrExpr::BlockStmt(block) => { @@ -377,6 +381,17 @@ pub(super) fn lower_arrow(ctx: &mut LoweringContext, arrow: &ast::ArrowExpr) -> vec![Stmt::Return(Some(return_expr))] } }; + // #6604: a capturing class expression in an EXPRESSION-bodied arrow + // (`x => new (class { … })(x)`) records a body-class-expr entry that no + // body twin will drain (the block-bodied arm drains its own inside + // `lower_fn_body_block_stmt`; default-param entries are self-truncated by + // `get_param_default`). Truncate on exit so the entry — whose ids are + // only meaningful in the arrow's own local numbering — never leaks into + // the ENCLOSING body's refresh statements. Nothing is lost: a + // single-expression body has no later statements that could reassign the + // class's captured locals. + ctx.body_class_expr_captures + .truncate(body_class_expr_captures_mark); ctx.current_strict = outer_strict; // Prepend destructuring statements to body @@ -579,6 +594,13 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul fn_expr.function.is_async, ); let scope_mark = ctx.enter_scope(); + // #6604: capturing class EXPRESSIONS lowered in THIS function register + // from here for the end-of-body refresh (twin of + // `lower_fn_body_block_stmt`); the mark sits at scope entry so nothing + // recorded for this function can leak into the enclosing body. + // (Default-param entries never reach the drain — `get_param_default` + // self-truncates.) + let body_class_expr_captures_mark = ctx.body_class_expr_captures.len(); // A plain function has its own `arguments` object, so a direct `eval` // inside its body may reference `arguments` even when the function sits // in a class field initializer. Cleared here, restored at the end. @@ -1221,6 +1243,26 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul } } } + // #6604: capturing class EXPRESSIONS lowered directly in this body — + // the semver/esbuild `__commonJS` wrapper shape `var Comparator = + // class _Comparator { … }; …; var parseOptions = require_…()` — join + // the same refresh machinery as class declarations, so the snapshot + // tracks captured vars assigned AFTER the class. Recorded by + // `lower_class_expr` under the RESOLVED registration name; see the + // block-body twin (`lower_fn_body_block_stmt`) for why no + // `append_new_args_stmt` pass runs for expressions. + for (cname, ids) in ctx + .body_class_expr_captures + .split_off(body_class_expr_captures_mark) + { + let captures: Vec = ids.iter().map(|id| Expr::LocalGet(*id)).collect(); + let re_reg = Stmt::Expr(Expr::RegisterClassCaptures { + class_name: cname, + captures, + }); + re_reg_capsets.push((re_reg.clone(), ids.iter().copied().collect())); + re_regs.push(re_reg); + } if !re_regs.is_empty() { // Audit P0-B twin of the block-body path: refresh after every // same-body assignment to a captured local so mid-body constructs diff --git a/crates/perry-hir/src/lower/lower_expr/arm_class.rs b/crates/perry-hir/src/lower/lower_expr/arm_class.rs index 623a231a4a..2b8ed8bd1f 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_class.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_class.rs @@ -186,6 +186,23 @@ pub(crate) fn lower_class_expr( // expressions inside a function body (factories like effect's // `make()`), which produce a distinct class object per call. let at_module_top = ctx.scope_depth == 0 && ctx.inside_block_scope == 0; + // #6604: register this capturing class EXPRESSION with the enclosing + // body's end-of-body capture-refresh machinery (#6037/#6052), which + // previously scanned class DECLARATION statements only. Without the + // refresh, a captured var assigned AFTER the class expression (semver's + // `var Comparator = class _Comparator { … }; …; var parseOptions = + // require_parse_options()`) stays `undefined` in the decl-site snapshot, + // and dynamic construction of the escaped class value replays that stale + // snapshot. Recording the RESOLVED registration name here (post + // rename/dedup) sidesteps re-deriving it from the AST at body end. Module + // top is skipped — module-level ids are stripped from capture lists by + // `filter_module_level_captures`, so there is nothing to refresh. + if !at_module_top && !captured_args.is_empty() { + if let Some(ids) = ctx.lookup_class_captures(&synthetic_name) { + ctx.body_class_expr_captures + .push((synthetic_name.clone(), ids.to_vec())); + } + } if !at_module_top && (!named_statics.is_empty() || !static_symbol_registrations.is_empty() diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index ad12c9cd46..00d39c08fa 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -777,6 +777,23 @@ pub struct LoweringContext { /// here so the `Expr::New { class_name }` lowering can append /// `LocalGet(id)` for each captured id at every construction site. pub(crate) class_captures: Vec<(String, Vec)>, + /// #6604: capturing class EXPRESSIONS lowered while the CURRENT function + /// body is being lowered — `(registration_name, captured_outer_ids)`, + /// pushed by `lower_class_expr` (skipped at module top, where + /// `filter_module_level_captures` already strips module-level ids). The + /// #6037/#6052 end-of-body capture-refresh machinery previously scanned + /// only `ast::Decl::Class` DECLARATION statements, so `var Comparator = + /// class _Comparator { … }` (semver's shape in every bundled class file) + /// never got refresh statements: a captured var assigned AFTER the class + /// (`var parseOptions = require_parse_options()` at file bottom) stayed + /// `undefined` in the snapshot forever, and dynamic construction of the + /// escaped class value threw "value is not a function" at pi-native init. + /// Both body twins (`lower_fn_body_block_stmt` and `lower_fn_expr`) mark + /// this list's length at entry and drain their own suffix at body end; + /// every other body-lowering path must truncate back to its entry mark so + /// entries (whose ids are only meaningful in THEIR OWN function scope) + /// never leak into an enclosing body's refresh statements. + pub(crate) body_class_expr_captures: Vec<(String, Vec)>, /// Issue #740: `let_name → class_name` for `let/const/var = ` /// initializers. Lets `Expr::New { class_name }` (where `class_name` is /// the source-level identifier of an alias binding) resolve to the diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index fe14061a13..e72b1e1903 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -1126,6 +1126,10 @@ pub fn lower_fn_body_block_stmt( // Used by the Phase 1.6 forward `let`/`const` pre-registration so a const // that shadows an outer binding still gets a fresh this-body local. let body_entry_locals_len = ctx.locals.len(); + // #6604: entries pushed while lowering THIS body belong to THIS body's + // capture-refresh pass (their ids are this function's locals); drain the + // suffix at body end, truncate on the error path so nothing leaks upward. + let body_class_expr_captures_mark = ctx.body_class_expr_captures.len(); let hoisted_var_slots = predefine_var_bindings_in_function_body(ctx, block); // Phase 1: pre-define hoisted FnDecl locals so forward references in @@ -1224,6 +1228,8 @@ pub fn lower_fn_body_block_stmt( let mut body = match lower_block_stmt(ctx, block) { Ok(body) => body, Err(err) => { + ctx.body_class_expr_captures + .truncate(body_class_expr_captures_mark); ctx.current_strict = parent_strict; ctx.forward_class_names = saved_forward_class_names; ctx.forward_class_decl_depth = saved_forward_class_decl_depth; @@ -1291,6 +1297,30 @@ pub fn lower_fn_body_block_stmt( } } } + // #6604: capturing class EXPRESSIONS lowered directly in this body + // (`var Comparator = class _Comparator { … }`, argument-position + // `register(class { … })`, …) need the same assignment-tracking + // refresh as class declarations: semver assigns the captured + // `parseOptions`/`debug` vars AFTER the class, so the snapshot (and + // the per-evaluation `__perry_ctor_caps` array, whose stale-undefined + // slots the runtime construct path now backfills from this snapshot) + // must be re-registered with the live values. Entries were recorded + // by `lower_class_expr` under the RESOLVED registration name; no + // `append_new_args_stmt` pass — a class expression's construct sites + // are either static (binding-name `new C()`, live locals appended at + // the site) or dynamic (replayed through the snapshot). + for (cname, ids) in ctx + .body_class_expr_captures + .split_off(body_class_expr_captures_mark) + { + let captures: Vec = ids.iter().map(|id| Expr::LocalGet(*id)).collect(); + let re_reg = Stmt::Expr(Expr::RegisterClassCaptures { + class_name: cname, + captures, + }); + re_reg_capsets.push((re_reg.clone(), ids.iter().copied().collect())); + re_regs.push(re_reg); + } if !re_regs.is_empty() { // Audit P0-B: the decl-site snapshot is authoritative at // construct time, so keep it TRACKING same-body assignments — diff --git a/crates/perry-hir/src/lower_patterns.rs b/crates/perry-hir/src/lower_patterns.rs index 83422bcdc0..3d1a01de5c 100644 --- a/crates/perry-hir/src/lower_patterns.rs +++ b/crates/perry-hir/src/lower_patterns.rs @@ -1454,7 +1454,19 @@ pub(crate) fn get_param_default(ctx: &mut LoweringContext, pat: &ast::Pat) -> Re } } ast::Pat::Assign(assign) => { + // #6604: a capturing class EXPRESSION used as a default value + // (`function f(C = class { … }) {}`) must NOT register with the + // enclosing body's end-of-body capture-refresh machinery: param + // defaults are lowered BEFORE the callee's own body twin takes + // its list mark (fn-decl / ctor / method param sites), so the + // entry would be drained by the WRONG (enclosing) body and its + // ids interpreted in the wrong function's local numbering. + // Truncate whatever this default expression recorded — the + // default is re-evaluated at every call anyway, so its + // evaluation-time snapshot is per-call fresh. + let mark = ctx.body_class_expr_captures.len(); let default_expr = lower_expr(ctx, &assign.right)?; + ctx.body_class_expr_captures.truncate(mark); Ok(Some(default_expr)) } _ => Ok(None), diff --git a/test-parity/node-suite/object/class-expr-capture-refresh.ts b/test-parity/node-suite/object/class-expr-capture-refresh.ts new file mode 100644 index 0000000000..bf414e6687 --- /dev/null +++ b/test-parity/node-suite/object/class-expr-capture-refresh.ts @@ -0,0 +1,155 @@ +// #6604: a class EXPRESSION capturing an enclosing-function var that is +// assigned AFTER the class in source order (semver's ubiquitous +// `var Comparator = class _Comparator { … }; …; var parseOptions = +// require_parse_options()` CJS shape) must see the LIVE value when the class +// value escapes and is constructed DYNAMICALLY. The #6037/#6052 end-of-body +// capture-refresh machinery previously covered class DECLARATIONS only; the +// stale declaration-time snapshot made the captured var read `undefined` +// forever ("TypeError: value is not a function" at pi-native init). + +// Shape 1: var-assigned NAMED class expression. +var wrapVar = function () { + var C = class _C { + constructor(x: string) { + (this as any).v = helper(x); + } + }; + var out = { K: C }; + var helper = function (s: string) { + return "var:" + s; + }; + return out; +}; +console.log("named var:", new (wrapVar().K)("a").v); + +// Shape 2: let / const anonymous class expressions. +var wrapLet = function () { + let C = class { + constructor(x: string) { + (this as any).v = helperL(x); + } + }; + var box = { K: C }; + var helperL = function (s: string) { + return "let:" + s; + }; + return box; +}; +var wrapConst = function () { + const C = class { + constructor(x: string) { + (this as any).v = helperC(x); + } + }; + var box = { K: C }; + var helperC = function (s: string) { + return "const:" + s; + }; + return box; +}; +console.log("let:", new (wrapLet().K)("b").v); +console.log("const:", new (wrapConst().K)("c").v); + +// Shape 3: class expression in ARGUMENT position (no binding statement). +var registry: any = {}; +var register = function (name: string, cls: any) { + registry[name] = cls; +}; +var wrapArg = function () { + register( + "K", + class { + constructor(x: string) { + (this as any).v = helperA(x); + } + } + ); + var helperA = function (s: string) { + return "arg:" + s; + }; +}; +wrapArg(); +console.log("arg:", new registry.K("d").v); + +// Shape 4: the esbuild `__commonJS` semver comparator.js layout — class +// expression + `module.exports` + trailing requires, constructed dynamically +// from another wrapper at init time. +var __commonJS = (cb: any, mod: any = undefined) => + function __require() { + return ( + mod || (0, cb[Object.keys(cb)[0]])((mod = { exports: {} }).exports, mod), + mod.exports + ); + }; +var require_parse_options = __commonJS({ + "parse-options.js"(exports: any, module: any) { + "use strict"; + var emptyOpts = Object.freeze({}); + module.exports = (options: any) => + options && typeof options === "object" ? options : emptyOpts; + }, +}); +var require_comparator = __commonJS({ + "comparator.js"(exports: any, module: any) { + "use strict"; + var ANY = Symbol("SemVer ANY"); + var Comparator = class _Comparator { + static get ANY() { + return ANY; + } + value: string; + loose: boolean; + constructor(comp: any, options?: any) { + options = parseOptions(options); + if (comp instanceof _Comparator) { + comp = (comp as any).value; + } + this.loose = !!options.loose; + this.value = String(comp); + } + toString() { + return this.value; + } + }; + module.exports = Comparator; + var parseOptions = require_parse_options(); + }, +}); +var Comparator = require_comparator(); +var minimum = [new Comparator(">=0.0.0-0")]; +var c2 = new Comparator(">=1.2.3", { loose: true }); +console.log("semver:", String(minimum[0]), c2.value, c2.loose); + +// Refresh must keep tracking REASSIGNMENTS of the captured var, not just its +// first initialization. +var wrapReassign = function () { + var C = class { + constructor() { + (this as any).v = tag(); + } + }; + var out = { K: C }; + var tag = function () { + return "first"; + }; + tag = function () { + return "second"; + }; + return out; +}; +console.log("reassign:", new (wrapReassign().K)().v); + +// Per-evaluation isolation of the NORMAL (assigned-before-class) shape must +// be preserved: two factory calls, two classes, distinct captured values. +var mk = function (t: string) { + var prefix = "p" + t; + var C = class { + constructor() { + (this as any).v = t + ":" + prefix; + } + }; + return C; +}; +var A = mk("x"); +var B = mk("y"); +console.log("multi-eval:", new A().v, new B().v); diff --git a/tests/test_class_expr_capture_refresh_6604.sh b/tests/test_class_expr_capture_refresh_6604.sh new file mode 100755 index 0000000000..c4591c86c8 --- /dev/null +++ b/tests/test_class_expr_capture_refresh_6604.sh @@ -0,0 +1,173 @@ +#!/bin/bash +# Regression (#6604): a class EXPRESSION capturing an enclosing-function var +# assigned AFTER the class in source order — semver's shape in every bundled +# class file: +# +# var Comparator = class _Comparator { +# constructor(comp, options) { options = parseOptions(options); ... } +# }; +# module.exports = Comparator; +# var parseOptions = require_parse_options(); // assigned AFTER the class +# +# Correct JS: the ctor closes over the live binding; by the time anything +# constructs a Comparator the wrapper has completed and the binding holds the +# function. Pre-fix, the #6037/#6052 end-of-body capture-refresh machinery +# scanned only `ast::Decl::Class` DECLARATION statements, so no refresh was +# ever emitted for the expression shape: DYNAMIC construction of the escaped +# class value replayed the stale declaration-time snapshot (captured var = +# undefined forever) and threw "TypeError: value is not a function" at +# pi-native init (wall #2 of the pi coding-agent bring-up, behind #6593). +# +# Covers: var/let/const-assigned named and anonymous class expressions, +# argument-position class expressions, the esbuild `__commonJS` semver layout, +# captured-var reassignment tracking, and multi-evaluation factory isolation +# for the normal (assigned-before-class) shape. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PERRY="$SCRIPT_DIR/../target/release/perry" +[ ! -f "$PERRY" ] && PERRY="$SCRIPT_DIR/../target/debug/perry" +if [ ! -f "$PERRY" ]; then + echo "SKIP: perry binary not found (build with cargo build --release)" + exit 0 +fi +if ! command -v cc >/dev/null 2>&1; then + echo "SKIP: cc not available" + exit 0 +fi + +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +cat > "$TMPDIR/main.js" << 'EOF' +// Shape 1: var-assigned NAMED class expression, captured var assigned after. +var wrapVar = function () { + var C = class _C { + constructor(x) { this.v = helper(x); } + }; + var out = { K: C }; + var helper = function (s) { return "var:" + s; }; + return out; +}; +console.log(new (wrapVar().K)("a").v); + +// Shape 2: let / const anonymous class expressions. +var wrapLet = function () { + let C = class { + constructor(x) { this.v = helperL(x); } + }; + var box = { K: C }; + var helperL = function (s) { return "let:" + s; }; + return box; +}; +var wrapConst = function () { + const C = class { + constructor(x) { this.v = helperC(x); } + }; + var box = { K: C }; + var helperC = function (s) { return "const:" + s; }; + return box; +}; +console.log(new (wrapLet().K)("b").v); +console.log(new (wrapConst().K)("c").v); + +// Shape 3: class expression in ARGUMENT position (no binding statement). +var registry = {}; +var register = function (name, cls) { registry[name] = cls; }; +var wrapArg = function () { + register("K", class { + constructor(x) { this.v = helperA(x); } + }); + var helperA = function (s) { return "arg:" + s; }; +}; +wrapArg(); +console.log(new registry.K("d").v); + +// Shape 4: the esbuild __commonJS semver comparator.js layout. +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[Object.keys(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var require_parse_options = __commonJS({ + "parse-options.js"(exports, module) { + "use strict"; + var emptyOpts = Object.freeze({}); + module.exports = (options) => + options && typeof options === "object" ? options : emptyOpts; + }, +}); +var require_comparator = __commonJS({ + "comparator.js"(exports, module) { + "use strict"; + var ANY = Symbol("SemVer ANY"); + var Comparator = class _Comparator { + static get ANY() { return ANY; } + constructor(comp, options) { + options = parseOptions(options); + if (comp instanceof _Comparator) { comp = comp.value; } + this.loose = !!options.loose; + this.value = String(comp); + } + toString() { return this.value; } + }; + module.exports = Comparator; + var parseOptions = require_parse_options(); + }, +}); +var Comparator = require_comparator(); +var minimum = [new Comparator(">=0.0.0-0")]; +var c2 = new Comparator(">=1.2.3", { loose: true }); +console.log(String(minimum[0]), c2.value, c2.loose); + +// Refresh must track REASSIGNMENT of a captured var, not just its init. +var wrapReassign = function () { + var C = class { + constructor() { this.v = tag(); } + }; + var out = { K: C }; + var tag = function () { return "first"; }; + tag = function () { return "second"; }; + return out; +}; +console.log(new (wrapReassign().K)().v); + +// Per-evaluation isolation of the normal (assigned-before-class) shape. +var mk = function (t) { + var prefix = "p" + t; + var C = class { + constructor() { this.v = t + ":" + prefix; } + }; + return C; +}; +var A = mk("x"); +var B = mk("y"); +console.log(new A().v, new B().v); +EOF + +cd "$TMPDIR" +COMPILE_OUTPUT=$(PERRY_NO_AUTO_OPTIMIZE=1 "$PERRY" compile main.js -o test_bin --no-cache 2>&1) || { + echo "FAIL: compile error" + echo "$COMPILE_OUTPUT" | tail -20 + exit 1 +} + +RUN_OUTPUT=$(./test_bin 2>&1) +EXPECTED="var:a +let:b +const:c +arg:d +>=0.0.0-0 >=1.2.3 true +second +x:px y:py" + +if [ "$RUN_OUTPUT" = "$EXPECTED" ]; then + echo "PASS" + exit 0 +fi + +echo "FAIL: class-expression capture snapshot stale (vars assigned after the class)" +echo "Expected:" +echo "$EXPECTED" +echo "Got:" +echo "$RUN_OUTPUT" +exit 1