You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to v0.5.213 SSO infrastructure. Adds parallel
SHORT_STRING_TAG arms to every == STRING_TAG dispatch in json.rs
stringify paths, plus a test-mode env gate that flips
DirectParser::parse_string_value to emit inline SSO values so the
migration test matrix can exercise every consumer arm.
Added:
- PERRY_SSO_FORCE=1 env-var gate (cached via OnceLock in
sso_emit_enabled() — default OFF, test-only)
- Parallel SHORT_STRING_TAG arms in json.rs:
- stringify_object_inner field-value inline dispatch
- stringify_object_inner replacer block
- stringify_array_depth element inline dispatch
- extract_string_array
- 3 replaced_tag sites on replacer spacer paths
- js_json_stringify_full top-level replacer arm
- spacer tag check (short indent strings)
- js_jsvalue_to_string materializes SSO to heap StringHeader
via js_string_materialize_to_heap
- js_object_get_field_by_name handles `.length` on SSO receivers
directly from the NaN-box length byte
- js_object_get_field_ic_miss routes SSO receivers to by_name
(avoids priming the PIC with SSO bit patterns)
Measured:
- Default mode (SSO-off): 10/10 test_json_*.ts match Node
byte-for-byte — no user-visible regressions
- PERRY_SSO_FORCE=1: 8/10 match. Remaining 2 failures caused by
Step 1.5 (codegen PropertyGet receiver guard filters out SSO
before reaching runtime) — documented in migration plan
Runtime tests 136/136 unchanged.
docs/sso-migration-plan.md updated with new Step 1.5: the codegen
receiver-validity guard at expr.rs:~2647 masks `tag & 0xFFFD` and
checks `== 0x7FFD`. SSO (0x7FF9) fails. Attempted widening to
0xFFF9 accepted SSO but the PIC fast-path's *(obj_handle + 16)
read lands in arbitrary userspace memory for SSO values — verified
crashes 2 tests. Safe fix is a three-way codegen branch routing
SSO directly to the by_name helper. ~2 hours, one site.
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
8
8
9
9
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.
10
10
11
-
**Current Version:** 0.5.213
11
+
**Current Version:** 0.5.214
12
12
13
13
## TypeScript Parity Status
14
14
@@ -148,6 +148,7 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re
148
148
Keep entries to 1-2 lines max. Full details in CHANGELOG.md.
149
149
150
150
- **v0.5.205** — Fix #183: `perry compile --target web` on a real-world app (Bloom Jump built on the Bloom engine) produced a WASM binary the browser refused to load — `Compiling function #687 failed: expected 0 elements on the stack for fallthru, found 103` (count varies with engine state). Root cause in `crates/perry-codegen-wasm/src/emit.rs`: the four direct-`Call`-instruction code paths — `Expr::Call` FuncRef arm (~4302), `Expr::Call` ExternFuncRef arm (~4324), `Expr::New` user-class ctor (~5844), `Expr::SuperCall` parent-ctor (~5894), `Expr::StaticMethodCall` direct-static path (~5979) — each emit `emit_expr(arg)` per source arg and pad up with `TAG_UNDEFINED` when `args.len() < expected`, but had no matching drop-excess branch when `args.len() > expected`. WASM `call` consumes exactly the callee's declared param count, so when JS's "extra args evaluated for side effects, then silently ignored" semantics met Perry's WASM codegen, every extra evaluated arg leaked past the call and accumulated on the enclosing block's operand stack — 103 values by the time `_start`'s final `end` hit the validator. The shape that triggered it in jump/bloom was `bloom/src/core/colors.ts`'s `Colors = new __AnonShape_2(...24 PropertyGets...)` landing on a Phase-3-synthesized ctor with lower declared arity, multiplied across bloom's 10 submodules. Fix: after each existing `for _ in args.len()..expected { I64Const(TAG_UNDEFINED) }` pad-up loop, add the mirror `for _ in expected..args.len() { Drop }` — matches JS semantics (extras evaluated for side effects but discarded) and keeps the operand stack aligned with the callee's WASM type at every direct-Call site. Verified end-to-end against the exact issue repro cloned fresh from `github.com/Bloom-Engine/jump` + `github.com/Bloom-Engine/engine`: both path A `file:./vendor/bloom/` and path B `file:../engine/` now compile to a WebAssembly-validating `.wasm` (416,923 / 413,780 bytes respectively, 140 FFI imports intact, `WebAssembly.compile` resolves clean on node 20+); a synthetic `takesFive(mc(),mc(),1,2,3,4)` minimal case that previously failed `Compiling function #213 failed: ... found 1` also validates. `cargo test --release -p perry-runtime -p perry-hir -p perry-codegen-wasm -p perry`: 262/262 passed. Note: issue #183 also claimed path A found only 1 module and emitted 9 FFI imports — could not reproduce in a fresh clone (both paths find 10 modules identically); most likely an artifact of the reporter's local `vendor/bloom` snapshot predating the `exports` map, and the "runGame silently no-ops" symptom the user actually observed was the browser refusing to instantiate the invalid WASM with the surrounding JS glue swallowing the error — fixed here.
151
+
- **v0.5.214** — SSO Step 1 consumer-arm migration (follow-up to v0.5.213 infrastructure landing). Added `PERRY_SSO_FORCE=1` env-var gate (cached via `OnceLock` in `crates/perry-runtime/src/json.rs::sso_emit_enabled`) that flips `DirectParser::parse_string_value` to emit inline SSO values for strings ≤ 5 bytes — default OFF, used exclusively by the migration test matrix. Added parallel `SHORT_STRING_TAG` arms to every `== STRING_TAG` dispatch in `json.rs` stringify paths: `stringify_object_inner` field-value inline dispatch + replacer block, `stringify_array_depth` element inline dispatch, `extract_string_array`, the 3 `replaced_tag` sites on the replacer spacer paths, `js_json_stringify_full` top-level replacer arm, and the spacer-as-string check (for `JSON.stringify(obj, null, " ")` with short indent). Runtime additions: `js_jsvalue_to_string` now materializes SSO to a heap `StringHeader` via `js_string_materialize_to_heap` for the common "caller needs `*mut StringHeader`" contract; `js_object_get_field_by_name` handles `.length` on an SSO receiver by reading the length byte directly from the NaN-box payload (returns `JSValue::undefined()` for other keys on SSO values, matching the string-property baseline). Measured: 8 out of 10 `test_json_*.ts` tests match Node byte-for-byte under `PERRY_SSO_FORCE=1`; all 10 match under default (SSO-off) mode — no user-visible regressions from the infrastructure landing. Remaining 2 failures are both caused by **Step 1.5** (new section in `docs/sso-migration-plan.md`): the codegen's PropertyGet receiver-validity guard at `crates/perry-codegen/src/expr.rs:~2647` masks `tag & 0xFFFD` and checks `== 0x7FFD`, which accepts POINTER_TAG + STRING_TAG but rejects SHORT_STRING_TAG (0x7FF9). SSO receivers fall to the "invalid" branch → return `undefined`. Widening the mask to `0xFFF9` accepts SSO but the PIC fast path's subsequent `*(obj_handle + 16)` read lands in arbitrary userspace memory for SSO values (the low 48 bits are SSO data, not a heap pointer) — verified: widening without further guarding crashed 2 tests under SSO_FORCE. Safe fix is a three-way codegen branch: POINTER/STRING → PIC fast path, SSO → call `js_object_get_field_by_name_f64` directly skipping the PIC memory read, else → invalid. Estimated ~2 hours, one codegen site — scheduled as Step 1.5. Runtime tests 136/136 (unchanged — no new unit tests added this commit). Also verified: no infrastructure crashes on stringify / equality / comparison / typeof / length paths when SSO values do reach them from the runtime side.
151
152
- **v0.5.213** — Small String Optimization (SSO) infrastructure (tier 1 #2 per `docs/memory-perf-roadmap.md`). **Infrastructure-only landing**; no creation sites migrated yet. New tag `SHORT_STRING_TAG = 0x7FF9_0000_0000_0000` encoding strings of length 0..=5 inline in the 48-bit NaN-box payload (8-bit length at bits 40..47 + 5 bytes of data at bits 0..39). Zero heap allocation for short strings when emitted — the value IS the data. Added: `JSValue::try_short_string(&[u8])` (constructor), `short_string_to_buf` / `short_string_len` (decoders), `is_short_string` / `is_any_string` (predicates, with `is_string` kept strict for legacy call sites that rely on `as_string_ptr` returning a real heap pointer), `js_string_new_sso(ptr, len) -> f64` (SSO-aware creation that falls back to heap for long inputs), `str_bytes_from_jsvalue(value, &mut scratch)` (central decoder producing `(*const u8, u32)` view for either representation), `js_string_materialize_to_heap(value)` (compatibility shim that allocates a heap StringHeader from an SSO value). Consumer-side dispatch already wired in: `typeof` (builtins.rs, accepts both tags), `js_jsvalue_equals` + `js_jsvalue_compare` (value.rs — SSO fast path when both operands are SSO because encoding is canonical, otherwise decode via scratch buffers and byte-compare), `js_value_length_f64` (direct bit extraction for SSO, no heap access), `js_jsvalue_to_string` (materializes SSO to heap when caller needs `*mut StringHeader`), three stringify arms in json.rs (top-level `stringify_value`, object field inline dispatch in `stringify_object_inner`, array element inline dispatch in `stringify_array_depth`). 6 new unit tests in `value::tests` cover roundtrip, rejection of 6+ byte inputs, embedded-NUL handling (length is authoritative), tag-band distinctness from POINTER/INT32/NUMBER/UNDEFINED, empty-string roundtrip, and byte-order stability (first byte lands in LSB of payload — invariant relied on by any future SIMD bulk-decoder). **Why infrastructure-only:** flipping `DirectParser::parse_string_value` to emit SSO without first auditing every consumer produces regressions — `grep "== STRING_TAG" crates/perry-runtime/src/json.rs` alone shows 20+ sites, and the broader consumer surface spans object.rs property-get helpers, string.rs methods (split/replace/slice/indexOf/etc.), regex.rs match extractors, set.rs/map.rs key equality, stdlib HTTP/DB paths, and codegen string-literal emission. Attempting the flip in-session reproduced the hazard: 3 `test_json_lazy_*.ts` tests diffed from Node with stringify emitting `"null"` where SSO values should have decoded. Rolled back the producer flip; kept every consumer arm already added so Step 1 of the migration is ~50% complete. New doc `docs/sso-migration-plan.md` sequences the 6-step roll-out (stringify consumers → DirectParser emit → object key storage → string methods → codegen literals → stdlib) with per-step ship criteria and a decision gate after Step 2 to re-evaluate whether Steps 3-6 are worth the effort vs jumping to tier 2/3 (escape analysis + generational GC). Runtime tests 130 → 136 (added 6 SSO tests). All 10 existing `test_json_*` regressions green under infrastructure-only landing.
152
153
- **v0.5.212** — Three lazy-correctness fixes + comprehensive external-auditor document. (1) `Array.isArray(parsed)` returned `false` on lazy `JSON.parse` results. Root cause: `Expr::ArrayIsArray` in `crates/perry-codegen/src/expr.rs` was a pure compile-time check that emitted `TAG_FALSE` whenever the operand's static HIR type wasn't definitively `Array`/`Tuple`. For `JSON.parse` results (typed `any`) this meant the static check always failed, so even a runtime array always returned false. Fixed by routing indeterminate static types (`Any`, `Unknown`, no annotation) through a new `js_array_is_array` runtime dispatch in the same codegen arm; the fast-path for definitively-array statics still emits `TAG_TRUE` without a runtime call, and definitively-scalar types (Number/String/Boolean/Null/Void/BigInt/Symbol) still emit `TAG_FALSE` statically. (2) `parsed instanceof Array` returned `false` on lazy arrays. Root cause: `js_instanceof` in `crates/perry-runtime/src/object.rs:2864-2890` (the `CLASS_ID_ARRAY` branch) only matched `GC_TYPE_ARRAY`, not `GC_TYPE_LAZY_ARRAY`. Added the lazy-array obj_type to the OR. (3) `Array.isArray` runtime (`crates/perry-runtime/src/array.rs:js_array_is_array`) already had the same eager-only check; extended to accept `GC_TYPE_LAZY_ARRAY` too (now reachable via the new codegen dispatch path). Net effect: every reasonable type-predicate against a lazy array now matches Node byte-for-byte. Pre-existing limitation `parsed.constructor === Array` (returns false on Perry regardless of lazy vs eager — it's a property-lookup limitation in Perry's class system) remains unchanged. The `Array.isArray` codegen change is also a bug-fix for ANY `any`-typed value — before v0.5.212 `Array.isArray(anyTyped)` returned false even when the runtime value was a real eager array; pre-existing latent bug unrelated to lazy work. Verified: `test_json_lazy_per_element.ts` and the new `test_json_lazy_predicates.ts` match Node; all other `test_json_*` regressions clean. New document `docs/audit-lazy-json.md` — **~1400-line comprehensive external-auditor reference** covering the tape-based lazy JSON system end-to-end: layout (TapeEntry, LazyArrayHeader offset invariants, arena layout), dispatch (every user-observable array operation mapped to its runtime entry point), garbage collection (trace_lazy_array, cache zero-on-alloc invariant, tracer safety), correctness proof (14-row case analysis against every JS array operation), performance characteristics (measured numbers vs Node + Bun on all three main benches), edge cases (15 distinct cases with handling), test coverage (every file + gate), known limitations, and a 20-item audit checklist with file:line references. Written so a reviewer with no prior Perry knowledge can verify the entire system against source.
153
154
- **v0.5.211** — Fix pre-existing linker regression from v0.5.204: `js_json_stringify` in `crates/perry-runtime/src/json.rs` was missing `#[no_mangle]`. Rust compiled it as a mangled symbol (`__ZN13perry_runtime4json17js_json_stringify17h…E`) which `perry-stdlib::fastify::context::jsvalue_to_json_string` (and `perry-stdlib::fastify::server::build_response_body`) couldn't find when linking against a statically-built `libperry_runtime.a` — surfaced as `ld: Undefined symbols: _js_json_stringify` when running `scripts/run_fastify_tests.sh` or `cargo test -p perry-stdlib`. Tracking down via `git log -L "/pub unsafe extern \"C\" fn js_json_stringify(/,/^}/"` identified v0.5.204 (`feat(json): lazy parse + lazy stringify`) as the commit that inadvertently removed the attribute when inserting `try_stringify_lazy_array` directly above. All 7 fastify integration tests now pass (GET /hello, GET /users/:id, POST /echo status + body, GET /does-not-exist → 404) — this was blocking end-to-end Fastify coverage since v0.5.204. One-line fix. Also: comprehensive test sweep at v0.5.211: `cargo test --release --workspace` with CI-matching exclusions = **44/44 test runs, 0 failed**, gap tests 24/28 (unchanged), parity tests 106 pass / 12 fail (same 12 as pre-v0.5.208 baseline — no regressions), thread tests 4/4, cache tests PASS, fastify tests 5/5 (newly unblocked), doc tests 95/115 (7 tvos-simulator cross-compile failures are env-dependent — `libperry_ui_tvos.a` built for macOS host not tvOS-sim; pre-existing, unrelated to JSON work).
0 commit comments