Skip to content

GetIterator on a class declaration throws 'is not iterable' — INT32 ClassRef rejected by js_get_iterator's primitive guard (blocks Effect's yield* SomeTag) #6454

Closed

Description

@proggeramlug

Summary

With #6449's fixes applied, effect's api.ts module init now completes and the fiber runtime starts (the effect logger emits), but Layer.launch dies with:

[HH:MM:SS.mmm] ERROR (#0):
  TypeError: is not iterable
      at <anonymous>

Trace

perry_runtime::symbol::iterator::throw_value_not_iterable
js_get_iterator + 1576
perry_closure_node_modules__effect_platform_src_HttpLayerRouter_ts__59
js_native_call_value
perry_closure_node_modules_effect_src_internal_fiberRuntime_ts__20
perry_closure_node_modules_effect_src_Utils_ts__38
perry_closure_node_modules_effect_src_internal_fiberRuntime_ts__19
…
perry_method_…_fiberRuntime_ts__FiberRuntime____computed_method_44208_44229

So a for…of / spread inside @effect/platform/src/HttpLayerRouter.ts is handed a non-iterable. The prime candidate is getMiddleware:

const getMiddleware = (context: Context.Context<never>): Array<middleware.Fn> => {
  let arr = middlewareCache.get(context)          // WeakMap
  if (arr) return arr
  const topLevel = Arr.empty<Array<middleware.Fn>>()
  let maxLength = 0
  for (const [key, value] of context.unsafeMap) { // <-- Map iteration + array destructuring
    …
  }

i.e. context.unsafeMap is not iterable (undefined, or a Map that Perry's js_get_iterator doesn't accept). Reached via stack.push(...getMiddleware(depsContext)).

Other iteration sites in that file, for triage: for (const fn of Arr.reverse(middleware)) (line ~212), for (const arr of topLevel) (~807), for (const route of router_.routes) (~1044).

Repro

git clone https://github.com/bjacobso/effect-compiled-experiment && cd effect-compiled-experiment
npm install
perry compile src/effect/web.ts -o dist/web
PORT=3000 ./dist/web

with PR #6449 applied (without it, web.ts fails earlier — see #6438).

Environment

Perry PR #6449 head (3187ef3e5, rebased on origin/main), macOS arm64, coherent full cargo build --release, compiled with auto-optimize.

Context — this is the 4th layer

web.ts has been behind a chain; each layer was a distinct bug, all now fixed in #6449:

  1. TypeError: undefined is not iterable — own statics shared across evaluations of a class expression with heritage
  2. cannot extend "BadArgument" with "BadArgument" — inherited statics resolving through the template's last-wins class_id chain instead of the object's own parent
  3. Cannot convert undefined or null to object — this bound to the PROTO instead of the receiver for a method inherited by a FUNCTION object (Object.setPrototypeOf(fn, Proto))
  4. this issue

logger.ts and forking.ts from the same repo already run byte-identical to node.

Note

I have not minimized this one yet. If context.unsafeMap is the culprit, the likely question is whether a Map-valued property read through effect's Context returns something js_get_iterator rejects — worth checking a for (const [k, v] of someObj.mapProp) shape (Map behind a property read, with array destructuring in the binding) before assuming it is undefined.

Activity

  1. proggeramlug commented on Jul 16, 2026

    @proggeramlug
    ContributorAuthor

    Summary

    yield* / for…of / destructuring / spread over a class declaration throws
    TypeError: is not iterable in Perry, where Node iterates it.

    This blocks Effect's most central idiom — yield* SomeTag, where the tag is a
    class:

    class HttpRouter extends Context.Tag("HttpRouter")<...>() {}
    const router = yield* HttpRouter        // GetIterator(HttpRouter) — a CLASS

    It is the remaining failure in the web.ts app of the effect-compiled-experiment
    repro (logger.ts, forking.ts, api.ts all pass byte-identical to node).

    Minimal repro (no Effect involved)

    const Proto: any = {
      [Symbol.iterator](this: any) { return [this.key, "second"][Symbol.iterator]() }
    }
    function makeTag(key: string): any {
      function TagClass() {}
      Object.setPrototypeOf(TagClass, Proto)
      ;(TagClass as any).key = key
      return TagClass
    }
    
    const RawTag: any = makeTag("raw-tag")
    function* gRaw(): any { return yield* RawTag }
    console.log([...gRaw()])              // node ["raw-tag","second"] | perry ["raw-tag","second"]  OK
    
    class DeclTag extends makeTag("decl-tag") {}
    function* gDecl(): any { return yield* (DeclTag as any) }
    console.log([...gDecl()])             // node ["decl-tag","second"] | perry TypeError: is not iterable

    yield* on the factory's plain function object works. yield* on a class
    declaration
    extending it does not.

    Per-construct matrix (class declaration receiver)

    construct node perry
    yield* DeclTag ["decl-tag","second"] TypeError: is not iterable
    [...DeclTag] ["decl-tag","second"] TypeError: value is not iterable
    for (const v of DeclTag) ["decl-tag","second"] TypeError: is not iterable
    const [a,b] = DeclTag ["decl-tag","second"] TypeError: is not iterable
    Array.from(DeclTag) ["decl-tag","second"] [] (silently wrong, no throw)

    Root cause

    Perry lowers a class declaration to an INT32-tagged ClassRef
    (INT32_TAG \| class_id). js_get_iterator
    (crates/perry-runtime/src/symbol/iterator.rs) guards its generic
    [Symbol.iterator] lookup with:

    // A primitive number / boolean / null / undefined is not iterable...
    let jsv = JSValue::from_bits(val_f64.to_bits());
    if !jsv.is_pointer() && !jsv.is_any_string() {
        throw_value_not_iterable();
    }

    A ClassRef is not pointer-tagged, so it is misread as a primitive number and
    throws before the lookup at the bottom of the function — even though
    js_object_get_symbol_property already resolves class refs correctly
    (bits >> 48 == 0x7FFE → CLASS_STATIC_SYMBOLS → resolve_proto_chain_symbol
    → class_parent_closure, the last of which exists specifically for
    class Svc extends Context.Tag(id)<...>() {}, #36/#321).

    So the resolution machinery is present and correct; the value never reaches it.

    Confirmed in the emitted IR for web.ts — the operand is a bare constant:

    if.then.13:                                        ; generator state == 0
      %r43 = call double @js_get_iterator(double 0x7FFE000000000002)   ; INT32 2 = class_id 2

    6 of 709 js_get_iterator calls across the 311-module build pass a constant
    class ref (in HttpLayerRouter.ts and HttpApiBuilder.ts).

    Caveat worth recording

    INT32_TAG | 2 (the number 2) and a ClassRef with class_id == 2 are
    bit-identical; only is_class_id_registered distinguishes them. Any fix must
    therefore keep throwing when a class-ref-shaped value resolves no
    [Symbol.iterator], so for (const x of 37) {} remains a TypeError even when
    class id 37 happens to exist. A number colliding with a registered class id that
    does carry [Symbol.iterator] is inherently ambiguous under this
    representation — vanishingly rare, but it is a real property of the encoding.

    Secondary: error-message parity

    Perry's messages on this path do not match node's for any subject:

    subject node perry
    yield* undefined undefined is not iterable (cannot read property Symbol(Symbol.iterator)) is not iterable
    yield* 2 number 2 is not iterable (cannot read property Symbol(Symbol.iterator)) is not iterable
    [...2] 2 is not iterable value is not iterable

    crates/perry-runtime/src/collection_iter.rs::not_iterable_message already
    produces node's exact format and is reused by the Map/Set constructors; the
    symbol/iterator.rs + array/iterator.rs throwers predate it and emit bare or
    "value"-prefixed text. Worth routing them through it (it needs null/
    undefined cases added — node prints undefined and object null).

  2. changed the title [-]effect: HttpLayerRouter 'TypeError: is not iterable' blocks Layer.launch (4th layer; #6449 clears the first three)[/-] [+]GetIterator on a class declaration throws 'is not iterable' — INT32 ClassRef rejected by js_get_iterator's primitive guard (blocks Effect's `yield* SomeTag`)[/+] on Jul 16, 2026
  3. proggeramlug commented on Jul 16, 2026

    @proggeramlug
    ContributorAuthor

    Combined-build validation (PR #6460 + PR #6449 + PR #6459)

    On a build carrying all three fixes, the minimal-repro matrix is fully identical to node — yield*, spread, for…of, Array.from, and destructuring on a class declaration all behave like node, and every primitive still throws. The only remaining diffs on the probe are the error message wording for primitives (perry: bare is not iterable; node: 2 is not iterable etc.) — the secondary parity gap above.

    Effect app status on the combined build (effect-compiled-experiment):

    app status
    logger.ts ✅ byte-identical to node
    forking.ts ✅ byte-identical to node
    api.ts ✅ byte-identical to node
    web.ts ❌ next layer — TypeError: value is not a function at startup

    Two intermediate walls resolved along the way:

  4. proggeramlug commented on Jul 17, 2026

    @proggeramlug
    ContributorAuthor

    Fixed by #6460 (fix(runtime): resolve [Symbol.iterator] on a class declaration), merged to main. effect's yield* SomeTag and the HttpLayerRouter iteration path now work — web.ts (which routes through HttpLayerRouter) serves byte-identical to Node. Closing as resolved.

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