Summary
After #711 closed the pipe is not a function throw in Schema.ts__init at v0.5.862, the Effect end-to-end DoD repro from #321 progresses past Schema.ts on perry 0.5.893 — but now trips earlier in the init chain on effect/src/ParseResult.ts__init:
TypeError: value is not a function
at <anonymous>
Strict improvement (Schema.ts no longer the blocker; ParseResult.ts is reached and then fails on a class extends FactoryCall<...> pattern) but #321 still blocked.
Repro
mkdir effect-321 && cd effect-321
cat > package.json <<'JSON'
{ "name": "effect-321", "version": "0.0.0", "type": "module",
"perry": { "compilePackages": ["effect"] } }
JSON
bun add effect@3.21.2
cat > test_bare.ts <<'TS'
console.log("[1] before");
import {} from "effect";
console.log("[2] after");
TS
perry compile test_bare.ts -o /tmp/out
/tmp/out
# stdout: (nothing)
# stderr: TypeError: value is not a function
# at <anonymous>
# exit: 1
Bisection
lldb -o "b _exit" -o "run" -o "bt" -o "quit" --batch backtrace:
* frame #0: libsystem_kernel.dylib`__exit
frame #1: libsystem_c.dylib`exit + 68
frame #2-#5: throw machinery (4 frames)
frame #6: out_b`<unnamed> + 32
frame #7: out_b`<unnamed> + 280
frame #8: out_b`<unnamed> + 1140 ← inside ParseResult.ts__init body
frame #9: out_b`_main + 1052 ← `bl @ _main + 1048` = unlinked offset 0x6A8
frame #10: dyld`start
_main + 1048 → unlinked offset 0x290 + 0x418 = 0x6A8 → relocation:
0x6a8 ARM64_RELOC_BRANCH26 _node_modules_effect_src_ParseResult_ts__init
So the crash is during effect/src/ParseResult.ts__init — a DIFFERENT module than the four prior sub-issues (Utils.ts → HashMap.ts → Schema.ts → Schema.ts → ParseResult.ts), and earlier in the topological chain than where v0.5.862 was failing (Schema.ts depends on ParseResult.ts).
Throw-site analysis
Message format value is not a function (no (kind). prefix, with the literal placeholder "value") traces to crates/perry-runtime/src/closure.rs:773-781 — throw_not_callable() invoked from js_closure_callN when func_ptr validation fails:
#[cold]
#[inline(never)]
fn throw_not_callable() -> ! {
crate::error::js_throw_type_error_not_a_function(
std::ptr::null(),
0,
b"value".as_ptr(),
5,
)
}
Same throw shape as the original #671 (HashMap.ts). The difference: there, the bad func_ptr came from a cross-module namespace-member getter; here, ParseResult.ts has a much more constrained candidate set.
Almost certainly: class ParseError extends TaggedError("ParseError")<...>
ParseResult.ts:230 has the canonical class-extends-factory pattern:
import { TaggedError } from "./Data.js"
// ...
export class ParseError extends TaggedError("ParseError")<{ readonly issue: ParseIssue }> {
readonly [ParseErrorTypeId] = ParseErrorTypeId
get message() { return this.toString() }
toString() { return TreeFormatter.formatIssueSync(this.issue) }
toJSON() { return { _id: "ParseError", message: this.toString() } }
[Inspectable.NodeInspectSymbol]() { return this.toJSON() }
}
TaggedError is defined in effect/src/Data.ts:755-765:
export const TaggedError = <Tag extends string>(tag: Tag) => {
const O = {
BaseEffectError: class extends Error<{}> {
readonly _tag = tag
}
}
;(O.BaseEffectError.prototype as any).name = tag
return O.BaseEffectError as any
}
So at module init, perry needs to:
- Call
TaggedError("ParseError") — a closure invocation that creates O, mutates its prototype, returns the class. If this closure's func_ptr was registered with a stale/invalid pointer (cross-module function-export from Data.ts), it throws here.
- Use the returned class as the base for
ParseError's extends clause.
- Evaluate
ParseError's body (computed-key field init [ParseErrorTypeId] = ParseErrorTypeId, getter get message(), methods).
The throw fires somewhere in this chain. Note this is conceptually identical to #711's class extends String$.pipe(...) — class extending a factory-call result — but here the factory itself is a cross-module function import (TaggedError from ./Data.js), so the candidate failure surface includes:
- Cross-module function-pointer registration for
TaggedError returning an invalid closure descriptor in the importer's module-init context
- Object-literal-with-inner-class-decl evaluation:
{ BaseEffectError: class extends Error<{}> { ... } } — codegen for an object literal containing a class declaration may not register the inner class's vtable correctly when the literal is evaluated at call time rather than module-init time
.prototype write on the inner class: (O.BaseEffectError.prototype as any).name = tag — if codegen doesn't track .prototype for classes that were declared inside an object literal, this write goes to nowhere and the resulting class's vtable lookup later fails
What I tried for a standalone repro
A minimal mimic that reproduces the surface shape (TaggedError factory returning a class declared inside an object literal, class extending the factory's return value with computed-key field init):
class BaseError { constructor(opts: { readonly issue: any }) { this.issue = opts.issue } }
function TaggedError<Tag extends string>(tag: Tag) {
const O = { BaseEffectError: class extends BaseError { readonly _tag = tag } }
;(O.BaseEffectError.prototype as any).name = tag
return O.BaseEffectError
}
const ParseErrorTypeId: unique symbol = Symbol.for("test/ParseError")
class ParseError extends TaggedError("ParseError")<{ readonly issue: any }> {
readonly [ParseErrorTypeId] = ParseErrorTypeId
// ...
}
…actually hits a different TypeError standalone (Cannot read properties of undefined (reading 'issue') during the new ParseError(...) step), but the same general class-extends-factory shape. The fact that perry already errors on this minimal pattern (rather than working clean as the prior sub-issue repros did) suggests this whole "class extends factory-returning-inner-class" path is fragile and may be addressable with a focused unit test.
Status snapshot for #321
| Phase |
v0.5.795 |
v0.5.809 |
v0.5.862 |
v0.5.893 |
| Boots into main |
✅ |
✅ |
✅ |
✅ |
| Crash module |
HashMap.ts (#26) |
Schema.ts |
Schema.ts (deeper) |
ParseResult.ts |
| Throw shape |
invalid closure |
(number).slice |
(real-obj).pipe |
invalid closure |
result: 42 |
— |
— |
— |
— |
The crash has finally moved out of Schema.ts. With #711's class-extends-pipe fix, Schema.ts is now reachable past the previous throw point — but the init chain immediately hits an earlier-bound module (ParseResult.ts is a Schema.ts dependency) where a parallel pattern fails.
Notes for whoever picks this up
- The standalone repro above fails standalone — much faster iteration than the full Effect repro. Whatever fix lands needs to make BOTH the standalone repro (printing
[3] ParseError defined, [4] pe._tag: ParseError, [5] pe.message: ...) AND the Effect DoD work.
- Object-literal-containing-class-declaration is the most novel shape vs. prior sub-issues. The inner class's prototype gets mutated immediately after the literal evaluates — if perry's class-registration runs eagerly at literal-creation time and the prototype write doesn't propagate to vtable, that's the lead.
- Effect's
Data.ts defines several factory functions with this same inner-class-in-object-literal shape (TaggedClass, TaggedEnum, Class, ...) — a fix here likely unblocks several downstream modules at once.
Refs #321, #711.
Summary
After #711 closed the
pipe is not a functionthrow inSchema.ts__initat v0.5.862, the Effect end-to-end DoD repro from #321 progresses past Schema.ts on perry 0.5.893 — but now trips earlier in the init chain oneffect/src/ParseResult.ts__init:Strict improvement (Schema.ts no longer the blocker; ParseResult.ts is reached and then fails on a
class extends FactoryCall<...>pattern) but #321 still blocked.Repro
Bisection
lldb -o "b _exit" -o "run" -o "bt" -o "quit" --batchbacktrace:_main + 1048→ unlinked offset0x290 + 0x418 = 0x6A8→ relocation:So the crash is during
effect/src/ParseResult.ts__init— a DIFFERENT module than the four prior sub-issues (Utils.ts → HashMap.ts → Schema.ts → Schema.ts → ParseResult.ts), and earlier in the topological chain than where v0.5.862 was failing (Schema.ts depends on ParseResult.ts).Throw-site analysis
Message format
value is not a function(no(kind).prefix, with the literal placeholder"value") traces tocrates/perry-runtime/src/closure.rs:773-781—throw_not_callable()invoked fromjs_closure_callNwhen func_ptr validation fails:Same throw shape as the original #671 (HashMap.ts). The difference: there, the bad func_ptr came from a cross-module namespace-member getter; here, ParseResult.ts has a much more constrained candidate set.
Almost certainly:
class ParseError extends TaggedError("ParseError")<...>ParseResult.ts:230 has the canonical class-extends-factory pattern:
TaggedErroris defined ineffect/src/Data.ts:755-765:So at module init, perry needs to:
TaggedError("ParseError")— a closure invocation that createsO, mutates its prototype, returns the class. If this closure's func_ptr was registered with a stale/invalid pointer (cross-module function-export from Data.ts), it throws here.ParseError'sextendsclause.ParseError's body (computed-key field init[ParseErrorTypeId] = ParseErrorTypeId, getterget message(), methods).The throw fires somewhere in this chain. Note this is conceptually identical to #711's
class extends String$.pipe(...)— class extending a factory-call result — but here the factory itself is a cross-module function import (TaggedErrorfrom./Data.js), so the candidate failure surface includes:TaggedErrorreturning an invalid closure descriptor in the importer's module-init context{ BaseEffectError: class extends Error<{}> { ... } }— codegen for an object literal containing a class declaration may not register the inner class's vtable correctly when the literal is evaluated at call time rather than module-init time.prototypewrite on the inner class:(O.BaseEffectError.prototype as any).name = tag— if codegen doesn't track.prototypefor classes that were declared inside an object literal, this write goes to nowhere and the resulting class's vtable lookup later failsWhat I tried for a standalone repro
A minimal mimic that reproduces the surface shape (TaggedError factory returning a class declared inside an object literal, class extending the factory's return value with computed-key field init):
…actually hits a different TypeError standalone (
Cannot read properties of undefined (reading 'issue')during thenew ParseError(...)step), but the same general class-extends-factory shape. The fact that perry already errors on this minimal pattern (rather than working clean as the prior sub-issue repros did) suggests this whole "class extends factory-returning-inner-class" path is fragile and may be addressable with a focused unit test.Status snapshot for #321
result: 42The crash has finally moved out of Schema.ts. With #711's class-extends-pipe fix, Schema.ts is now reachable past the previous throw point — but the init chain immediately hits an earlier-bound module (ParseResult.ts is a Schema.ts dependency) where a parallel pattern fails.
Notes for whoever picks this up
[3] ParseError defined,[4] pe._tag: ParseError,[5] pe.message: ...) AND the Effect DoD work.Data.tsdefines several factory functions with this same inner-class-in-object-literal shape (TaggedClass,TaggedEnum,Class, ...) — a fix here likely unblocks several downstream modules at once.Refs #321, #711.