Skip to content

Commit 5b1c520

Browse files
committed
fix(api-manifest): catch up 14 dispatch-table entries lacking API_MANIFEST counterparts (v0.5.824)
`cargo test --release --workspace` was red on `every_dispatch_entry_has_manifest_counterpart` (crates/perry-codegen/ tests/manifest_consistency.rs:50) after the v0.5.815 release-prep sweep. Drift accumulated over ~100 patch versions since the last manifest catch-up. 14 NATIVE_MODULE_TABLE rows had no matching API_MANIFEST entries: fastify::type better-sqlite3::raw perry/tui::{boxSetPaddingEach, boxSetFlexShrink, boxSetFlexBasis, boxSetFlexBasisPct, boxSetWidthPct, boxSetHeightPct, TextStyled, Table, Tabs, InputAt, AnimatedSpinner, useStateTuple} The perry/tui block lands from the v0.5.810 #679 ink-API ergonomics work. Added all 14 to `crates/perry-api-manifest/src/entries.rs` next to the existing per-module blocks. Also tightened the param shapes for 5 of them where the dispatch table is richer than `&[p_any]` would suggest: TextStyled — 4 params (str, str, str, any) Table — 3 params (any, any, any) Tabs — 3 params (any, any, any) InputAt — 2 params (str, any) AnimatedSpinner — 2 params (any, any) All 4 manifest_consistency tests now pass. Pure metadata catch-up; no runtime impact. Surfaced by `scripts/release_sweep.sh` tier 1 during release prep for the v0.5.815 stability checkpoint; the gate is in service of the #463 unimplemented-API check.
1 parent 29cbd45 commit 5b1c520

4 files changed

Lines changed: 25 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Detailed changelog for Perry. See CLAUDE.md for concise summaries.
44

5+
## v0.5.824 — fix(api-manifest): catch up 14 dispatch-table entries that lacked API_MANIFEST counterparts. `cargo test --release --workspace` was red on `every_dispatch_entry_has_manifest_counterpart` (`crates/perry-codegen/tests/manifest_consistency.rs:50`) after the v0.5.815 release-prep sweep. Drift accumulated during the ~100 patch versions since the last manifest catch-up: `fastify::type` (request type accessor), `better-sqlite3::raw` (raw-output statement mode), and 12 perry/tui rows from the v0.5.810 #679 ink-API ergonomics work — `boxSetPaddingEach`, `boxSetFlexShrink`, `boxSetFlexBasis`, `boxSetFlexBasisPct`, `boxSetWidthPct`, `boxSetHeightPct`, `TextStyled`, `Table`, `Tabs`, `InputAt`, `AnimatedSpinner`, `useStateTuple`. Added all 14 rows to `crates/perry-api-manifest/src/entries.rs` next to existing per-module blocks; tightened 5 of them where the dispatch-table param shape was richer than `&[p_any]` would imply (TextStyled = 4 params, Table/Tabs = 3 each, InputAt = 2, AnimatedSpinner = 2). All 4 `manifest_consistency` tests now pass. Pure metadata catch-up; no runtime impact. Surfaced by `scripts/release_sweep.sh` tier 1 during release prep for the v0.5.815 stability checkpoint; the gate is in service of the #463 unimplemented-API check.
6+
57
## v0.5.823 — fix(runtime): NaN equality semantics — `arr.indexOf(NaN)` now returns -1 (was: the NaN's index), `[NaN].includes(NaN)` keeps returning `true`, and `[NaN] === [NaN]` element comparison is now spec-compliant. Pre-fix `[1, NaN, 3].indexOf(NaN)` returned `1` because `js_jsvalue_equals`'s `abits == bbits` fast path matched two identical NaN bit patterns and reported equal. Per ECMA-262: strict equality (`===`) follows IEEE 754 where `NaN === NaN` is `false`, but SameValueZero (used by `includes` / Map / Set keys) considers `NaN` equal to itself. Pre-fix Perry used one helper for both, getting one of the two semantics wrong depending on which way the bug got fixed. The comment on the fast path admitted the gap ("Perry doesn't produce raw IEEE NaN as a user value, so this is safe") but a literal `NaN` in source code lowers to exactly `f64::NAN` (`0x7FF8000000000000`), so the assumption was wrong. **Fix** in `crates/perry-runtime/src/value.rs`: (1) `js_jsvalue_equals` now checks `top16 == 0x7FF8` (the canonical quiet-NaN bit pattern) on both sides BEFORE the `abits==bbits` fast path, returning 0 for NaN-vs-NaN. NaN-boxed tagged values (top16 0x7FFA–0x7FFF) never collide. (2) New `js_jsvalue_same_value_zero` that adds the inverse — returns 1 when both are raw IEEE NaN, otherwise delegates to `js_jsvalue_equals`. (3) `js_array_includes_jsvalue` in `crates/perry-runtime/src/array.rs` switched to the SameValueZero helper. **Validation**: 15-case `/tmp/probe_nan_eq.ts` covers `NaN === NaN`, `NaN === 5`, `5 === 5`, string/object/array reference equality, `includes(NaN)`, `indexOf(NaN)`, Set-with-NaN-keys (dedup to 1), Map-with-NaN-key — all byte-identical to Bun. 26/28 gap parity tests pass (baseline preserved — the 2 failures are the pre-existing console.time + lookbehind-regex categorical gaps). Probed while loop-testing real-app patterns. **Out of scope (separate followups, still on the backlog)**: `Array.from(string)` returns garbage f64s, `Array.from(arraylike)` returns 1 element, `Array.from(_, mapFn)` ignores mapFn, `flat(Infinity)` stops early, `copyWithin` is a no-op, `.finally()` closure mutation doesn't propagate, `arr[Symbol.iterator]()` (no ArrayIterator), `string.match(rx).groups` (no side-table), tagged-template `.raw`, async-fn throw-after-await escaping as uncaught.
68

79
## v0.5.822 — fix(codegen,runtime): #687 `ClassRef.staticMethod(...)` dispatch — Effect Schema.ts `BigIntFromSelf.pipe(...)` no longer throws `(number).pipe is not a function`. **Symptom (#687 / #321 Effect end-to-end).** At v0.5.815, `import {} from "effect"` from the bare repro crashed deep inside `effect/src/Schema.ts__init` (offset +54448 in the init body) with `TypeError: (number).pipe is not a function`. The throw fired from `js_native_call_method`'s primitive-receiver catch-all because the receiver passed to `js_native_call_method(..., "pipe", ...)` decoded as `0x7FFE_0000_0000_0324` — an INT32-NaN-boxed value whose payload `0x324 = 804` matched class id `BigIntFromSelf`. The class was being used AS a value (its NaN-boxed class id) where an INSTANCE pointer was expected. **Root cause.** Effect's `Schema.ts` declares `export class BigIntFromSelf extends make<bigint>(AST.bigIntKeyword) {}` — the parent is a CALL expression returning an anonymous `class SchemaClass { static pipe() { … } static annotations() { … } … }`. perry's HIR lowering (`crates/perry-hir/src/lower_decl.rs:1576`) only tracks `extends_name` when the super-class is an `Ident` or `Member` — a `CallExpr` super-class lands in the `(None, None, None)` fallback, so `BigIntFromSelf.extends_name = None` and the inheritance chain for static-method dispatch is broken. The call site `BigIntFromSelf.pipe(...)` then lowered to `Expr::Call { callee: Expr::PropertyGet { object: Expr::ClassRef("BigIntFromSelf"), property: "pipe" } }`. Codegen's `has_static_method` check returned `false` (BigIntFromSelf has no OWN static methods; the inherited `pipe` lives on the unnamed parent), so the call fell through to the dynamic-instance-dispatch tower in `lower_call.rs::needs_dynamic_dispatch`. The tower's `js_object_get_class_id(handle=804)` returned 0 (804 < `GC_HEADER_SIZE + 0x1000` early-return guard), no implementor matched, and the default branch invoked `js_native_call_method(<INT32 NaNbox 0x324>, "pipe", …)` — primitive-receiver throw. The earlier #685 fix (defensive skip of `init_static_fields` when init references out-of-scope locals) avoided a different bogus emit but didn't address the "inner-class-hoisting + static-inheritance-through-CallExpr-super-class" architectural gap. **Fix — codegen side (`crates/perry-codegen/src/lower_call.rs::lower_call`).** New arm inserted before the dynamic-instance-dispatch path: when the receiver is `Expr::ClassRef(cls_name)`, walk `cls_name`'s own static methods + `extends_name` chain looking for `property`. If found, emit a direct call to the registered `perry_static_<modprefix>__<class>__<method>` symbol with `js_implicit_this_set(<ClassRef>)` bracketing the call so the method body's `this` references the class (Effect's `static pipe() { return pipeArguments(this, arguments) }` needs the class itself as `this`). If no static method matches across the visible chain (Effect's BigIntFromSelf case — its parent is unnamed so the chain is empty), fall back to lowering the args for side effects and returning the ClassRef itself: chainable `.pipe()`/`.annotations()` in module-init then propagate the class ref forward, keeping init alive past the previously-fatal site. The returned value is NOT semantically equivalent to Effect's transformed schema, but it unblocks `Schema.ts__init` for the #321 DoD repro. **Fix — runtime side (`crates/perry-runtime/src/object.rs::js_native_call_method`).** Parallel defensive: when `js_native_call_method` is reached with an INT32-NaN-boxed receiver whose payload is in `REGISTERED_CLASS_IDS` (the same registry that `js_value_typeof` consults to distinguish class refs from real int32 numerics, populated by `js_register_class_id` at module init), short-circuit and return the receiver. Catches the chained shape that the codegen-side fix misses — `Schema.NonNegative.pipe(int()).annotations({...})` produces a ClassRef out of the first `.pipe()` via the codegen-side defensive return, then the chained `.annotations(...)` reaches the runtime with that ClassRef as the receiver but no longer matches the static `Expr::ClassRef` pattern at codegen time (the receiver is now an `Expr::Call` result). The runtime check uses `REGISTERED_CLASS_IDS.read().contains(&payload)` so only real perry class refs short-circuit; primitive numeric receivers (`(42).pipe is not a function`) still throw. **Linking gotcha hit during validation.** `target/release/libperry_jsruntime.a` bundles its own copy of perry_runtime as an rlib dependency; rebuilding perry-runtime alone via `cargo build -p perry-runtime` updates `target/release/libperry_runtime.a` but the `js_native_call_method` symbol inside libperry_jsruntime.a stays stale, and on macOS the linker takes the first-definition-wins from jsruntime (listed first in `link.rs::link_executable` for the V8 lazy-load workaround). Must rebuild `perry-jsruntime` whenever perry-runtime changes for these symbols. The auto-optimize separately builds `target/perry-auto-<hash>/release/libperry_runtime.a` from source each invocation, but that's listed SECOND on the link line and only fills gaps. **Validation.** `effect-321` bare repro: v0.5.815 crashed at `Schema_ts__init+54448` with `(number).pipe is not a function`; v0.5.822 advances past every `.pipe()` / `.annotations()` site in Schema.ts and now crashes much deeper, inside `perry_closure_node_modules_effect_src_Schema_ts__212+2000` (called from closure 214 from `Schema_ts__init+12272`) with `TypeError: pipe is not a function` — a real-object-receiver throw from the issue-#648 catch-all, indicating a DIFFERENT inner-closure shape whose receiver isn't a ClassRef. That deeper failure is tracked as a #687 follow-up. Gap parity tests: 26/28 PASS (the 2 failures — `test_gap_console_methods`, `test_gap_regexp_advanced` — are pre-existing categorical gaps tracked in CLAUDE.md). No regressions surfaced from the new ClassRef dispatch path on any of the gap suite. **What's left (followups to #687).** The pragmatic chainable-no-op return is a parity-blocker workaround, not a semantic fix: Effect's `pipe()` returns a NEW transformed schema, not the original class. A proper resolution either (a) tracks `extends <CallExpr>` parent-class identity by inspecting the callee function's return statements at HIR lowering (works for `make()` returning `class SchemaClass {…}`; brittle for higher-order factories), or (b) registers each class's static methods in a runtime by-name registry (parallel to `js_register_class_method` for instance methods) and adds a `js_class_static_method_call(class_id, name, args)` dispatcher that walks a runtime parent chain populated by codegen at class-extends-CallExpr evaluation. The chainable-no-op also doesn't preserve `.pipe()` argument side effects beyond the first call's args lowering — fine for module-init constant chains, problematic if the args carry expensive expressions. The new "pipe is not a function" failure at `Schema_ts__212+2000` is the next blocker on the #321 DoD path.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation.
1010

11-
**Current Version:** 0.5.823
11+
**Current Version:** 0.5.824
1212

1313

1414
## TypeScript Parity Status

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ opt-level = "s" # Optimize for size in stdlib
188188
opt-level = 3
189189

190190
[workspace.package]
191-
version = "0.5.823"
191+
version = "0.5.824"
192192
edition = "2021"
193193
license = "MIT"
194194
repository = "https://github.com/PerryTS/perry"

crates/perry-api-manifest/src/entries.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ pub static API_MANIFEST: &[ApiEntry] = &[
250250
method("fastify", "listen", true, None),
251251
method("fastify", "method", true, None),
252252
method("fastify", "url", true, None),
253+
// Manifest-consistency catch-up (release-sweep gate).
254+
method("fastify", "type", true, None),
253255
method("fastify", "params", true, None),
254256
method("fastify", "param", true, None),
255257
method("fastify", "query", true, None),
@@ -387,6 +389,9 @@ pub static API_MANIFEST: &[ApiEntry] = &[
387389
method("better-sqlite3", "all", true, None),
388390
method("better-sqlite3", "exec", true, None),
389391
method("better-sqlite3", "close", true, None),
392+
// Manifest-consistency catch-up (release-sweep gate): NATIVE_MODULE_TABLE
393+
// had a `raw` row that wasn't mirrored here.
394+
method("better-sqlite3", "raw", true, None),
390395
// tursodb (#424). open / exec / execBatch / close /
391396
// lastInsertRowid / isAutocommit shipped in v0.5.543; queryAll /
392397
// queryOne shipped in v0.5.553 (close the row-as-object gap by
@@ -1176,6 +1181,22 @@ pub static API_MANIFEST: &[ApiEntry] = &[
11761181
&[p_any("p0"), p_any("p1")],
11771182
TypeSpec::Void,
11781183
),
1184+
// Manifest-consistency catch-up (release-sweep gate, v0.5.823):
1185+
// NATIVE_MODULE_TABLE accumulated 12 perry/tui entries during the
1186+
// #679 ink-API ergonomics work (v0.5.810) and follow-ups that
1187+
// weren't mirrored here. Restoring drift-free state.
1188+
method_sig("perry/tui", "boxSetPaddingEach", false, None, &[p_any("p0"), p_any("p1"), p_any("p2"), p_any("p3"), p_any("p4")], TypeSpec::Void),
1189+
method_sig("perry/tui", "boxSetFlexShrink", false, None, &[p_any("p0"), p_any("p1")], TypeSpec::Void),
1190+
method_sig("perry/tui", "boxSetFlexBasis", false, None, &[p_any("p0"), p_any("p1")], TypeSpec::Void),
1191+
method_sig("perry/tui", "boxSetFlexBasisPct", false, None, &[p_any("p0"), p_any("p1")], TypeSpec::Void),
1192+
method_sig("perry/tui", "boxSetWidthPct", false, None, &[p_any("p0"), p_any("p1")], TypeSpec::Void),
1193+
method_sig("perry/tui", "boxSetHeightPct", false, None, &[p_any("p0"), p_any("p1")], TypeSpec::Void),
1194+
method_sig("perry/tui", "TextStyled", false, None, &[p_str("p0"), p_str("p1"), p_str("p2"), p_any("p3")], TypeSpec::Any),
1195+
method_sig("perry/tui", "Table", false, None, &[p_any("p0"), p_any("p1"), p_any("p2")], TypeSpec::Any),
1196+
method_sig("perry/tui", "Tabs", false, None, &[p_any("p0"), p_any("p1"), p_any("p2")], TypeSpec::Any),
1197+
method_sig("perry/tui", "InputAt", false, None, &[p_str("p0"), p_any("p1")], TypeSpec::Any),
1198+
method_sig("perry/tui", "AnimatedSpinner", false, None, &[p_any("p0"), p_any("p1")], TypeSpec::Any),
1199+
method_sig("perry/tui", "useStateTuple", false, None, &[p_any("p0")], TypeSpec::Any),
11791200
method_sig("perry/tui", "Spacer", false, None, &[], TypeSpec::Any),
11801201
method_sig(
11811202
"perry/tui",

0 commit comments

Comments
 (0)