Repository navigation
Description
Summary
After #740 closed the ParseResult.ts / TaggedError blocker, the Effect end-to-end DoD repro (#321) advances past ParseResult.ts on perry 0.5.910 and now throws during effect/src/HashRing.ts__init:
TypeError: value is not a function
at <anonymous>
Unlike the prior five sub-issues (which all needed the full Effect init prefix to repro), this one reproduces standalone in 20 lines and manifests as silent object-key loss + wrong method dispatch — a clean, fast iteration target.
Standalone repro (no Effect needed)
// insp.ts
export const BaseProto = {
toJSON() { return { kind: "base" }; },
toString() { return "base-proto"; },
};
export function pipeArguments(self: any, args: any): any { return self; }// entry.ts
import * as Inspectable from "./insp.ts";
import { pipeArguments } from "./insp.ts";
const TypeId = "~test/HashRing" as const;
const Proto = {
[TypeId]: TypeId,
[Symbol.iterator]<A>(this: any): Iterator<A> {
return [][Symbol.iterator]();
},
pipe() {
return pipeArguments(this, arguments);
},
...Inspectable.BaseProto,
toJSON(this: any) {
return { _id: "HashRing", x: 1 };
},
};
console.log("[3] Proto built, keys:", Object.keys(Proto).length);
const inst = Object.create(Proto);
console.log("[4] inst.toJSON:", JSON.stringify(inst.toJSON()));
console.log("[5] inst[TypeId]:", (inst as any)[TypeId]);perry compile entry.ts -o out && ./outActual output (perry 0.5.910):
[3] Proto built, keys: 2 ← should be 5
[4] inst.toJSON: "1970-01-01T00:00:00.000Z" ← should be {"_id":"HashRing","x":1}
[5] inst[TypeId]: undefined ← should be "~test/HashRing"
Expected (node / bun):
[3] Proto built, keys: 5
[4] inst.toJSON: {"_id":"HashRing","x":1}
[5] inst[TypeId]: ~test/HashRing
Three distinct symptoms from one mis-compiled object literal:
- Key count 2 vs 5 — the
[TypeId]: TypeIdcomputed-string-key property, the[Symbol.iterator]computed-key method, and the spread-in...Inspectable.BaseProtoprops are all dropped. Onlypipeand the finaltoJSONsurvive (and even those are mis-wired, see linux compilation and README #2). inst.toJSON()returns"1970-01-01T00:00:00.000Z"— the object's owntoJSONmethod is shadowed/mis-resolved toDate.prototype.toJSON(the NaN-box / dispatch fallback is treating the receiver as a Date). This is the smoking gun for "value is not a function" in the Effect context — the method-table for this object literal is corrupt.inst[TypeId]isundefined— confirms the computed-string-key property[TypeId]: TypeIdnever made it into the object.
Why Effect throws instead of mis-behaving
In effect/src/HashRing.ts:51-67:
const TypeId = "~effect/cluster/HashRing" as const
// ...
const Proto = {
[TypeId]: TypeId,
[Symbol.iterator]<A extends PrimaryKey.PrimaryKey>(this: HashRing<A>): Iterator<A> {
return Iterable.map(this.nodes.values(), ([n]) => n)[Symbol.iterator]()
},
pipe() {
return pipeArguments(this, arguments)
},
...Inspectable.BaseProto,
toJSON(this: HashRing<any>) {
return { _id: "HashRing", baseWeight: this.baseWeight, nodes: ... }
}
}Same shape: computed-string-key prop ([TypeId]: TypeId — TypeId is a string const, not a symbol), a [Symbol.iterator] computed-key method, a pipe() method, an object-spread of a cross-module namespace member (...Inspectable.BaseProto), and a trailing toJSON() method. At module init perry builds this corrupted Proto, and the first dispatch through one of its dropped/mis-wired methods hits js_closure_callN(invalid_ptr) → throw_not_callable() → TypeError: value is not a function.
Bisection (for the record)
lldb backtrace → _main + 1048 → unlinked offset 0x6A8 in test_bare_ts.o → relocation _node_modules_effect_src_HashRing_ts__init. Throw site is crates/perry-runtime/src/closure.rs:773-781 (throw_not_callable, b"value" placeholder).
Likely root cause
Object-literal codegen mis-handles the combination of: computed-key entries ([expr]: v and [expr]() {}) interleaved with an object-spread (...nsMember). The minimal repro suggests the spread either:
- truncates the key list (only 2 of 5 survive — possibly the spread resets/overwrites the in-progress key array instead of merging), and/or
- corrupts the method vtable so
toJSONresolves toDate.prototype.toJSON(a NaN-box tag confusion — the object pointer is being read as a Date).
Reducing the repro further (drop the spread, drop the computed [Symbol.iterator], drop the [TypeId] string-key one at a time) will localize which combination triggers the truncation — I left the full shape intact so the reproduction is faithful to HashRing.ts.
Status snapshot for #321
| Phase | v0.5.862 | v0.5.893 | v0.5.910 |
|---|---|---|---|
| Boots into main | ✅ | ✅ | ✅ |
| Crash module | Schema.ts | ParseResult.ts | HashRing.ts |
| Standalone repro | needs full Effect | needs full Effect | 20-line standalone ✅ |
result: 42 |
— | — | — |
This is the first sub-issue in the chain with a tight standalone repro — should be much faster to land than #611/#671/#685/#711/#740.
Notes for whoever picks this up
- The repro is self-contained (2 files, no npm deps). Iterate on it directly; the fix must produce
keys: 5,inst.toJSON: {"_id":"HashRing","x":1},inst[TypeId]: ~test/HashRing. - Suspect
crates/perry-codegenobject-literal lowering where a spread element follows computed-key elements. Likely the spread path allocates/returns a fresh object (like the oldObjectSpreadbehavior noted inObject.assign(target, src)drops Symbol-keyed properties already ontarget#590's changelog) and discards the computed-key entries accumulated before it. inst.toJSON → Date.prototype.toJSONstrongly implies the resulting object pointer carries a wrong NaN-box tag (Date interior-pointer aliasing) — cross-check against the Date side-table dispatch inobject.rs.
Refs #321, #740, #590 (object-spread allocate-vs-mutate history).
Activity
docs: #798 — refresh CLAUDE.md parity status section (#820)
fix: #809 — object literal spread+methods+computed-keys; Object.creat…
fix: #809 — object literal spread+methods+computed-keys; Object.creat…
fix: class-object value dispatch — static-method this-binding (#1787)…
fix(runtime): bind this to receiver for inherited string-keyed method…
fix(runtime): link Object.create synthetic class_id to original class…
docs(changelog): follow-up fragment, and correct PerryTS#7192's Objec…
fix(codegen): close #7192's own residual root-store hole, and make th…
test(codegen): pin the by-name object literal's nested closure roots
perf(codegen): delete six instructions from the generic property-read…
fix(gc): two root-dominance regressions + restore the corpus's missin…
fix(runtime): the [[Set]] owner walk takes the synthetic-class protot…
fix(runtime): the [[Set]] owner walk takes the synthetic-class protot…
fix(runtime): indexed writes honour a custom array prototype (from #9297