Repository navigation
cc -p fails with "prompt is not a function": a user-defined async method on an object literal is invisible to typed-feedback native dispatch (blocks #9949 rows) #11559
Description
Activity
Handover: ready-built assets on perrymaster (saves a ~2.6 h / 28 GB cc compile)
Everything needed to debug this without compiling cc yourself. Verified present today.
path what it is /root/rig9831/cc11499_symcc built from fix/11499-static-write-instance-accessor, SYMBOLIZED — 370 MB, 792,245 symbols. Use this one./root/rig9831/cc11499same build, stripped (314 MB) — for timing, not debugging /root/rig9831/ccMAIN_symcc at mainbefore #11556 (791,829 symbols) — dies earlier, with the #11499 TypeError/root/ccbuild/cli_2.1.112.jsthe pristine 2.1.112 bundle (14 MB) /root/ccbuild/cli_trace.jsthe same bundle with __t()trace hooks at four module-init sites/tmp/tt_home_9831,/tmp/tt_proj_9831sandbox HOME + project dir ( mk_sandbox.sh 9831recreates them)Always run with
HOME=/tmp/tt_home_9831from/tmp/tt_proj_9831. Never the real$HOME— it resets the live API key in~/.claude.json.The gdb recipe that cracked #11301 and #11499
BIN=/root/rig9831/cc11499_sym # 1. enumerate EVERY copy of the throw symbol — there are 2, and breaking on the # wrong one never fires and reads exactly like "the bug is somewhere else". nm $BIN | grep -oE '_RNv[A-Za-z0-9_]*throw_not_callable' | sort -u # 2. break on all of them, plus the C entry point # break *<mangled> <- the `*` matters: `break fn` stops AFTER the # prologue, with the arg registers clobbered # 3. print any NaN-boxed value with the runtime's own printer: # call (void) js_console_log_dynamic($xmm0.v2_double[0]) # it prints class names and live fields, e.g. `VR8 [Error]` # 4. a TUI needs a pty: script -qec "gdb -batch -x /root/g.gdb $BIN" /dev/null
Two more that cost me runs: gdb cannot take the address of a convenience variable, so to print a reconstructed box, poke it into a real memory slot first; and filtering a breakpoint on a key's length is not enough (
errorCodeandToolErrorare both 9 chars) — compare the first four bytes, e.g.*(unsigned int*)$rsi == 0x6f727265for"erro".Rebuilding, if you must
cd <your worktree> cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static -p perry-wasm-host \ --features perry-runtime/wasm-host cd /root/ccbuild # copy the bundle out if you want your own dir PERRY_HEAVY_NEED_GB=28 PERRY_HEAVY_MAX_WAIT=21600 /root/perry-heavy.sh \ env PERRY_RUNTIME_DIR=<wt>/target/release PERRY_WORKSPACE_ROOT=<wt> \ <wt>/target/release/perry compile --no-auto-optimize --enable-wasm-runtime cli_2.1.112.js -o <out>
PERRY_KEEP_SYMBOLS=1is not a cache key — a symbolized build is one ~3-8 min relink, not a recompile.- Build compiler and runtime from the same tree. The stamp guard compares version and commit-or-source-hash, and a dirty tree switches it to a source hash — so a cross-tree or cross-version relink is refused. Do not pass a short sha to
PERRY_BUILD_COMMIT. - Do not let
build-cc.shrun against/root/ccbuild: it doesrm -rf node_modules/.cache/perry, destroying the object cache that makes a relink minutes instead of ~46 min. - Disk is tight: 10 GB free right now. A cc compile needs headroom; the heavy wrapper waits for memory but not for disk.
The one open question, and the exact command to settle it
My write-up says the receiver "looks like the global object" and explicitly does not claim it. That matters more than anything else here, because the two possibilities lead to different fixes:
- if the receiver really is the global object → a receiver-resolution bug;
- if it is the tool descriptor → a method-lookup bug (a user-defined
async prompt()on an object literal invisible to typed-feedback dispatch).
I read
xmm0atjs_throw_type_error_not_a_function, which does not take the receiver inxmm0, so that dump may be a leftover register. Do not build on it. The clean way is to break one frame earlier, onjs_native_call_method, filter to the method name, and read its actual receiver argument:BIN=/root/rig9831/cc11499_sym nm $BIN | grep -w js_native_call_method # find the entry point # "prompt" is 6 bytes; first 4 bytes are "prom" == 0x6d6f7270 little-endian # break *js_native_call_method if <name-len reg> == 6 && *(unsigned int*)<name-ptr reg> == 0x6d6f7270 # then dump the receiver with the runtime printer: # call (void) js_console_log_dynamic(<receiver reg>.v2_double[0]) # call (int) js_object_get_class_id((void*)(<receiver bits> & 0x0000ffffffffffff))
Check
js_native_call_method's signature first to learn which register holds the receiver versus the name pointer/length — that is what I skipped, and it is why my read is unusable.Context worth having
- The JS-level frame name in the throw path was
perry_fn_cli_2_1_112_js__aMA. Treat minified frame names as unreliable until matched to a binding in scope at that site: on cc 2.1.112 compiled from main dies at startup: every subcommand but --version throws "value is not a function" #11301 the stack blamedOBY, which turned out to be a render loop, and anOqI trusted was the wrong one of three bindings with that name. - It throws inside a promise job (
pump_protected→run_microtasks→main), so it is reached during async startup, not from the CLI entry. - Not yet characterised for GC sensitivity.
PERRY_GEN_GC=0andPERRY_GC_FORCE_EVACUATE=1are cheap and were decisive on cc 2.1.112 compiled from main dies at startup: every subcommand but --version throws "value is not a function" #11301 (both identical there ⇒ not a lifetime bug). - Nine minimal reductions failed to reproduce cc 2.1.112 compiled from main dies at startup: every subcommand but --version throws "value is not a function" #11301/cc 2.1.112 on current main: compiles cleanly, then dies at startup with "Cannot set property errorCode of #<VR8> which has only a getter" #11499 because the obvious shapes lower statically and never reach the dynamic path. If a reduction here passes, suspect the reduction before the diagnosis, and force the dynamic path the way
test-files/test_gap_11499_class_object_static_write.tsdoes: hide identity behind a lazy initializer assigned to outervars, and call through a shared, IC-warmed site.
Fixed on main by #11610 (spill store past the buffer's high-water mark), with regression tests in #11614. Acceptance on main 63f6b2a (qb4): cc 2.1.112 compiled with
--no-auto-optimize --enable-wasm-runtimeruns-p "say hi"against a local mock Anthropic API. It exits 0, prints the mocked reply, and the mock logs a real /v1/messages turn.doctor(#11558, fixed by #11615) renders its full diagnostics with no crash, and--version/--helpboth exit 0.Verified fixed on main by #11610 (spill store read the uninitialized tail as the old value). Acceptance: cc 2.1.112 compiled from main be39bbf,
-p "say hi"passes 10/10 past the old failure point (plus a PERRY_OBJECT_SPILL=0 control, 3/3), with no "not a function" in any output. #11614 adds the regression tests.
With #11556 applied,
claude-code2.1.112 starts (--helpworks) but-p "say hi"fails immediately:This blocks every cc runtime measurement that needs a turn, including the four-turn tenuring rows for #9949 — those drive cc through
-p, so they cannot run until this is fixed.Caught in gdb on a symbolized build, breaking on every copy of the throw:
So a typed-feedback native method call by id for the method
promptfound a receiver that does not carry it, during microtask drain (hence noUncaughtprefix difference — it is thrown inside a promise job).What
promptis in this bundleNot a global.
grepfinds 48 object-literal method definitions of the formi.e. each tool descriptor carries its own
async prompt(). There is noglobalThis.prompt, nowindow.prompt, and notypeof promptguard anywhere in the bundle, and perry defines no globalprompteither. So the failing call issomeToolDescriptor.prompt(), and the defect is that a user-defined async method on a plain object literal is not found by the typed-feedback native-method dispatch.Reproduce
Verified on
fix/11499-static-write-instance-accessor(maind57f5139f6+ that fix).One thing I deliberately did NOT conclude
I read
xmm0at the breakpoint and the runtime's printer dumped what looks like the global object (PerformanceResourceTiming,crypto,navigator, a signal-exit emitter). I am not claiming that is the receiver. I broke onjs_throw_type_error_not_a_function, which does not take the receiver inxmm0, so that value may well be a leftover register — the same over-reading that produced a wrong first-bad attribution on #11301. The correct next step is to break onjs_native_call_methodwith the method name filtered topromptand read its actual receiver argument. I have not done that yet.If it does turn out to be the global object, this is a receiver-resolution bug rather than a missing-method bug, which would point somewhere quite different — worth establishing before anyone starts on a fix.
Related
Possibly the same family as #10893 (a static accessor reached through the class-id chain was invisible to the native-call tower) and #10848 (patching a built-in method), both of which were dispatch-tower gaps rather than missing implementations.