Next wall of the pi coding-agent bring-up (tracker #6564), directly behind #6593 (PR #6603 makes the bundle compile+link; this bug stops the binary at init).
Symptom
Every pi-native invocation dies during module init:
TypeError: value is not a function
at <anonymous>
Diagnosis (from a --debug-symbols build, breakpoint on js_throw_type_error_not_a_function)
js_throw_type_error_not_a_function
throw_not_callable
js_closure_unbox_callee_checked
pi_bundle_mjs___Comparator_constructor + 1112 ← bundle line 4836: options = parseOptions(options)
abi_trampoline / call_vtable_method
js_new_function_construct ← dynamic `new Comparator(...)`
semver/ranges/subset.js module init
semver/index.js → main
The failing shape is semver's standard CJS layout inside its esbuild __commonJS wrapper:
var Comparator = class _Comparator {
constructor(comp, options) {
options = parseOptions(options); // ← throws: parseOptions is undefined
...
}
};
module.exports = Comparator;
var parseOptions = require_parse_options(); // assigned AFTER the class definition
Correct JS: the ctor closes over the live parseOptions binding; by the time anything constructs a Comparator (the wrapper has completed), it holds the function.
Perry: the class captures the wrapper-local via the synthesized __perry_cap_<id> machinery. For dynamic construction (js_new_function_construct — subset.js constructs the require_comparator() VALUE), the ctor's capture params are filled from a capture snapshot taken at the class's source position — which predates the var parseOptions = ... assignment, so the snapshot holds undefined forever.
The #6037/#6052 refresh machinery (re-register snapshot after each captured var's assignment + before returns, insert_class_capture_refresh_after_assignments in crates/perry-hir/src/lower_decl/block.rs ~1250) only scans ast::Stmt::Decl(ast::Decl::Class(..)) — class declarations. var C = class _C {...} (a class expression in a var initializer — semver uses this in every class file, and the per-evaluation __perry_ctor_caps value-snapshot path applies) is not covered, so no refresh ever happens for this shape.
Suggested direction
Extend the refresh machinery to the var/let/const <name> = class ... statement shape (both the block-body and lower_fn_expr twins), or refresh the class value's __perry_ctor_caps on assignment to any captured var. Note the naming subtlety: the class registers under its expression ident (_Comparator), not the binding (Comparator), and scope renames must be resolved the same way the decl-path fix does (2026-07-02 audit P0 in block.rs).
Repro: compile secret-tests/pi-target/dist/pi-bundle.mjs with PR #6603's compiler, run HOME=$(mktemp -d) ./pi-native --help.
Next wall of the pi coding-agent bring-up (tracker #6564), directly behind #6593 (PR #6603 makes the bundle compile+link; this bug stops the binary at init).
Symptom
Every
pi-nativeinvocation dies during module init:Diagnosis (from a
--debug-symbolsbuild, breakpoint onjs_throw_type_error_not_a_function)The failing shape is semver's standard CJS layout inside its esbuild
__commonJSwrapper:Correct JS: the ctor closes over the live
parseOptionsbinding; by the time anything constructs a Comparator (the wrapper has completed), it holds the function.Perry: the class captures the wrapper-local via the synthesized
__perry_cap_<id>machinery. For dynamic construction (js_new_function_construct— subset.js constructs therequire_comparator()VALUE), the ctor's capture params are filled from a capture snapshot taken at the class's source position — which predates thevar parseOptions = ...assignment, so the snapshot holds undefined forever.The #6037/#6052 refresh machinery (re-register snapshot after each captured var's assignment + before returns,
insert_class_capture_refresh_after_assignmentsincrates/perry-hir/src/lower_decl/block.rs~1250) only scansast::Stmt::Decl(ast::Decl::Class(..))— class declarations.var C = class _C {...}(a class expression in a var initializer — semver uses this in every class file, and the per-evaluation__perry_ctor_capsvalue-snapshot path applies) is not covered, so no refresh ever happens for this shape.Suggested direction
Extend the refresh machinery to the
var/let/const <name> = class ...statement shape (both the block-body andlower_fn_exprtwins), or refresh the class value's__perry_ctor_capson assignment to any captured var. Note the naming subtlety: the class registers under its expression ident (_Comparator), not the binding (Comparator), and scope renames must be resolved the same way the decl-path fix does (2026-07-02 audit P0 in block.rs).Repro: compile
secret-tests/pi-target/dist/pi-bundle.mjswith PR #6603's compiler, runHOME=$(mktemp -d) ./pi-native --help.