You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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>
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).
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:
TypeError: undefined is not iterable — own statics shared across evaluations of a class expression with heritage
cannot extend "BadArgument" with "BadArgument" — inherited statics resolving through the template's last-wins class_id chain instead of the object's own parent
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))
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.
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:
classHttpRouterextendsContext.Tag("HttpRouter")<...>(){}constrouter=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)
constProto: any={[Symbol.iterator](this: any){return[this.key,"second"][Symbol.iterator]()}}functionmakeTag(key: string): any{functionTagClass(){}Object.setPrototypeOf(TagClass,Proto);(TagClassasany).key=keyreturnTagClass}constRawTag: any=makeTag("raw-tag")function*gRaw(): any{returnyield*RawTag}console.log([...gRaw()])// node ["raw-tag","second"] | perry ["raw-tag","second"] OKclassDeclTagextendsmakeTag("decl-tag"){}function*gDecl(): any{returnyield*(DeclTagasany)}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:
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).
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
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
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.
Activity
proggeramlug commentedon Jul 16, 2026
Summary
yield*/for…of/ destructuring / spread over a class declaration throwsTypeError: is not iterablein Perry, where Node iterates it.This blocks Effect's most central idiom —
yield* SomeTag, where the tag is aclass:
It is the remaining failure in the
web.tsapp of the effect-compiled-experimentrepro (
logger.ts,forking.ts,api.tsall pass byte-identical to node).Minimal repro (no Effect involved)
yield*on the factory's plain function object works.yield*on a classdeclaration extending it does not.
Per-construct matrix (class declaration receiver)
yield* DeclTag["decl-tag","second"]TypeError: is not iterable[...DeclTag]["decl-tag","second"]TypeError: value is not iterablefor (const v of DeclTag)["decl-tag","second"]TypeError: is not iterableconst [a,b] = DeclTag["decl-tag","second"]TypeError: is not iterableArray.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 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_propertyalready resolves class refs correctly(
bits >> 48 == 0x7FFE→CLASS_STATIC_SYMBOLS→resolve_proto_chain_symbol→
class_parent_closure, the last of which exists specifically forclass 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:6 of 709
js_get_iteratorcalls across the 311-module build pass a constantclass ref (in
HttpLayerRouter.tsandHttpApiBuilder.ts).Caveat worth recording
INT32_TAG | 2(the number2) and a ClassRef withclass_id == 2arebit-identical; only
is_class_id_registereddistinguishes them. Any fix musttherefore keep throwing when a class-ref-shaped value resolves no
[Symbol.iterator], sofor (const x of 37) {}remains a TypeError even whenclass id 37 happens to exist. A number colliding with a registered class id that
does carry
[Symbol.iterator]is inherently ambiguous under thisrepresentation — 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:
yield* undefinedundefined is not iterable (cannot read property Symbol(Symbol.iterator))is not iterableyield* 2number 2 is not iterable (cannot read property Symbol(Symbol.iterator))is not iterable[...2]2 is not iterablevalue is not iterablecrates/perry-runtime/src/collection_iter.rs::not_iterable_messagealreadyproduces node's exact format and is reused by the Map/Set constructors; the
symbol/iterator.rs+array/iterator.rsthrowers predate it and emit bare or"value"-prefixed text. Worth routing them through it (it needsnull/undefinedcases added — node printsundefinedandobject null).[-]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`)[/+]proggeramlug commentedon Jul 16, 2026
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: bareis not iterable; node:2 is not iterableetc.) — the secondary parity gap above.Effect app status on the combined build (effect-compiled-experiment):
logger.tsforking.tsapi.tsweb.tsTypeError: value is not a functionat startupTwo intermediate walls resolved along the way:
Unsupported schema or overlapping types / cannot extend "BadArgument" with "BadArgument"seen on a fix(runtime): resolve [Symbol.iterator] on a class declaration (#6454) #6460-only build (no fix(hir): per-evaluation statics for class expressions with heritage (#6438) #6449) does not reproduce on the combined build — it was an artifact of the class-statics aliasing fix(hir): per-evaluation statics for class expressions with heritage (#6438) #6449 fixes, feedingSchema.extendthe same_tag: Literaltwice.value is not a function) is a new, later failure — being traced now; will get its own issue rather than piling onto this one.fix(runtime): resolve `[Symbol.iterator]` on a class declaration (#6454…
fix(codegen): make FFI provenance part of the object cache entry (#6439…
proggeramlug commentedon Jul 17, 2026
Fixed by #6460 (
fix(runtime): resolve [Symbol.iterator] on a class declaration), merged to main. effect'syield* SomeTagand theHttpLayerRouteriteration path now work —web.ts(which routes throughHttpLayerRouter) serves byte-identical to Node. Closing as resolved.