Skip to content

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

@proggeramlug

Description

@proggeramlug

Summary

After #671 closed the v0.5.795 TypeError: value is not a function during HashMap.ts__init, the Effect end-to-end DoD repro from #321 advances much further at perry 0.5.809 — at least 308 module inits run cleanly before the runtime trips on a new uncaught throw during effect/src/Schema.ts__init:

TypeError: (number).slice is not a function
    at <anonymous>

Strict improvement over #671 (HashMap.ts was the 26th init; Schema.ts is around the 309th of 362), but still blocks the DoD repro.

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: (number).slice is not a function
#              at <anonymous>
# exit:    1

Crashes before [1] before prints — still pre-main module-init territory.

Bisection

lldb -o "b _exit" -o "run" -o "bt" -o "quit" --batch backtrace:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint on __exit
  * frame #0: libsystem_kernel.dylib`__exit
    frame #1: libsystem_c.dylib`exit + 68
    frame #2: out_b`<unnamed> + 12                ← _Exit panic-exit
    frame #3: out_b`<unnamed> + 28                ← throw bubbles
    frame #4: out_b`<unnamed> + 440               ← throw machinery
    frame #5: out_b`<unnamed> + 504
    frame #6: out_b`<unnamed> + 12012             ← inside Schema.ts__init body
    frame #7: out_b`<unnamed> + 5024              ← deeper init helper
    frame #8: out_b`_main + 1252                  ← `bl @ _main + 1248`
    frame #9: dyld`start

Mapping _main + 1252 (return addr) → bl @ _main + 1248 → unlinked offset 0x290 + 0x4E0 = 0x770 in the entry .o. Looking up the relocation:

0x770 ARM64_RELOC_BRANCH26  _node_modules_effect_src_Schema_ts__init

So the crash is during effect/src/Schema.ts__init. Note the very deep +12012 offset on frame #6 — Schema.ts is huge (11,443 lines), its init function is enormous, and the throw fires deep inside it.

Throw-site analysis

(number).<method> is not a function traces to crates/perry-runtime/src/object.rs:6488-6506js_native_call_method's primitive-receiver catch-all:

let primitive_kind: Option<&'static str> = if jsval.is_any_string() {
    Some("string")
} else if jsval.is_int32() || jsval.is_number() {
    Some("number")
} else if jsval.is_bool() {
    Some("boolean")
} else if jsval.is_bigint() {
    Some("bigint")
} else {
    None
};
if let Some(kind) = primitive_kind {
    crate::error::js_throw_type_error_not_a_function(
        kind.as_ptr(), kind.len(),
        method_name.as_ptr(), method_name.len(),
    );
}

So during Schema.ts__init, codegen emitted a dynamic dispatch js_native_call_method(value, "slice", args) where value carried a NaN-box int32/double tag rather than a string or array pointer. Two plausible upstream causes:

  1. Wrong value flowing into a .slice() call — somewhere a binding that should hold a string/array was bound to a number (e.g. a Symbol.for(...) mis-resolved, or a cross-module property read returned a placeholder int).
  2. Codegen mis-typed the receiver — a top-level const X = someFactory(...); X.slice(...) where X is typed as any because the factory's return type couldn't be inferred, so codegen falls through to runtime tag dispatch; if the factory ran in a way that produced a numeric NaN-box (e.g. the codegen-side scalar-replacement perry/ui: state.text() and ForEach(stateCount,...) don't propagate on macOS after .set() #599 / sentinel-int patterns), the dispatch hits the number arm.

Schema.ts:945-960 has the only top-level .slice(...) calls I can find:

return class TemplateLiteralParserClass extends transformOrFail(from, to, {
  strict: false,
  decode: (i, _, ast) => {
    const match = re.exec(i)
    return match
      ? ParseResult.succeed(match.slice(1, params.length + 1))  // line 955
      : ParseResult.fail(...)
  },
  encode: (tuple) => ParseResult.succeed(tuple.join(""))
}) {
  static params = params.slice()                                // line 960
}

The match.slice(1, params.length + 1) (line 955) is inside a closure body, but static params = params.slice() (line 960) is a static class field initializer, evaluated when the class is declared, which itself is inside the TemplateLiteralParser factory body — also not directly at module-init time.

A grep for top-level .slice( in Schema.ts only finds those three sites. The init-time .slice() may be coming from a different shape that codegen lowers to slice — e.g. spread-rest into an array allocator, or destructuring into rest patterns.

Status snapshot for #321

Phase v0.5.706 v0.5.795 v0.5.809
Modules collected 362 ✅ 362 ✅ 362 ✅
Codegen failures 0 ✅ 0 ✅ 0 ✅
Missing-symbol refs 0 ✅ 0 ✅ 0 ✅
Link clean ✅ clean ✅ clean ✅
Boots into main ❌ SIGSEGV
Inits cleared before crash 25/362 (HashMap) 308/362 (Schema)
result: 42

Each landed fix has unblocked more of the init chain. The remaining surface is now ~54 modules of Effect (Schema.ts + everything topologically downstream of it).

Notes for whoever picks this up

  • --keep-intermediates keeps test_bare_ts.o; objdump -r test_bare_ts.o | grep "_node_modules_effect_src.*__init$" | sort gives init order, lldb -o "b _exit" -o "run" -o "bt" -o "quit" --batch <bin> captures the backtrace.
  • Schema.ts is 11,443 lines and probably the highest-yield place in the whole Effect tree to find more issues — if this throw covers a generic "dynamic .slice() against a numeric receiver" codegen bug, the same root cause likely affects other heavy-generics packages.
  • Easiest next step: instrument the throw helper in error.rs:293 to also dump the calling code's address (std::backtrace::Backtrace::capture()) before throwing — would identify the exact lowering shape that emitted js_native_call_method(<numeric>, "slice", ...).
  • Schema.ts has a single top-level .slice() that isn't inside a closure: static params = params.slice() at line 960, inside the TemplateLiteralParser factory's returned class. If codegen's static-class-field init for declared-inside-factory classes evaluates eagerly at module init when it shouldn't, that's the lead.

Refs #321, #671.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions