Skip to content

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

@proggeramlug

With #11556 applied, claude-code 2.1.112 starts (--help works) but -p "say hi" fails immediately:

prompt is not a function

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:

#0  js_throw_type_error_not_a_function
#1  js_native_call_method
#2  js_typed_feedback_native_call_method_by_id
#3  perry_fn_cli_2_1_112_js__aMA
#4  … cli_2_1_112_js closures …
#7  perry_runtime::promise::microtasks::pump_protected
#10 perry_runtime::promise::microtasks::run_microtasks
#11 main

So a typed-feedback native method call by id for the method prompt found a receiver that does not carry it, during microtask drain (hence no Uncaught prefix difference — it is thrown inside a promise job).

What prompt is in this bundle

Not a global. grep finds 48 object-literal method definitions of the form

{ …, async description(){ return … }, async prompt(){ return … }, get inputSchema(){ … }, … }

i.e. each tool descriptor carries its own async prompt(). There is no globalThis.prompt, no window.prompt, and no typeof prompt guard anywhere in the bundle, and perry defines no global prompt either. So the failing call is someToolDescriptor.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

PERRY_RUNTIME_DIR=$PWD/target/release \
  ./target/release/perry compile --no-auto-optimize --enable-wasm-runtime cli_2.1.112.js -o cc
HOME=/tmp/cc_home ./cc -p "say hi"      # prompt is not a function
HOME=/tmp/cc_home ./cc --help           # works

Verified on fix/11499-static-write-instance-accessor (main d57f5139f6 + that fix).

One thing I deliberately did NOT conclude

I read xmm0 at 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 on js_throw_type_error_not_a_function, which does not take the receiver in xmm0, 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 on js_native_call_method with the method name filtered to prompt and 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.

Activity

  1. proggeramlug commented on Sep 28, 2026

    @proggeramlug
    ContributorAuthor

    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_sym cc built from fix/11499-static-write-instance-accessor, SYMBOLIZED — 370 MB, 792,245 symbols. Use this one.
    /root/rig9831/cc11499 same build, stripped (314 MB) — for timing, not debugging
    /root/rig9831/ccMAIN_sym cc at main before #11556 (791,829 symbols) — dies earlier, with the #11499 TypeError
    /root/ccbuild/cli_2.1.112.js the pristine 2.1.112 bundle (14 MB)
    /root/ccbuild/cli_trace.js the same bundle with __t() trace hooks at four module-init sites
    /tmp/tt_home_9831, /tmp/tt_proj_9831 sandbox HOME + project dir (mk_sandbox.sh 9831 recreates them)

    Always run with HOME=/tmp/tt_home_9831 from /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 (errorCode and ToolError are both 9 chars) — compare the first four bytes, e.g. *(unsigned int*)$rsi == 0x6f727265 for "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=1 is 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.sh run against /root/ccbuild: it does rm -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.
  2. proggeramlug commented on Sep 28, 2026

    @proggeramlug
    ContributorAuthor

    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 xmm0 at js_throw_type_error_not_a_function, which does not take the receiver in xmm0, so that dump may be a leftover register. Do not build on it. The clean way is to break one frame earlier, on js_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

  3. added 2 commits that reference this issue on Sep 28, 2026
  4. proggeramlug commented on Sep 28, 2026

    @proggeramlug
    ContributorAuthor

    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-runtime runs -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 / --help both exit 0.

  5. proggeramlug commented on Sep 28, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions