Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
547 changes: 547 additions & 0 deletions .github/workflows/container-tests.yml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

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.

**Current Version:** 0.5.369
**Current Version:** 0.5.370

## TypeScript Parity Status

Expand Down Expand Up @@ -149,5 +149,6 @@ First-resolved directory cached in `compile_package_dirs`; subsequent imports re

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

- **v0.5.370** — End-to-end shake-out of `perry/container` + `perry/compose` driven by running [example-code/forgejo-deployment](example-code/forgejo-deployment/main.ts) against live Docker. Surfaced and fixed six interlocking codegen + FFI + orchestration bugs that together blocked any non-trivial compose stack from running. (1) **`composeUp({...})` with object literal failed at JSON parse** — the codegen `NativeArgKind::StrPtr` arm called `js_get_string_pointer_unified(arg)` which for object operands returned the raw object pointer; the FFI then read it as `StringHeader` and `serde_json` produced "expected value at line 1 column 1". New runtime helper `js_value_to_str_ptr_for_ffi` (in `crates/perry-runtime/src/value.rs`) returns the heap string pointer for actual strings/SSO and otherwise routes through `js_json_stringify` to auto-encode object/array/number/bool args. Codegen `StrPtr` arm in `crates/perry-codegen/src/lower_call.rs` now calls the new helper instead. Closes the headline ergonomic gap: `composeUp({ services: {...} })` now Just Works with a TS object literal. (2) **`getBackend()` returned `"unknown"`** when called before any async FFI — the backend `OnceLock` was empty. Updated `js_container_getBackend` to perform a synchronous in-place probe (`block_in_place` inside a tokio worker, fresh `current_thread` runtime otherwise) so the live name appears even at module top-level. (3) **`composeUp` Promise resolved with `5e-324` subnormal** — the async-bridge stored bare u64 handles in the resolution slot which then decoded as f64 bits. Two fixes: (a) `handle_to_promise_bits(id)` NaN-boxes with `POINTER_TAG | (id & POINTER_MASK)` so subsequent `unbox_to_i64` recovers the id verbatim and template-string interpolation no longer prints "0"; (b) `Ok(0u64)` void resolutions become `PROMISE_VOID_BITS = TAG_UNDEFINED` so they read as `undefined`. Swept across 23 call sites in `crates/perry-stdlib/src/container/mod.rs`. (4) **`down(stack, opts)` failed with "Invalid compose handle"** — the codegen dispatch `args: &[NA_F64, NA_F64]` for `js_compose_down` lowered both args to LLVM `double`, but the Rust signatures took `(handle_id: i64, volumes: i32)` — calling-convention mismatch (f64 in XMM0, Rust reads RDI). Changed every compose handle-arg FFI signature to `(handle: f64, ...)` and added `handle_id_from_f64(boxed)` that masks `POINTER_TAG` off, mirroring the codegen contract. Touches `js_container_compose_down`/`_ps`/`_logs`/`_exec`/`_config`/`_start`/`_stop`/`_restart` and their `js_compose_*` aliases. `tail: i32` likewise → `f64` with `is_finite() && >= 0` gate; `volumes: i32` → `f64` with `!= 0.0`. (5) **`exec(stack, 'postgres', cmd)` failed with "No such container"** — `service::service_container_name` regenerates a fresh `{md5_8}-{random_hex8}` name on every call (per its own `test_service_container_name_stability` assertion), so post-`up` operations could never find the container. Added a `service_container_names: Mutex<HashMap<String, String>>` cache to `ComposeEngine` populated by `up()` during the start loop; `down`/`ps`/`exec`/`logs`/`start`/`stop` now read through `resolve_container_name(svc)` instead of regenerating. (6) **`${FORGEJO_DB_USER:-forgejo}` env interpolation didn't apply to TS-side specs** — postgres bombed with `FATAL: invalid character in extension owner: must not contain any of "$..."` because the literal placeholder string flowed straight through. The yaml interpolation engine in `crates/perry-container-compose/src/yaml.rs::interpolate` was YAML-only; wired into `parse_compose_spec` (in `crates/perry-stdlib/src/container/types.rs`) so `${VAR}` and `${VAR:-default}` get expanded against `std::env::vars()` BEFORE `serde_json::from_str`, matching SPEC §7.8 / §7.9 — same engine, applied at the FFI boundary. **Verified end-to-end**: a fresh `forgejo-deployment` run on Docker creates the network + 3 named volumes + postgres + gitea containers, polls `pg_isready` until accepting connections (succeeds within ~7s on a clean volume), prints the "Forgejo Stack is Ready!" banner with web UI + SSH URLs, and the SIGINT handler tears down + removes volumes. The forgejo image stayed on `gitea/gitea:1.23` (the upstream Forgejo forked from) since codeberg.org's registry intermittently returns `unauthorized: reqPackageAccess` for public pulls; env-var keys updated to the matching `GITEA__*` form (Forgejo's image accepts both during the migration window). Example refreshed to use the canonical standalone-function API per SPEC §C4 (`up(spec)`, `down(handle, opts)`, `exec(handle, svc, cmd)` — method-chain `stack.down()` was sugar that required a TS-library wrapper that doesn't exist). New `tsconfig.json` with `paths: { "perry/*": ["../../types/perry/*"] }` so IDE typechecking finds the workspace types. **Workspace re-registration**: had to re-add `perry-container-compose` to `[workspace] members` + `default-members` + `[workspace.dependencies]` after the intentional Cargo.toml edit removed them — `perry-stdlib`'s `optional` dep declaration requires the entry, build can't succeed without it.
- **v0.5.368** — Closes #248: codegen for `arr.push(...src)` and the V8 / perry-jsruntime interop expression family. **Phase 1 (ArrayPushSpread)**: `arr.push(...src)` was rejected by the LLVM backend with `expression ArrayPushSpread not yet supported`. HIR has lowered the variant since v0.5.x (`crates/perry-hir/src/lower/expr_call.rs:2077`); only the codegen arm in `crates/perry-codegen/src/expr.rs` was missing — WASM (`crates/perry-codegen-wasm/src/emit.rs:5441`) and analysis helpers (`collectors.rs:1218`, `walker.rs:786`, `analysis.rs:34`) all knew about it. Fix is a single new arm mirroring `Expr::ArrayPush` at line 3016 (same three receiver storage cases — LocalGet / boxed-captured / plain — and the same realloc-aware writeback). No new runtime helper needed: `js_array_concat(dst, src)` already existed at `crates/perry-runtime/src/array.rs:1011`, comment-as-spec'd "reserved for the internal push-spread desugaring path"; Set sources work transparently via the SET_REGISTRY check inside `js_array_concat`. **Phase 2 (V8 interop, 8 new arms)**: the LLVM backend bailed for **JsLoadModule, JsGetExport, JsCallFunction, JsCallMethod, JsGetProperty, JsSetProperty, JsNew, JsNewFromHandle** — the HIR family `perry-hir/src/js_transform.rs::transform_js_imports` produces whenever a `.ts` entry imports from a `.js` module the resolver classifies as JS-runtime-loaded (`crates/perry/src/commands/compile/collect_modules.rs:73`, extension-driven). Pre-fix the user's `bun i @codehz/pipeline` repro bombed at codegen with `JsCallFunction not yet supported`. New arms call into the existing perry-jsruntime FFI surface: `js_load_module(path_ptr, path_len) -> u64`, `js_get_export/get_property/set_property/call_function/call_method/new_instance/new_from_handle` etc. — all eight already declared in `runtime_decls.rs` except `js_call_method` (added here at line 1631 with signature `DOUBLE, &[DOUBLE, I64, I64, I64, I64]`). New shared helper `lower_js_args_array(ctx, lowered_args) -> (ptr, len)` marshals already-lowered NaN-boxed args into a stack alloca'd `[N x double]` via the issue-#167 `alloca_entry_array` pattern (hoisted to function entry block); empty input returns `("null", "0")` for the FFI's null-pointer fallback. **Module handle representation**: V8 module ids are u64; codegen returns them as f64 via `bitcast_i64_to_double` to fit `lower_expr`'s return-type contract, then consumers bitcast back to i64 before passing to the runtime. **JS value handles** are NaN-boxed f64 with V8-handle tag 0x7FFB — handled internally by perry-jsruntime's `v8_to_native` / `native_to_v8` helpers; codegen treats them as opaque doubles. **Runtime bootstrap**: new `needs_js_runtime: bool` field on `CrossModuleCtx` (threaded from `CompileOptions::needs_js_runtime`, originally set in `collect_modules.rs:105` when any `.js` module enters `ctx.js_modules`), wired into `compile_module_entry` so the entry main's prelude calls `js_runtime_init()` between `js_gc_init` and user code. Without this, every `js_load_module` site bailed at the runtime with `[js_load_module] no JS runtime state!`. **`JsCreateCallback` deliberately deferred** to Phase 2B: the runtime FFI `js_create_callback(func_ptr, closure_env, param_count)` expects `func_ptr` to have signature `(closure_env: i64, args_ptr: *const f64, args_len: i64) -> f64` (see the `native_callback_trampoline` in `crates/perry-jsruntime/src/interop.rs:993`), but Perry closure bodies have `(closure_ptr, arg0, arg1, ...)` per arity — there's no direct call-compatible mapping. Wiring this needs either codegen-emitted per-arity adapter thunks or a runtime-side closure-array dispatcher; for now the arm bails with a clear message so users see exactly what's blocked. The user's exact `pipeline()` repro at `/tmp/issue248/test.ts` now compiles + links + runs (exit 0). **Regression tests**: `test-files/test_issue_248_array_push_spread.ts` (10 cases — number/string/object arrays, empty src/dst, array-literal spread, chained push-spread, post-spread `.indexOf` + `.length`, push-spread inside loop forcing realloc past the 16-cap, mixed `push` + `push-spread` — all byte-for-byte against `node --experimental-strip-types`), plus `test-files/test_issue_248_phase2_js_interop.ts` + fixture `test-files/fixtures/issue_248_jsmod.js` exercising JsLoadModule + JsCallFunction (compile + link + clean exit). **Verified**: cargo build --release -p perry-runtime -p perry-stdlib -p perry-jsruntime -p perry clean; cargo test --release -p perry-codegen --lib 22/0; gap tests 25/28 = baseline. Bumped 0.5.366 → 0.5.368 above origin's parallel-track 0.5.366 (HarmonyOS SDK fix #250) + 0.5.367 (HarmonyOS HAP bundler #252) per the merge-collision precedent. PR #251.
- **v0.5.369** — HarmonyOS PR B.4 + B.5 + B.6 squash-equivalent: cherry-picks `3042563a` + `b01653f6` + `41d597c0` (originally v0.5.127 / v0.5.128 / v0.5.129) from the `harmony-os` branch — the audit-driven fixes the original branch made AFTER its own first emulator run. End-to-end `hdc install` is now achievable (modulo cert + bundle-name match). **B.5 (v0.5.128) — DevEco 6.x SDK + ets-loader replacement**: most of B.5's compile.rs work already on main via the v0.5.366 fast-follow (DevEco app-bundle SDK probe + macOS framework leak fix); cherry-pick fold-in here is just the NEW hunks: (a) extends the `is_harmonyos` linker arm in `compile/link.rs` with OHOS runtime libs `-Wl,--allow-multiple-definition -lm -lpthread -ldl -lace_napi.z`. `libace_napi.z.so` is what ArkTS exposes for `napi_module_register` / `napi_create_*` (consumed by `perry-runtime/src/ohos_napi.rs`); OHOS naming convention is `<name>.z.so` and `-l` strips `lib`+`.so` but NOT the middle `.z`, so `-lace_napi.z` is the deliberate spelling. (b) Skip BSD strip on harmonyos targets — macOS strip emits a noisy `non-object and non-archive file` warning on ELF binaries. (c) `crates/perry/src/commands/harmonyos_hap.rs` rewritten to skip the ets-loader Node/rollup pipeline entirely and shell out to `es2abc --extension ts --module --merge-abc` directly — the harmony-os branch found ets-loader needs ~15 env vars (aceModuleRoot, aceModuleBuild, aceModuleJsonPath, aceProfilePath, compileMode=moduleJson, plus a full DevEco build-profile.json5); synthesizing all of that is effectively re-implementing hvigor. The Phase-1 ArkTS shim is plain TypeScript (no `@Entry`/`@Component`/`struct` decorators yet) so es2abc accepts it via the `--extension ts` flag. HAPs now ship a single merged `ets/modules.abc` instead of per-file .abc. PR C reintroduces ets-loader once the TS→ArkUI emitter produces real ArkUI decorators. (d) `EntryAbility.ets` no longer imports `@ohos.window` or has `onWindowStageCreate` with `windowStage.loadContent('pages/Index')`; window stays blank but `console.log` reaches hilog — enough to validate Phase 1's goal of "cross-compile → NAPI bind → TS main() executes". `module.json5` drops `pages: "$profile:main_pages"`, `main_pages.json` no longer emitted, `resources/base/profile/` no longer created. **B.6 (v0.5.129) — native-object pickup**: `compile/link.rs` walks `target/<perry-auto-*>/<triple>/release/build/*/out/` for loose `.o` files emitted by `cc-rs` build scripts (notably `libmimalloc-sys`, which produces a 362-KB `<hash>-static.o` containing 154 mi_* symbols). Rust's staticlib normally bundles these into `libperry_runtime.a`, but on macOS→OHOS cross-builds the `libmimalloc.a` wrapper comes out as a zero-member BSD-format archive (BSD ar's `__.SYMDEF SORTED` layout — macOS-host `ar` creates it, llvm-ar can't read it back) and rustc's "bundle native libs into staticlib" silently skips it. Without forwarding the loose `.o` files to the final link, `libentry.so` ends up with `mi_malloc_aligned` marked UND, and the OHOS dynamic linker rejects dlopen at `EntryAbility.onCreate` with "symbol not found." Walked-pickup is coarser than Rust's per-crate link-lib directive walking (picks up `.o` from any transitive C dep, not just mimalloc), but mimalloc is the only C dep in perry-runtime's closure today and unreferenced ones are dead-stripped via the existing `--gc-sections`. **B.4 (v0.5.127) — earlier audit fixes** mostly bundle into B.5/B.6 above (`-appCertFile` vs `-profileFile` distinction in the hap-sign CLI invocation, `developtools_hapsigner` README pointers in code comments). **Cherry-pick fold-in**: 3 cherry-picks across 3 commits required Cargo.toml + CLAUDE.md conflict resolution per commit (mechanical). compile.rs conflicts taken-as-ours each time and the meaningful new hunks (linker libs, native-object pickup) hand-applied to their current homes in `compile/link.rs` since main has refactored that code out of compile.rs. The harmonyos_hap.rs es2abc rewrite + EntryAbility.ets simplification auto-merged cleanly. Bumped 0.5.129 → **0.5.369** (above main's current 0.5.368 from PR #251).
Loading