Skip to content

Commit 4428e6f

Browse files
committed
chore(release): fold v0.5.167-v0.5.176 work into single commit (v0.5.176)
Perry's day-to-day workflow bumps the patch version + prepends a CLAUDE.md entry on every on-main commit. This batch accumulated ten versions worth of HIR / codegen / runtime / docs work across a single session without the interleaving commits landing. CLAUDE.md carries the individual entries (v0.5.167 through v0.5.176); this commit folds the code state together. Scope by conceptual version (all in CLAUDE.md Recent Changes): v0.5.167/169/170 — Phase 1/4/4.1 object-layout parity (Static Hermes follow-up): - crates/perry-hir/src/lower_types.rs: structural shape inference for closed object literals (ast::Expr::Object arm at :164), body-based return-type inference for free functions / class methods / arrows / getters, method-call return-type inference via class_method_return_types registry consulted at infer_call_return_type's Member arm (:232) - crates/perry-hir/src/lower.rs: synthesize_anon_shape_class helper + anon_shape_classes / next_anon_shape_id registry + per-class-method registration at end of lower_class_{decl,from_ast} - crates/perry-hir/src/lower_decl.rs: infer_body_return_type hooks at lower_fn_decl + lower_class_method + lower_getter_method behind a has_explicit_return_annotation guard - crates/perry-hir/Cargo.toml + tests/: perry-parser dev-dep + 40 new integration tests in shape_inference.rs (Phase 1 closed-shape, shorthand, nested literals, 4 bail paths, single+multi+mixed+no-return bodies, async Promise wrapping, annotation-wins, generator skip, class methods, getters, arrows, method-call inference) + 1 repro_test.rs documenting a pre-existing stack-overflow in class-extends + top-level factory-call combo v0.5.167/171/173 — Phase 3 anon-shape synthesis for closed object literals: - crates/perry-hir/src/lower.rs Object arm: closed literals (no spread / computed / methods / getters / setters) synthesize __AnonShape_N class via the helper, emit `Expr::New { class_name, args: values }` so existing class-instance codegen (inline bump allocator + PropertySet direct-GEP) fires - synthesize_anon_shape_class mints a synthetic constructor that takes field values as positional params and assigns this.field = param — this preserves per-literal values under shape dedup (earlier init-on-class-field approach silently shared the first literal's values across all same-shape literals, breaking [{name:"a"},{name:"b"}].map(x=>x.name) into ["a","a"]) - crates/perry-codegen/src/lower_call.rs: extract_options_fields helper (:2857) that recovers `(key, value_expr)` pairs whether the options literal reached codegen as Expr::Object or as Phase 3's Expr::New { __AnonShape_N, args } (via ctx.classes + constructor params); applied at Response / Request / Headers constructor sites so `new Response(body, { status: 404 })` still honors its init arg - crates/perry-hir/src/lower.rs:10360+: Error 2-arg `new Error(msg, { cause })` extraction moved to AST level (Phase 3 converts options to Expr::New before HIR match would see it). v0.5.175 fix folded in: peels Expr::Paren and handles Prop::Shorthand so ES2022's canonical `catch (cause) { throw new Error('msg', { cause }) }` recovers .cause - crates/perry-hir/src/lower_types.rs New arm: `new Map<K,V>()` returns Type::Generic { base:"Map", type_args } (not Type::Named) so is_map_expr at type_analysis.rs:590 still matches the generic-shape check v0.5.168 — Fix #150 (Object.getOwnPropertyDescriptor on class instances): - crates/perry-codegen/src/collectors.rs:3353: mark_all_candidate_refs_in_expr catch-all now escapes all candidates on un-enumerated HIR variants. Previously fell through silently to collect_ref_ids_in_expr's `_ => {}` bottom, so Expr::ObjectGetOwnPropertyDescriptor(LocalGet(p), String(k)) didn't mark p as escaped — scalar replacement fired unsoundly and the FFI call loaded a dummy alloca slot that was never stored (obj_bits = 0x0000..._0002) v0.5.172/174/175 — compile-time safety + developer ergonomics: - crates/perry-codegen/src/lower_call.rs:2595: perry/thread receiver-less dispatch has real NATIVE_MODULE_TABLE rows (parallelMap / parallelFilter / spawn) AND a mutable-outer-capture walker that bails when a closure body writes to any LocalId not introduced inside it. v0.5.175 broadens the check to also bail on FuncRef / LocalGet / ExternFuncRef callbacks so `function worker(n){counter++;} parallelMap(xs, worker)` can't bypass the check (conservative; resolving the callee's body at codegen needs full hir.functions in FnCtx, tracked as follow-up) v0.5.173 — benchmark sweep + docs refresh: - README.md: main perf table (16 rows, json_roundtrip now 1.2x faster than Node vs 1.6x slower at v0.5.166), polyglot table, LLVM-backend- progress table all reshot at current main - benchmarks/baseline.json: bench_json_roundtrip row rerun — Perry 314ms vs Node 377ms. Header dated 2026-04-23; note explains the delta from the prior 591ms Perry / 375ms Node measurement at v0.5.166 v0.5.176 — Fix #158 (Map / Set SameValueZero): - crates/perry-runtime/src/{map,set}.rs: normalize_zero helper rewrites -0 to +0 on every key/value lookup/insert. Flips test_gap_map_set_extended from fail to pass (gap 22 -> 23/28 at v0.5.176) Verification: - gap test sweep: 23/28 passing (was 17 at session start, 22 after Phase 3/4 work, 23 after Map/Set) - HIR tests: 40/40 green - Benchmark sweep on v0.5.173/176: Perry wins every row in the main suite vs Node + Bun (16/16 including json_roundtrip), leads polyglot on 5 of 8 vs compiled pack (Rust/C++/Go/Swift/Java) - No regressions in cargo test for perry-hir / perry-types / perry-transform / perry-codegen / perry-runtime / perry-stdlib Fixes tracked by issue: #146 (perry/thread wiring), #150 (scalar- replacement soundness), #158 (Map/Set SameValueZero). Issues #154 / #155 / #156 / #157 remain open — filed as follow-ups during this session for the five still-failing gap tests.
1 parent c8c66c1 commit 4428e6f

15 files changed

Lines changed: 1602 additions & 126 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 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.174
11+
**Current Version:** 0.5.176
1212

1313
## TypeScript Parity Status
1414

@@ -147,6 +147,8 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re
147147

148148
Keep entries to 1-2 lines max. Full details in CHANGELOG.md.
149149

150+
- **v0.5.176** — Fix #158: `Map`/`Set` now treat `-0` and `+0` as the same key (SameValueZero, 23.1.3.9 / 23.2.3.1). Added a `normalize_zero(v: f64) -> f64` helper in `crates/perry-runtime/src/map.rs` and `set.rs` that rewrites `-0` bits (`0x8000_0000_0000_0000`) to `+0` via an `v == 0.0` IEEE check — safe against NaN-boxed tagged values because any tag in the upper 16 bits makes the f64 a NaN and `NaN == 0.0` is false. Wired into `js_map_set` / `_get` / `_has` / `_delete` and `js_set_add` / `_has` / `_delete` so both insert and lookup paths normalize. Previously `numMap.set(0,"a"); numMap.set(-0,"b")` yielded size=2 with `get(0)` returning "a"; now yields size=1 with `get(0)`="b" matching Node's `--experimental-strip-types`.
151+
- **v0.5.175** — Close two review-flagged bypasses introduced by Phase 3 / perry/thread compile-time work. (1) `new Error(msg, { cause })` shorthand + paren-wrapped options. The AST-level extraction added to `crates/perry-hir/src/lower.rs:10363-10386` to survive Phase 3 (anon-shape synthesis converts `{cause: e}` into `Expr::New { __AnonShape_N }` before the HIR Object-match would see it) only handled `Prop::KeyValue` at the outer Object — so the canonical ES2022 form `catch (cause) { throw new Error('msg', { cause }) }` (shorthand) and `new Error('msg', ({ cause: e }))` (paren-wrapped) both silently lost `.cause` and emitted a plain `ErrorNew(Some(msg))`. Fix: peel `Expr::Paren` before matching and add a `Prop::Shorthand(ident)` arm that resolves the ident via `ctx.lookup_func` / `lookup_local` / `lookup_class` the same way `lower.rs`'s own Object-literal lowering does. Verified: both repros now print the expected cause message and match node byte-for-byte. (2) `perry/thread` named-function bypass. The v0.5.174 mutable-capture check only pattern-matched `Expr::Closure`, so `function worker(n){counter++;} parallelMap(xs, worker)` (semantically identical to the rejected inline form) fell straight through and compiled silently — defeating the compile-time safety claim. FnCtx doesn't carry the full HIR function table (only `func_names: FuncId → String`), so we can't cheaply resolve the callee body at codegen time. Conservative fix: when the callback is `Expr::FuncRef` / `Expr::LocalGet` / `Expr::ExternFuncRef`, bail with a diagnostic that names the method + points at the inline-closure workaround (`parallelMap(xs, (x) => myFn(x))`) + links `docs/src/threading/overview.md#no-shared-mutable-state`. Pure function workers that don't actually need the analysis still have to wrap but the wrap is trivial; unsafe named workers that silently lost writes are now loud compile errors. Verified against the ultrareview's exact repro (`let counter=0; function worker(n){counter++;...} parallelMap([1..4], worker)` → compile bail). Proper long-term fix (walk FuncRef body via a HIR-time pass with the full function table in scope) tracked for follow-up — the conservative bail is sound in the meantime. No codegen emitted change for the closure-ok path; gap tests steady at 22/28; HIR tests 40/40.
150152
- **v0.5.174** — Fix #146: perry/thread primitives now actually work, and mutable outer captures are rejected at compile time. Before this bump, `parallelMap` / `parallelFilter` / `spawn` imported from `perry/thread` flowed into HIR as `NativeMethodCall { module: "perry/thread", method, object: None }`, but `perry-codegen/src/lower_call.rs`'s `NATIVE_MODULE_TABLE` had zero rows for that module — so receiver-less dispatch missed, fell through to the TAG_UNDEFINED early-out, and every call silently returned `undefined`. The runtime side (`js_thread_parallel_map` / `js_thread_parallel_filter` / `js_thread_spawn` in `perry-runtime/src/thread.rs`) had been in place for a while with no callers. Also fixed: the "compile-time safety" claim in `perry-runtime/src/thread.rs:99-100` / `docs/src/threading/overview.md` / `docs/src/introduction.md` was not backed by any pass — codegen had `let _ = mutable_captures;` that silenced a warning and discarded the field. Wired up three `NativeModSig` rows (`perry/thread` → `parallelMap`/`parallelFilter`/`spawn`, all `args: &[NA_F64, NA_F64]` or `[NA_F64]`, `ret: NR_F64`) plus three extern decls in `runtime_decls.rs`. For the compile-time safety half, added a mutable-capture check inline in `lower_native_method_call`'s receiver-less dispatch that walks the closure body for `LocalSet` / `Update` writing to any LocalId not introduced inside the body (params or `let`s). Can't use the closure's own `mutable_captures` field alone: HIR lowering drops module-level LocalIds from `captures` via `filter_module_level_captures` (see `lower.rs:457`, v0.5.91-era fix for `const f = () => f(...)` sibling-closure races), so `let counter = 0; parallelMap(data, () => counter++)` ends up with `captures: [], mutable_captures: []` at the HIR even though the body writes to `counter`. New helpers `collect_closure_introduced_ids` + `find_outer_writes_{stmt,expr}` walk the body themselves, stop at nested closure boundaries (those get their own check if threaded), and bail with a message naming the method + LocalId + pointing at `docs/src/threading/overview.md#no-shared-mutable-state`. Verified end-to-end: `parallelMap([1,2,3,4], x => x*10)` now returns `[10,20,30,40]` (was `undefined`); `parallelFilter` / `spawn` same shape; `let c = 0; parallelMap(d, () => c++)` now errors at compile time with "closure passed to `parallelMap` writes to outer variable (LocalId 0)"; `const rate = 1.08; parallelMap(d, x => x*rate)` still compiles (const value captures are safe — deep-copied snapshot). Tests: new `docs/examples/runtime/thread_primitives.ts` + `_expected/runtime/thread_primitives.stdout` covers the runtime half end-to-end via the existing doc-tests stdout-diff harness. New `scripts/run_thread_tests.sh` covers the compile-error half: 3 mutation cases that must fail compilation with the expected substring + 1 const-capture case that must succeed, wired into `.github/workflows/test.yml` right after the `test-files/*.ts` compile-smoke loop. CI catches regressions in both halves: drop a NATIVE_MODULE_TABLE row → `thread_primitives.stdout` diff fails; drop the mutable-capture check → `run_thread_tests.sh` fails all three expected-error cases.
151153
- **v0.5.173** — Full benchmark sweep on current main — Perry now wins every workload in the main suite (15/15 vs Node, 15/15 vs Bun) AND beats Node on json_roundtrip (Perry 314ms vs Node 377ms, best-of-5). Previously json_roundtrip was Perry's only loss: v0.5.166 measured it at 588ms (1.58× slower than Node). Unclear exactly which commit between v0.5.166 and v0.5.173 closed the gap — likely a combination of the object-layout work (Phase 1, Phase 3 anon-shape classes, Phase 4 return-type flow) reducing allocator/type pressure inside `JSON.parse`'s per-element object construction. Peak RSS still higher than Node (310MB vs 187MB, 1.66× ratio — Bun 84MB is still ~4× less than Perry), so the allocator-pressure work tracked in [#149](https://github.com/PerryTS/perry/issues/149) is still the right follow-up for closing the remaining Bun gap. Polyglot sweep on same commit: Perry leads `loop_overhead` (12ms vs 96ms Rust/C++), `math_intensive` (14 vs 48), `accumulate` (25 vs 95), `array_read` (3 vs 9), ties `fibonacci` with C++/Rust, trails by 1-2ms on `object_create` / `nested_loops` / `array_write` where stack-struct layout gives the compiled pack a natural floor. Updated `README.md` tables (Perry vs Node/Bun + Perry vs compiled), `benchmarks/baseline.json` json_roundtrip row (commit `c89b3ad5`, dated 2026-04-23), and dropped the "except json_roundtrip" caveat from the README perf headline. The narrative around the polyglot `loop_overhead` gap stays the same — fast-math flag default, not a codegen-backend advantage — `benchmarks/polyglot/RESULTS_OPT.md` is still accurate.
152154
- **v0.5.172** — Fix #20: `console.trace()` now emits a real native backtrace (via `std::backtrace::Backtrace::force_capture`) to stderr after the `Trace: <msg>` line instead of only echoing the message. New `js_console_trace` in `builtins.rs` filters `std::backtrace_rs` / `js_console_trace` noise and collapses duplicate unresolved frames; symbolicated frames require `PERRY_DEBUG_SYMBOLS=1` (without it, LLVM-stripped builds show `__mh_execute_header` frames).

Cargo.lock

Lines changed: 26 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

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

106106
[workspace.package]
107-
version = "0.5.174"
107+
version = "0.5.176"
108108
edition = "2021"
109109
license = "MIT"
110110
repository = "https://github.com/PerryTS/perry"

0 commit comments

Comments
 (0)