Skip to content

fix(runtime): in operator on a closure value no longer SIGSEGVs (#1758) - #1811

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-schema-2d
May 25, 2026
Merged

fix(runtime): in operator on a closure value no longer SIGSEGVs (#1758)#1811
proggeramlug merged 1 commit into
mainfrom
worktree-schema-2d

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

key in fn (the in operator on a closure/function value) SIGSEGV'd.
Functions are objects in JS, so key in fn is valid — but
js_object_has_property treated the closure pointer as an ObjectHeader and
read (*obj_ptr).keys_array at the closure's capture-slot offset (a NaN-boxed
value, not a real *ArrayHeader), then crashed in js_array_length
is_registered_set dereferencing the tagged value.

This is the last blocker on import * as S from "effect/Schema" MODULE INIT
(after #1804 / #1809 / #1810): effect's dual-wrapped helpers do
<key> in someClosure deep in the fiber runtime. With this fix, the Schema
import initializes byte-identical to node (ok object).

Localization

PERRY_DEBUG_SYMBOLS=1 + lldb -k crash hooks pinned the fault to
is_registered_set deref'ing 0x7ffd… (a NaN-boxed value). A conditional
breakpoint on js_array_length with a tagged arg captured the receiver: GC
type 0x04 (GC_TYPE_CLOSURE), first field a code pointer (a fiberRuntime
closure body) — i.e. a function, not an object.

Fix

js_object_has_property detects a GC_TYPE_CLOSURE receiver (sibling to the
existing GC_TYPE_ARRAY fast path) and mirrors the closure read path
(js_object_get_field_by_name: length → arity, others →
CLOSURE_DYNAMIC_PROPS): present-and-not-undefined ⇒ true. Regular
object/array in is untouched.

Validation

  • New gap test test_gap_in_operator_closure.ts — byte-for-byte with node:
    absent key (false, no crash), length, dynamically-assigned props.
  • import * as S from "effect/Schema"ok object (exit 0). effect/Effect
    deep import stays green (42).
  • 80-test object/array/closure/class/symbol regression sweep
    (PERRY_NO_AUTO_OPTIMIZE=1): 0 regressions — every failing test is
    pre-existing and none uses the in operator (the fix's only blast
    radius). ('name'/'prototype' in fn remain a separate pre-existing
    closure-property gap — fn.name also reads undefined — intentionally not
    asserted.)

Remaining (reported, not chained)

Schema init works now, but actually USING Schema and the barrel hit further
distinct blockers:

  • S.decodeUnknownSync(S.Number)(42)_tag-undefined in
    ParseResult.ts__69 (the parse machinery).
  • import { Effect } from "effect" barrel → value is not a function.

Refs #1758, #1785, #1772, #1791.

)

Functions are objects in JS, so `key in fn` is valid. `js_object_has_property`
treated a closure pointer as an `ObjectHeader` and read `(*obj_ptr).keys_array`
at the closure's capture-slot offset — a NaN-boxed value, not a real
`*ArrayHeader` — then crashed in `js_array_length` → `is_registered_set`
dereferencing the tagged value.

This is the last blocker on `import * as S from "effect/Schema"` MODULE INIT
(after #1804/#1809/#1810): effect's `dual`-wrapped helpers do `<key> in
someClosure` deep in the fiber runtime. Localized via PERRY_DEBUG_SYMBOLS=1 +
lldb `-k` crash hooks + a conditional breakpoint on `js_array_length` with a
tagged arg: the receiver had GC type 0x04 (GC_TYPE_CLOSURE) and its first field
was a code pointer (a fiberRuntime closure body).

Fix: `js_object_has_property` detects a `GC_TYPE_CLOSURE` receiver (sibling to
the existing GC_TYPE_ARRAY fast path) and mirrors the closure read path
(`js_object_get_field_by_name`: `length` → arity, others → CLOSURE_DYNAMIC_PROPS)
— present-and-not-undefined ⇒ true. Matches node for absent keys (false, no
crash), `length`, and dynamically-assigned props.

With this, `import * as S from "effect/Schema"` initializes byte-identical to
node (`ok object`). `effect/Effect` deep import stays green (42). New gap test
`test_gap_in_operator_closure.ts`. (`'name'`/`'prototype' in fn` remain a
separate pre-existing closure-property gap — `fn.name` also reads undefined —
intentionally not asserted.)

NOTE: actually USING Schema (`S.decodeUnknownSync(S.Number)(42)`) and the
`effect` barrel hit further distinct blockers (ParseResult `_tag`-undefined;
`value is not a function`). Reported separately.

Refs #1758, #1785, #1772, #1791.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant