Skip to content

perry-runtime: Effect framework throws TypeError: pipe is not a function during Schema.ts module init (Effect end-to-end blocker, post-#685) #711

Description

@proggeramlug

Summary

After #685 closed the static params = params.slice() lowering bug in Schema.ts__init, the Effect end-to-end DoD repro from #321 makes it further into Schema.ts on perry 0.5.862 — but trips on a new uncaught throw:

TypeError: pipe is not a function
    at <anonymous>

Strict improvement (got ~132 more bytes into Schema.ts's init body) but still blocks the DoD.

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: pipe is not a function
#              at <anonymous>
# exit:    1

Bisection

lldb backtrace via b _exit / run / bt:

  * 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> + 12144            ← inside Schema.ts__init body (was +12012 pre-#685)
    frame #7: out_b`<unnamed> + 2000
    frame #8: out_b`<unnamed> + 64
    frame #9: out_b`<unnamed> + 12272
   frame #10: out_b`_main + 1252                 ← `bl @ _main + 1248` = unlinked offset 0x770
   frame #11: dyld`start

_main + 1248 → unlinked offset 0x290 + 0x4E0 = 0x770 → relocation:

0x770 ARM64_RELOC_BRANCH26  _node_modules_effect_src_Schema_ts__init

Same module as #685, slightly further in. Frame #6 is +12144 (vs +12012 pre-#685, +132 bytes / 33 instructions of additional init body covered).

Throw-site analysis

Message format <prop> is not a function (no (kind). prefix) traces uniquely to crates/perry-runtime/src/object.rs:6521-6531 — the "real-object receiver, method-not-found anywhere in dispatch chain" catch-all (added by #648):

crate::error::js_throw_type_error_not_a_function(
    std::ptr::null(),
    0,
    method_name.as_ptr(),   // = "pipe"
    method_name.len(),
);

So during Schema.ts__init, codegen emitted js_native_call_method(<real-object>, "pipe", args) but the dispatch chain didn't find pipe on the receiver's class vtable, prototype, or field-stored closures.

Almost certainly: top-level class X extends Y.pipe(...)

Schema.ts is full of top-level class declarations that eagerly evaluate String$.pipe(...) (or Lowercased.pipe(...), etc.) as the base class. The first one is at line 4970:

export class Lowercased extends String$.pipe(
  lowercased({ identifier: "Lowercased" })
) {}

export class Uppercased extends String$.pipe(...) {}        // line 5003
export class Capitalized extends String$.pipe(...) {}        // line 5036
export class Uncapitalized extends String$.pipe(...) {}      // line 5069
export class Char extends String$.pipe(length(1, ...)) {}    // line 5079
export class Trimmed extends String$.pipe(...) {}            // line 5162
export class NonEmptyTrimmedString extends Trimmed.pipe(...) {}  // line 5182
export class NonEmptyString extends String$.pipe(...) {}     // line 5324
// ... ~30 more `class X extends Y.pipe(...)` patterns

String$ is itself defined as class String$ extends make<string>(AST.stringKeyword) {} (line 1230) — a class extending the result of a factory function call. In the Effect runtime, make<...>(...) returns a class whose prototype chain reaches Pipeable, so String$.pipe(...) resolves to the universal pipeArguments-based implementation.

In Perry, that lookup fails. Likely culprits:

  • Class-method dispatch doesn't walk the extends factory() prototype chain. When the base is a function call rather than a named class, perry's class-id propagation may not register the inheritance link, so String$'s vtable doesn't include pipe from the factory-produced base.
  • Cross-module pipeArguments doesn't get registered as a method on the result of make(). pipeArguments is imported from ./Pipeable.js and assigned into the prototype of base schemas, but if the assignment path goes through a shape Perry can't track, the method-table entry is missing.
  • Class-id collision — same name (String, possibly aliased to String$ only in Schema.ts) resolves to a different class registration that doesn't have pipe.

What I tried for a standalone repro

A minimal mimic — class Base { pipe(...) {} }; class Derived extends make() {}; const X = Derived.pipe(fn); class Final extends X {} — runs clean. So the trigger needs more state than the surface pattern; consistent with #611 / #671 / #685 (each required the full Effect init prefix to repro).

Status snapshot for #321

Phase v0.5.706 v0.5.795 v0.5.809 v0.5.862
Boots into main ❌ SIGSEGV
Inits cleared before crash 25/362 308/362 308/362*
Crash module Utils.ts HashMap.ts Schema.ts (early) Schema.ts (~132 bytes later)
result: 42

* Same module as v0.5.809 but reaching deeper into the init body — Schema.ts itself is large enough that there's room for several sequential throw sites.

Notes for whoever picks this up

  • The throw is from object.rs:6521 (real-object dispatch catch-all from Calling undefined as a function silently no-ops instead of throwing TypeError #648). Adding eprintln! of the receiver's class_id + the dispatch path tried before the throw would identify exactly where the inheritance lookup falls off.
  • The class X extends Y.pipe(...) shape is the strongest hypothesis. Counter-test: comment out everything after Schema.ts line 4970 (or replace the String$.pipe(...) bases with Object or a no-pipe class) and see if the throw moves or goes away.
  • Effect's Pipeable protocol is implemented in effect/src/Pipeable.tspipeArguments(self, args) walks varargs as a fold. Any class produced by make() (Schema.ts:1230's class String$ extends make<string>(AST.stringKeyword) {}) should have pipe installed via the standard Pipeable inheritance chain.

Refs #321, #685.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions