Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

## v0.5.927 — fix(compile): #851 — Rollup-bundled CJS-inside-ESM modules no longer trigger `ImportExportInScript`. **Symptom.** `import { expect } from "vitest"` against a project with `perry.compilePackages: ["vitest"]` fails at parse time: `Failed to parse vitest/dist/chunks/test.DNmyFkvJ.js: Parse error: Error { error: (12817..12823, ImportExportInScript) }`. The vitest dist bundle is Rollup-produced — top-level ESM `import`/`export` statements with inlined CJS dependencies preserved as nested IIFE bodies (`(function (module, exports$1) { module.exports = factory(); })(...)`). **Root cause.** `crates/perry/src/commands/compile/cjs_wrap.rs::is_commonjs` returned true whenever the source contained any of `module.exports`, `exports.`, or `require(` — regardless of where in the file the token appeared. For Rollup hybrid output the inner CJS body trips the `module.exports` check and the entire file gets fed through `wrap_commonjs`, which moves the file body (including the top-level `import`/`export`) inside an IIFE. SWC then parses each function body as a script context, so the relocated `import` statement raises `ImportExportInScript`. **Fix.** Added a top-level ESM check that short-circuits `is_commonjs`: if any unindented line begins with `import ` / `import{` / `import"` / `import'` / `import(` / `export ` / `export{` / `export*` / `export"` / `export'`, the file is unambiguously ESM and the wrap is skipped. The `starts_with_esm_keyword` helper rejects identifier-continuation characters (so `exports.foo` doesn't match `export` and `importMap` doesn't match `import`), and indented occurrences are ignored because `import`/`export` statements are only valid at module top-level. **Validation.** Pre-fix: `cd /tmp/perry-vitest && perry main.ts -o out` fails at `Failed to parse .../test.DNmyFkvJ.js: ImportExportInScript`. Post-fix: the same command parses the bundle cleanly and advances to the link stage (where it then fails on missing `libperry_jsruntime.a` because vitest pulls in V8-fallback modules — a separate downstream issue tracked under #793/#805). Seven new unit tests in `crates/perry/src/commands/compile/cjs_wrap.rs` cover the Rollup hybrid shape, top-level `export`-wins-over-CJS-tokens, `export *`, the `exports.foo`/`importMap` non-match guards, the indented-import guard, and top-level dynamic `import('./x')` classification. Existing `is_commonjs`/`wrap_commonjs` tests unaffected (48/48 pass). New end-to-end smoke test `test-files/test_issue_851_rollup_cjs_in_esm.ts` compiles natively and produces `ok-851`, guarding against later-stage regressions when bare `module`/`exports`/`require` identifiers appear inside ESM function bodies. **Files touched.** `crates/perry/src/commands/compile/cjs_wrap.rs` (top-level ESM guard + 7 unit tests), `test-files/test_issue_851_rollup_cjs_in_esm.ts` (new regression test). Closes #851 (parser-mode half). Refs #793 (Node.js + TypeScript compatibility roadmap), #805 (npm sweep tier2 — vitest is the canonical example; the same Rollup-CJS-inlining shape affects an estimated 20%+ of top-1k npm packages including vite, modern React tooling, and anything built with `@rollup/plugin-commonjs`).

## v0.5.926 — fix(codegen): #842 — emit `__perry_ns_<prefix>` for side-effect-only dynamic-import targets. **Symptom.** A module loaded via `await import("./X.ts")` that declares no `export` statements (a "side-effect-only" module — common shape for `./polyfills.js`, `./register.js`, `./shim.js`, and the `node_modules/ink/build/devtools.js` case that surfaced this from the #803 ink e2e scaffold) failed to link: `Undefined symbols for architecture arm64: "___perry_ns_<target_prefix>", referenced from: _<consumer>__init_body`. **Root cause.** Producer-side codegen in `crates/perry-codegen/src/codegen.rs` gated emission of the `@__perry_ns_<prefix>` namespace global on `cross_module.namespace_entries.is_empty()` (was-line 2954). For export-less modules `namespace_entries` is always empty, so the global definition was never emitted. The consumer-side dispatch in the same file (was-line 2978) declared `@__perry_ns_<target_prefix>` as `external` unconditionally — the link step then saw a referenced-but-undefined symbol. The runtime `js_create_namespace` already tolerated `n == 0` (returns a fresh empty NaN-boxed object — `crates/perry-runtime/src/object.rs:9397`); the bug was strictly in the codegen guard. **Fix.** Added `is_dynamic_import_target: bool` to both `CompileOptions` and the internal `CrossModuleInfo` struct in `crates/perry-codegen/src/codegen.rs`. Wired from the already-computed `dyn_target_paths` set in `crates/perry/src/commands/compile.rs` so each module knows whether it is a target. Producer-side global emission and both populator call-sites now fire when EITHER `!namespace_entries.is_empty()` OR `is_dynamic_import_target` is true. `emit_namespace_populator` no longer early-returns on empty entries — it alloca's minimum-size buffers (`[1 x ?]` so LLVM doesn't choke on `[0 x ?]`) and passes `n=0` + the buffer pointers to `js_create_namespace`, which produces the empty-namespace object and stores it into the global. Cache key in `crates/perry/src/commands/compile/object_cache.rs` includes the new flag so toggling dynamic-import target status doesn't get stale cached `.o` bytes. **Validation.** Repro `test-files/test_issue_842_side_effect_dynamic_import.ts` + helper compiles + runs, producing the deterministic four-line output (`before-import / helper-side-effect-ran / after-import / ok`). Pre-fix the same scenario hits the exact undefined-symbol error from the bug report; post-fix link succeeds. Gap suite: 36/36 PASS (including all 8 `test_gap_dynamic_import_*.ts`). Full parity sweep: 94.2% (328 pass / 20 fail / 47 compile-fail / 13 skip); the parity-fail entries match `test-parity/known_failures.json` plus pre-existing stdlib gaps, and the compile-fail tally is dominated by suite-internal noise from concurrent stdlib auto-rebuilds — spot-checked tests in that list compile fine individually. No regressions traceable to the change. **Files touched.** `crates/perry-codegen/src/codegen.rs` (CompileOptions field, CrossModuleInfo field, wire-through at construction, producer-side global guard, both populator call-site guards, populator empty-entries handling), `crates/perry/src/commands/compile.rs` (set `is_dynamic_import_target` from `dyn_target_paths`), `crates/perry/src/commands/compile/object_cache.rs` (cache key + test helper), `test-files/test_issue_842_side_effect_dynamic_import.ts` + `test-files/test_issue_842_side_effect_helper.ts` (new regression test). Closes #842. Refs #793 (Node.js + TypeScript compatibility roadmap), #803 (ink e2e CI scaffold — surfaced this), #348 (compile-as-package: ink end-to-end).

## v0.5.925 — fix(driver): #837 — failed-module init stubs now use the `<prefix>__init` symbol name the rest of the codegen actually references, so a non-entry module that fails to lower no longer poisons the link. **Symptom (#837 followup).** v0.5.916's 1dd9f471 closed the original report (the `__perry_wrap_perry_fn_<src>__default` wrapper-symbol gap for `export default function`), but the issue stayed open because the uuid v4 repro from the bug body still failed at link — same shape, different missing symbol. With the wrapper fix in place, `perry main.ts -o out` (where `main.ts` is `import { v4 } from "uuid"; console.log(v4());` and `package.json` lists `uuid` in `perry.compilePackages`) advances past the codegen-time module sweep, prints `⚠ 1 module(s) failed to compile — linking with empty stubs` for `node_modules/uuid/dist/sha1.js` (codegen still bails on `Uint8Array.of(...)` with 20 args — a `Call callee shape not supported (PropertyGet) with 20 args` from `crates/perry-codegen/src/lower_call.rs::3226` when `args.len() > 16` and the receiver is a `GlobalGet(_)` — that's a separate codegen limit, filed below as the remaining v4 runtime blocker), then the link still dies with `Undefined symbols for architecture arm64: "_node_modules_uuid_dist_sha1_js__init", referenced from: _main in main_ts.o, _node_modules_uuid_dist_v5_js__init in node_modules_uuid_dist_v5_js.o`. The driver's `linking with empty stubs` reassurance was a lie — the stub object did get emitted, but under the wrong symbol name. **Root cause.** `crates/perry/src/commands/compile.rs:5448-5454` (added in f679c502, "v0.5.3: driver hard-fails when entry module fails to compile") generated each failed-module stub as `_perry_init_<sanitized>` (`format!("_perry_init_{}", sanitized)`), then handed it to `perry_codegen::stubs::generate_stub_object` which dutifully emitted `define double @_perry_init_<sanitized>() { ret double 0x7FFC000000000001 }`. But the actual cross-module module-init naming convention — emitted by codegen for every module since the `<prefix>__init` rename and used by every caller — is `<sanitized>__init` (`crates/perry-codegen/src/codegen.rs:4668`: `let init_name = format!("{}__init", module_prefix);`, called by the entry module's main and by every consumer's `<prefix>__init_body` via `blk.call_void(&format!("{}__init", dep_prefix), &[])` at codegen.rs:4702). The two naming conventions never converged after the rename. Empty-stub generation has been writing to a name no caller references for the lifetime of the rename — the bug only surfaced now because `compilePackages` is the first feature that routinely puts non-entry npm modules through a codegen-failure path with downstream module imports (the entry module itself fails the hard-fail check higher up; standalone single-file builds never reach this stub path). For uuid the v5.js module imports sha1.js's default export and references `node_modules_uuid_dist_sha1_js__init` in its own `__init_body` wrapper — sha1.js's `_perry_init_node_modules_uuid_dist_sha1_js` stub was sitting right there in `_perry_failed_stubs.o`, defining a symbol nothing in the link graph asks for. **Fix.** One-line format-string change at `crates/perry/src/commands/compile.rs:5452` — `format!("_perry_init_{}", sanitized)` → `format!("{}__init", sanitized)`. The stub's IR shape (`define double @<name>() { ret double 0x7FFC000000000001 }` — the existing nullary-fn arm in `crates/perry-codegen/src/stubs.rs:67-73`) is left as-is: callers declare and call `void <prefix>__init()`, the stub defines `double <prefix>__init()`, but at the Mach-O / ELF / COFF link layer symbols are resolved by name only — return-type mismatch doesn't break the link, and on AArch64 the caller's no-arg call sequence + the stub's immediate `ret` (no save/restore, no inspection of xmm0/d0) is benign at runtime. Comment block expanded to document the rename history + why the bug went undetected this long. Two user-facing `eprintln!` strings in the entry-module-failure abort message (compile.rs:5039 and :5048) updated to reference the new naming so the diagnostic stays accurate (`_perry_init_*` → `<prefix>__init`). **Validation.** `mkdir /tmp/perry-uuid && cd /tmp/perry-uuid && cat > package.json <<'JSON' {"name":"d","private":true,"type":"module","dependencies":{"uuid":"*"},"perry":{"compilePackages":["uuid"]}}\nJSON\nnpm install && cat > main.ts <<'TS' import { v4 } from "uuid"; console.log(v4());\nTS && perry main.ts -o out` — pre-fix: link fails with `Undefined symbols: _node_modules_uuid_dist_sha1_js__init`; post-fix: link succeeds (`Wrote executable: out, Binary size: 2.2MB`), `nm _perry_failed_stubs.o` shows `T _node_modules_uuid_dist_sha1_js__init` (the actually-referenced symbol). Substituting `console.log(v4({ random: new Uint8Array(16).fill(7) }))` produces a well-formed UUID `07070707-0707-4707-8707-070707070707` byte-shape — proves the link → runtime → uuid v4 path is intact for explicit-bytes invocations. Same fix unblocks any compile-packages program with one or more non-entry npm modules that hit a codegen limit (currently a long tail — `Uint8Array.of` >16 args is one; `applyMixins` and others tracked under #793/#805 are next). **Remaining for the original v4 invocation specifically.** `v4()` (no args) hits two downstream gaps that fall outside the link scope of #837: (1) perry returns `0`/falsy for `crypto.randomUUID` instead of either a working function or a missing property (so v4.js's early-return-via-randomUUID branch is skipped — `console.log(typeof crypto.randomUUID)` → `number`, `crypto.randomUUID` → `0`), and (2) `import rng from "uuid/dist/rng.js"; rng()` returns `undefined` when invoked through a compilePackages module boundary even though an inlined copy of the same `function rng() { return crypto.getRandomValues(rnds8); }` works correctly — looks like a cross-module function-call lowering bug on the compilePackages path. Both filed as separate follow-up issues; neither is on the link-side critical path that #837 was tracking. **Files touched.** `crates/perry/src/commands/compile.rs` (one format-string fix at :5452, two diagnostic message updates at :5039 and :5048, expanded comment block at :5440-5466), `Cargo.toml` (workspace version bump), `CLAUDE.md` (version line). Closes #837. Refs #793, #805 (the broader npm-sweep compatibility roadmap that surfaced this; sha1.js's 20-arg call and the rng/crypto runtime gaps remain on that tracker).
Expand Down
2 changes: 1 addition & 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.926
**Current Version:** 0.5.927


## TypeScript Parity Status
Expand Down
Loading
Loading