Skip to content

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

Description

@proggeramlug

On current main (f3828c7ec), claude-code 2.1.112 compiles cleanly but still dies at startup — with a different error from the one in #11301:

Uncaught (in promise) TypeError: Cannot set property errorCode of #<VR8> which has only a getter
    at OBY (<anonymous>)
    at L (<anonymous>)
    at new __AnonShape_6e5125e2965aca2f (<anonymous>)
    at L (<anonymous>)
    at new __AnonShape_0f21577b01597d8e (<anonymous>)
    at L (<anonymous>)
    at N8 [0x5d9bd60f333f] (<anonymous>)

--version works (2.1.112 (Claude Code)); --help and doctor both fail identically. This is the current blocker for any cc-based runtime measurement.

First: the reported compile failure does NOT reproduce

A parallel lane reported "ENTRY MODULE FAILED TO COMPILE — REFUSING TO LINK" for this bundle on a90cd9d44, and a separate IR dump found a function __44728 with undefined SSA values. Neither appears here. On f3828c7ec:

COMPILE_RC=0            (2 h 38 m wall)
Wrote executable: /root/rig9831/ccMAIN
Binary size: 313.4MB

The full 676-line log (kept unfiltered) contains no __44728, no verifier failure, and no "REFUSING TO LINK". Its only notices are the expected AOT-unsupported sites (3 Function(...), 1 eval(...), 1 import(...), process._rawDebug) plus one unknown identifier 'MessageEvent' warning. Built with compiler and runtime from the same tree (--no-auto-optimize --enable-wasm-runtime), run under an exclusive heavy-job lock with 28 GB of headroom reserved — the earlier failure was at 24.2 GB peak while other lanes compiled, so resource starvation is the likelier explanation for that report than a codegen defect.

The construct

XX = class XX extends Error {
  constructor(q, K) { super(q); this.errorUri = K, this.name = this.constructor.name }
  get errorCode() { return this.constructor.errorCode }   // INSTANCE accessor, getter-only
};
VR8 = class VR8 extends XX {};
VR8.errorCode = "invalid_request";        // a STATIC assignment on the class object
Sy6 = class Sy6 extends XX {}; Sy6.errorCode = "invalid_client";
// …a dozen more subclasses in the same shape

The getter is an instance accessor (it lives on XX.prototype). VR8.errorCode = … is a static set on the constructor. Perry reports that the class object has a getter-only errorCode, i.e. the instance accessor is visible on the static side, so the static assignment hits it and throws. In node the same code runs and hasOwnProperty is true on both the class and the prototype.

The new __AnonShape_* frames match the anonymous class … extends XX {} expressions, and L is esbuild's __esm lazy-init wrapper, so this runs during module initialisation.

Characterisation

Note for #11301 / #11469

The commits named there as fixing #11174, c025870294 and 256925ac46, are not ancestors of f3828c7ec — git merge-base --is-ancestor reports both absent from current main. Worth confirming where they actually live before treating that fix as landed.

Activity

  1. proggeramlug commented on Sep 27, 2026

    @proggeramlug
    ContributorAuthor

    Correction: the #11174 fixes ARE in this arm — my ancestry note above was wrong

    The closing note in the issue body said c025870294 and 256925ac46 are not ancestors of f3828c7ec. That is false. Re-checked from a freshly fetched clone:

    c025870294 IS an ancestor of origin/main and of f3828c7ec
    256925ac46 IS an ancestor of origin/main and of f3828c7ec
    

    They landed as #11226 / #11263 on 09-24/25. My check ran against a repo whose ref set was incomplete — that host's shared repo had been re-seeded from another clone, so merge-base answered against refs it did not have. Ancestry from a re-seeded or partially fetched repo is not evidence; that is my error, not a problem with the fix.

    This matters in the right direction: it explains the good news in this issue. The #11301 failure (value is not a function from an unregistered Math) is gone in this binary precisely because the #11174 fix is present — the no-auto HTTP rebuild no longer bundles a runtime built without perry-runtime's default features. So that diagnosis is corroborated here, and the errorCode accessor failure below is a genuinely separate, later bug rather than the same one wearing a new face.

  2. proggeramlug commented on Sep 27, 2026

    @proggeramlug
    ContributorAuthor

    Root cause: a STATIC write on a class constructor is resolved against the INSTANCE accessor chain

    Confirmed in the debugger on the symbolized build (ccMAIN_sym, f3828c7ec), breaking on class_chain_setter_apply with the key filtered to errorCode:

    class_id = 7515
    key      = errorCode
    receiver bits (xmm0) = 0x7ffd041fa5e0bf18
    --- runtime's own printer on the receiver ---
    VR8 [Error]
    --- js_object_get_class_id(receiver) ---
    $1 = 7515
    

    The receiver is the class constructor object (VR8 [Error]), not an instance — and it carries a non-zero class_id. Native path:

    class_chain_setter_apply
    set_field_by_name_object_tail
    proxy::target_set
    proxy::create_or_update_receiver_property
    proxy::ordinary_set_with_receiver
    js_put_value_set
    js_put_value_set_packed_miss
    … cli_2_1_112_js closures (module init)
    

    Why that is wrong

    In the bundle:

    XX  = class XX extends Error { get errorCode(){ return this.constructor.errorCode } };  // INSTANCE accessor
    VR8 = class VR8 extends XX {};
    VR8.errorCode = "invalid_request";        // STATIC set on the constructor object

    VR8.errorCode = … is an OrdinarySet on the constructor. The constructor's prototype chain is the static side — VR8 → XX → Error → Function.prototype — and there is no errorCode accessor anywhere on it. The getter lives on XX.prototype, which is not on the constructor's chain.

    set_field_by_name_object_tail (at object/field_set_by_name/tail.rs:384 in this tree) guards only on class_id != 0 before consulting class_chain_setter_apply, i.e. the instance accessor chain. Since the constructor object carries the class's class_id, a static write is resolved against instance accessors, finds the getter-only errorCode, and takes the Some(false) arm:

    let class_id = (*obj).class_id;
    if class_id != 0 {
        match class_chain_setter_apply(class_id, name, this_f64, value) {
            Some(true)  => return,
            Some(false) => throw "Cannot set property {name} of #<{class_name}> which has only a getter",
            None => {}
        }
    }

    The comment above that arm — "Class accessors are properties of the class prototype chain" — is true for instances. The missing condition is that the receiver must be an ordinary instance, not the class object itself.

    Fix shape

    Apply the instance accessor chain only when the receiver is not a class constructor. A regression test should pin the bundle's shape directly, since it is ordinary ES semantics:

    class XX extends Error { get errorCode(){ return this.constructor.errorCode } }
    const VR8 = class VR8 extends XX {}
    VR8.errorCode = "invalid_request";          // must NOT throw
    new VR8("x").errorCode === "invalid_request" // getter delegates to the static

    Worth pinning the same for a static data write shadowed by an instance getter of the same name on any base in the chain, and for Reflect.set(VR8, "errorCode", v) — the failing path here runs through ordinary_set_with_receiver, so the receiver-carrying entry point deserves its own case.

    Notes on two suspects raised elsewhere

    • fix: class methods called on class-id-0 objects whose prototype chain reaches the class #11206 ("class members reached through a class-id-0 object") is the right neighbourhood but not this mechanism as stated: the receiver's class_id here is 7515, not 0.
    • Frame names in the JS stack are unreliable for this bug. The reported stack blames OBY, but OBY in the bundle is a render loop (function OBY(q,K,_,z,Y,A){let{width,height,index}=q; …}) and is uniquely defined — so it is a nearest-symbol artefact, not the writer. The native backtrace above is the one to work from.
    • Not GC-sensitive: identical under PERRY_GEN_GC=0 and PERRY_GC_FORCE_EVACUATE=1.
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