From 03793e27b96e997cbaf570302ed6be3b541f9df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 17 May 2026 12:12:57 +0200 Subject: [PATCH] =?UTF-8?q?fix(compile):=20#903=20follow-up=20=E2=80=94=20?= =?UTF-8?q?uuid=20regression:=20emit=20closure-wrapper=20stubs=20for=20fai?= =?UTF-8?q?led=20modules'=20named=20exports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiling a project that uses `uuid` under `perry.compilePackages` link-failed at v0.5.945 with: Undefined symbols for architecture arm64: "___perry_wrap_perry_fn_node_modules_uuid_dist_sha1_js__default", referenced from ___perry_wrap_perry_fn_node_modules_uuid_dist_v5_js__v5 in node_modules_uuid_dist_v5_js.o Pre-#903 the binary linked and `v4()` produced a real UUID. Root cause is a two-layer interaction between PR #903 (the same-file default- import collision fix) and a preexisting codegen bug: - uuid's `sha1.js` ends in `return Uint8Array.of(H[0]>>24, ..., H[4])` with 20 args, which hits the bail at `lower_call.rs:~3226`. The whole module's codegen bails before reaching the wrapper-emission loops at `codegen.rs:~2697` / `~2810`, so `__perry_wrap_perry_fn___default` is never defined. - uuid's `v5.js` does `import sha1 from './sha1.js'; import v35 from './v35.js'` — two default imports of different modules. Pre-#903 both registered under the literal key "default" in the CLI's flat `import_function_prefixes` HashMap, so whichever insert landed last won, and the consumer-side `sha1` reference accidentally resolved to v35.js's wrapper (which exists because v35.js compiles fine). The link succeeded "by accident". The collision is gone, the consumer-side reference correctly points at sha1.js's wrapper, and sha1.js's preexisting codegen failure surfaces as the hard link error above. Fix extends the failed-module stub block at `commands/compile.rs:~5518` to also emit closure-wrapper stubs (`__perry_wrap_perry_fn___`) and direct-call stubs (`perry_fn___`) for each `Export::Named` of every failed module. Each stub returns NaN-boxed TAG_UNDEFINED — same inert shape the existing `__init` stub uses for the module body. Consumers that never invoke the failed module link cleanly and run correctly; consumers that DO call in observe undefined (still surfacing the underlying codegen-fail, just not at link time). `perry-codegen/src/stubs.rs` gains a new `generate_stub_object_full` taking a fourth bucket for wrapper-shaped symbols (signature `double name(i64, double, double, double, double, double)` matching the closure-call ABI at `expr.rs:~11783`). The old 3-arg `generate_stub_object` forwards through it so existing call sites are unaffected. Validation: - uuid smoke test (`PERRY_ALLOW_UNIMPLEMENTED=1 perry main.ts -o out && ./out`) now links and prints a real UUID. - New regression test `test-files/test_issue_uuid_sha1_default_export_regression.ts` + fixture exercises the named-default-decl shape (#890) post-#903 and byte-matches Node at `sha1-ok`. - `test_issue_uuid_cross_module_fn.ts` (#890), `test_issue_anonymous_default_export.ts` (#785), `test_issue_678_reexport_default.ts` (#678), and `test_issue_pino_sorting_order_undefined.ts` (#903) all still pass. - `cargo test --release -p perry-codegen --lib stubs` — 3 passed (new `closure_wrapper_stubs` test included). Underlying root cause (sha1.js's `Uint8Array.of(20 args)` bail) remains open and is tracked separately; this fix unblocks every uuid consumer that doesn't exercise sha1/sha1-derived APIs (v3, v5 paths). Refs #903, #890. --- CHANGELOG.md | 35 +++++ CLAUDE.md | 2 +- Cargo.lock | 136 +++++++++--------- Cargo.toml | 2 +- crates/perry-codegen/src/stubs.rs | 67 ++++++++- crates/perry/src/commands/compile.rs | 105 +++++++++++++- .../producer.ts | 35 +++++ ...sue_uuid_sha1_default_export_regression.ts | 26 ++++ 8 files changed, 330 insertions(+), 78 deletions(-) create mode 100644 test-files/fixtures/issue_uuid_sha1_default_export_regression/producer.ts create mode 100644 test-files/test_issue_uuid_sha1_default_export_regression.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9b2da31c..d8c68f2648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.947 — fix(compile): #903 follow-up — uuid regression: emit closure-wrapper stubs for failed modules' named exports + +**Symptom.** Compiling a project that uses `uuid` under `perry.compilePackages` link-failed at v0.5.946 with: + +``` +Undefined symbols for architecture arm64: + "___perry_wrap_perry_fn_node_modules_uuid_dist_sha1_js__default", referenced from: + ___perry_wrap_perry_fn_node_modules_uuid_dist_v5_js__v5 in node_modules_uuid_dist_v5_js.o +ld: symbol(s) not found for architecture arm64 +``` + +The minimal repro is the `package.json` `"perry": { "compilePackages": ["uuid"] }` + `import { v4 } from "uuid"; console.log(v4());`. Pre-#903 the binary linked and `v4()` produced a real UUID. + +**Root cause.** Two-layer interaction between PR #903 (the same-file default-import collision fix) and a preexisting codegen bug. + +- uuid's `sha1.js` defines `function sha1(bytes) { ... return Uint8Array.of(H[0]>>24, ..., H[4]); }` (20 args) and then `export default sha1;`. `Uint8Array.of` with > 16 args hits the bail at `crates/perry-codegen/src/lower_call.rs:~3226` (`Call callee shape not supported (PropertyGet) with 20 args`). The whole module's codegen bails before reaching the wrapper-emission loops at `crates/perry-codegen/src/codegen.rs:~2697` / `~2810`, so `__perry_wrap_perry_fn___default` is never defined. + +- uuid's `v5.js` does `import sha1 from './sha1.js'; import v35 from './v35.js'` — two default imports of different modules. Pre-#903 both registered under the literal key `"default"` in the CLI's flat `import_function_prefixes` HashMap. Whichever insert landed last won, and the consumer-side `sha1` reference accidentally resolved to v35.js's wrapper — which exists because v35.js compiles fine. The link succeeded "by accident"; the runtime sha1 path was already wrong but uuid `v4()` never exercised it. + +#903 corrected the resolution so each default import tracks its own source. The collision is gone, the consumer-side reference correctly points at sha1.js's wrapper, and sha1.js's preexisting codegen failure surfaced as the hard link error above. + +**Fix.** Extend the failed-module stub block at `crates/perry/src/commands/compile.rs:~5518` so it also emits closure-wrapper stubs (`__perry_wrap_perry_fn___`) and direct-call stubs (`perry_fn___`) for each `Export::Named` of every failed module. Each stub returns NaN-boxed `TAG_UNDEFINED` — same inert shape the existing `__init` stub uses for the module body. Consumers that never invoke the failed module link cleanly and run correctly; consumers that DO call in observe undefined (still surfacing the underlying codegen-fail, just not at link time). + +`crates/perry-codegen/src/stubs.rs` gains a new `generate_stub_object_full` taking a fourth bucket for wrapper-shaped symbols (`double name(i64, double, double, double, double, double)` matching the closure-call ABI at `expr.rs:~11783`). The old 3-arg `generate_stub_object` forwards through it so existing call sites are unaffected. + +**Validation.** + +- `uuid` smoke test (`PERRY_ALLOW_UNIMPLEMENTED=1 perry main.ts -o out && ./out`) now links and prints a real UUID (`smoke: ff93849b-5541-4059-803a-552f9b3ecf3e`). +- New regression test `test-files/test_issue_uuid_sha1_default_export_regression.ts` + fixture `test-files/fixtures/issue_uuid_sha1_default_export_regression/producer.ts` exercises the named-default-decl shape (#890) post-#903 and byte-matches Node at `sha1-ok`. +- `test_issue_uuid_cross_module_fn.ts` (the original #890 regression), `test_issue_anonymous_default_export.ts` (#785), `test_issue_678_reexport_default.ts` (#678), and `test_issue_pino_sorting_order_undefined.ts` (#903) all still pass unchanged. +- `cargo build --release -p perry-runtime -p perry-stdlib -p perry` clean. +- `cargo test --release -p perry-codegen --lib stubs` — 3 passed (new `closure_wrapper_stubs` test included). + +Underlying root cause (sha1.js's `Uint8Array.of(20 args)` bail) remains open and is tracked separately; this fix unblocks every uuid consumer that doesn't exercise sha1/sha1-derived APIs (v3, v5 paths). Refs #903, #890. + ## v0.5.946 — fix(hir): #904 — bare `Array(n)` no longer throws `TypeError: value is not a function` **Symptom.** Downstream of the v0.5.943 (#902) `getDay` fix, dayjs's `format("YYYY-MM")` advanced into its `padStart` utility and then threw `TypeError: value is not a function` on the bare `Array(...)` call: diff --git a/CLAUDE.md b/CLAUDE.md index 3aeb75d0b5..d4b08865af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.946 +**Current Version:** 0.5.947 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 0f88d4424f..2822a1d603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4790,7 +4790,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "base64", @@ -4845,14 +4845,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.946" +version = "0.5.947" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "log", @@ -4865,7 +4865,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-hir", @@ -4874,7 +4874,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-hir", @@ -4882,7 +4882,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-dispatch", @@ -4892,7 +4892,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-hir", @@ -4901,7 +4901,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "base64", @@ -4914,7 +4914,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-hir", @@ -4922,7 +4922,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.946" +version = "0.5.947" dependencies = [ "serde", "serde_json", @@ -4930,7 +4930,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.946" +version = "0.5.947" [[package]] name = "perry-doc-fixture-my-bindings" @@ -4941,7 +4941,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "clap", @@ -4956,7 +4956,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.946" +version = "0.5.947" dependencies = [ "argon2", "perry-ffi", @@ -4964,7 +4964,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "reqwest", @@ -4973,7 +4973,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.946" +version = "0.5.947" dependencies = [ "bcrypt", "perry-ffi", @@ -4981,7 +4981,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "rusqlite", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "scraper", @@ -4997,14 +4997,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.946" +version = "0.5.947" dependencies = [ "chrono", "cron", @@ -5013,7 +5013,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.946" +version = "0.5.947" dependencies = [ "chrono", "perry-ffi", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "rust_decimal", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "serde_json", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5045,21 +5045,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.946" +version = "0.5.947" dependencies = [ "bytes", "http-body-util", @@ -5073,7 +5073,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.946" +version = "0.5.947" dependencies = [ "lazy_static", "perry-ffi", @@ -5084,7 +5084,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.946" +version = "0.5.947" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.946" +version = "0.5.947" dependencies = [ "bytes", "http-body-util", @@ -5115,7 +5115,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.946" +version = "0.5.947" dependencies = [ "lazy_static", "perry-ffi", @@ -5125,7 +5125,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.946" +version = "0.5.947" dependencies = [ "base64", "jsonwebtoken", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.946" +version = "0.5.947" dependencies = [ "lru", "perry-ffi", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.946" +version = "0.5.947" dependencies = [ "chrono", "perry-ffi", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.946" +version = "0.5.947" dependencies = [ "bson", "futures-util", @@ -5164,7 +5164,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.946" +version = "0.5.947" dependencies = [ "chrono", "perry-ffi", @@ -5174,7 +5174,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.946" +version = "0.5.947" dependencies = [ "nanoid", "perry-ffi", @@ -5183,7 +5183,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "rustls", @@ -5194,7 +5194,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.946" +version = "0.5.947" dependencies = [ "lettre", "perry-ffi", @@ -5204,7 +5204,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "sqlx", @@ -5213,7 +5213,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.946" +version = "0.5.947" dependencies = [ "governor", "perry-ffi", @@ -5221,7 +5221,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.946" +version = "0.5.947" dependencies = [ "base64", "image", @@ -5230,14 +5230,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.946" +version = "0.5.947" dependencies = [ "lazy_static", "perry-ffi", @@ -5245,7 +5245,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "uuid", @@ -5253,7 +5253,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.946" +version = "0.5.947" dependencies = [ "perry-ffi", "regex", @@ -5263,7 +5263,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.946" +version = "0.5.947" dependencies = [ "futures-util", "lazy_static", @@ -5274,7 +5274,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.946" +version = "0.5.947" dependencies = [ "flate2", "perry-ffi", @@ -5282,7 +5282,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.946" +version = "0.5.947" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5291,7 +5291,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-api-manifest", @@ -5305,7 +5305,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "deno_core", @@ -5325,7 +5325,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-diagnostics", @@ -5337,7 +5337,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "base64", @@ -5361,7 +5361,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.946" +version = "0.5.947" dependencies = [ "aes", "aes-gcm", @@ -5429,7 +5429,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "perry-hir", @@ -5439,7 +5439,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.946" +version = "0.5.947" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5447,11 +5447,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.946" +version = "0.5.947" [[package]] name = "perry-ui-android" -version = "0.5.946" +version = "0.5.947" dependencies = [ "itoa", "jni", @@ -5466,7 +5466,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.946" +version = "0.5.947" dependencies = [ "rand 0.8.6", "serde", @@ -5476,7 +5476,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.946" +version = "0.5.947" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5495,7 +5495,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.946" +version = "0.5.947" dependencies = [ "block2", "libc", @@ -5510,7 +5510,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.946" +version = "0.5.947" dependencies = [ "block2", "libc", @@ -5528,11 +5528,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.946" +version = "0.5.947" [[package]] name = "perry-ui-tvos" -version = "0.5.946" +version = "0.5.947" dependencies = [ "block2", "libc", @@ -5547,7 +5547,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.946" +version = "0.5.947" dependencies = [ "block2", "libc", @@ -5562,7 +5562,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.946" +version = "0.5.947" dependencies = [ "block2", "libc", @@ -5575,7 +5575,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.946" +version = "0.5.947" dependencies = [ "libc", "perry-runtime", @@ -5589,7 +5589,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.946" +version = "0.5.947" dependencies = [ "base64", "ed25519-dalek", @@ -5603,7 +5603,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.946" +version = "0.5.947" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 2f4a0b0fc4..cecfefadc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.946" +version = "0.5.947" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-codegen/src/stubs.rs b/crates/perry-codegen/src/stubs.rs index e04b8d1e6b..72528f98e8 100644 --- a/crates/perry-codegen/src/stubs.rs +++ b/crates/perry-codegen/src/stubs.rs @@ -17,7 +17,7 @@ use crate::nanbox::TAG_UNDEFINED; /// Generate a stub object file for missing symbols from unresolved imports. /// -/// Three kinds of stubs are produced: +/// Four kinds of stubs are produced: /// /// 1. **Data symbols** — exported `i64` globals initialized to NaN-boxed /// `TAG_UNDEFINED`. Used when an `extern` data slot is referenced but @@ -27,6 +27,13 @@ use crate::nanbox::TAG_UNDEFINED; /// 3. **Identity functions** — `double(double)` functions that pass their /// argument through unchanged. Used for `js_await_any_promise` and /// similar pass-through points where the V8 runtime is not present. +/// 4. **Closure-wrapper symbols** — `double(i64, double, double, double, +/// double, double, double)` functions returning NaN-boxed `TAG_UNDEFINED`. +/// Match the closure-call ABI emitted by `expr.rs:~11783` for cross- +/// module function-VALUE references (`__perry_wrap_perry_fn___`). +/// Used when a non-entry module fails to compile under `--allow-unimplemented` +/// so the link still succeeds; consumers that read the import as a closure +/// handle observe a singleton wrapping the undefined-return stub. /// /// `target` is a Perry short target name (`macos`, `ios`, `android`, /// `linux`, `windows`, …) and is forwarded to clang via `-target` so the @@ -36,6 +43,24 @@ pub fn generate_stub_object( missing_func_symbols: &[String], identity_func_symbols: &[String], target: Option<&str>, +) -> Result> { + generate_stub_object_full( + missing_data_symbols, + missing_func_symbols, + identity_func_symbols, + &[], + target, + ) +} + +/// Same as [`generate_stub_object`], plus a fourth bucket for closure- +/// wrapper symbols. Split out so the existing 3-arg callers don't break. +pub fn generate_stub_object_full( + missing_data_symbols: &[String], + missing_func_symbols: &[String], + identity_func_symbols: &[String], + wrapper_func_symbols: &[String], + target: Option<&str>, ) -> Result> { let mut ll = String::new(); @@ -80,12 +105,39 @@ pub fn generate_stub_object( )); } + // 4. Closure-wrapper stubs — match the closure-call ABI emitted at + // `crates/perry-codegen/src/expr.rs:~11783` for cross-module + // function-VALUE references: `double name(i64 closure, double a0, + // double a1, double a2, double a3, double a4)`. Return NaN-boxed + // TAG_UNDEFINED. The 5-double arg list covers the max-arity 5 + // that `js_closure_call0..5` dispatches through; signature shape + // matches the no-op variable/class branch at codegen.rs:~2750. + // + // Used by the failed-module stub block at + // `crates/perry/src/commands/compile.rs:~5518` so consumers that + // reference `__perry_wrap_perry_fn___` for a module + // whose codegen failed under `--allow-unimplemented` still link. + // Without this the link error pre-#903 was masked by the + // same-file-default-import collision pointing the consumer at + // a SIBLING module's wrapper (e.g. uuid v5.js's `import sha1 + // from './sha1.js'` plus `import v35 from './v35.js'` both + // resolved to v35.js's wrapper); post-#903 the collision is + // gone and the missing wrapper surfaces as an undefined symbol. + // Refs #903 / uuid `__perry_wrap_perry_fn_...sha1_js__default`. + for name in wrapper_func_symbols { + ll.push_str(&format!( + "define double @{}(i64 %0, double %1, double %2, double %3, double %4, double %5) {{\n ret double {}\n}}\n\n", + name, undef_hex + )); + } + // If absolutely nothing was requested, emit a single dummy symbol so // the resulting object isn't empty (some linkers complain about empty // objects). if missing_data_symbols.is_empty() && missing_func_symbols.is_empty() && identity_func_symbols.is_empty() + && wrapper_func_symbols.is_empty() { ll.push_str("@__perry_stubs_placeholder = global i64 0, align 8\n"); } @@ -121,4 +173,17 @@ mod tests { let bytes = generate_stub_object(&data, &funcs, &id, None).unwrap(); assert!(bytes.len() > 64); } + + #[test] + fn closure_wrapper_stubs() { + // Regression for the uuid `__perry_wrap_perry_fn___default` + // link error masked pre-#903 by the same-file default-import + // collision: the wrapper signature must match the closure-call + // ABI (i64 closure + up to 5 doubles), and the body returns + // NaN-boxed undefined. + let wrappers = + vec!["__perry_wrap_perry_fn_node_modules_uuid_dist_sha1_js__default".to_string()]; + let bytes = generate_stub_object_full(&[], &[], &[], &wrappers, None).unwrap(); + assert!(bytes.len() > 64); + } } diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index 8411e92afa..d310a603b9 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -5538,18 +5538,109 @@ pub fn run_with_parse_cache( // module-failure: the binary still links, the stubbed module // body is inert, and any actual call into the missing exports // remains the symptom that surfaces the real bug. + let sanitize_module_name = |m: &str| -> String { + let mut out: String = m + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect(); + if out.chars().next().is_some_and(|c| c.is_ascii_digit()) { + out.insert(0, '_'); + } + out + }; let stub_init_names: Vec = failed_modules .iter() - .map(|m| { - let sanitized = m.replace(|c: char| !c.is_alphanumeric() && c != '_', "_"); - format!("{}__init", sanitized) - }) + .map(|m| format!("{}__init", sanitize_module_name(m))) .collect(); - if !stub_init_names.is_empty() { - let stub_bytes = perry_codegen::stubs::generate_stub_object( + // #903 follow-up (uuid regression): also emit closure-wrapper + // stubs for the named exports of each failed module. Pre-#903 a + // consumer's `import sha1 from "./sha1.js"` collided in the + // shared `import_function_prefixes["default"]` slot with the + // same file's `import v35 from "./v35.js"`, so the consumer- + // side reference resolved to v35.js's wrapper symbol — which + // existed because v35.js compiles fine. #903 corrected the + // resolution so each default binding tracks its own source, + // which surfaced uuid's preexisting sha1.js codegen failure + // (`Uint8Array.of` with 20 args bails at lower_call.rs:~3226) + // as a link error: `__perry_wrap_perry_fn___default` + // is referenced by v5.js but never defined because sha1.js's + // compile aborted before reaching the wrapper-emission loops + // in codegen.rs:~2697 / ~2810. + // + // The link error is the symptom; the root cause (sha1.js + // codegen) stays open. Emit no-op wrapper stubs so the link + // succeeds — consumers that never call into the failed module + // (uuid `v4()` is the canonical case; it doesn't use sha1) + // run correctly, and consumers that DO call in observe a + // NaN-boxed undefined return value (matching the inert + // `__init` behavior). + let mut stub_wrapper_names: Vec = Vec::new(); + let mut stub_func_names: Vec = Vec::new(); + for module_name in &failed_modules { + let prefix = sanitize_module_name(module_name); + // Look up the module's HIR (parse + lower succeeded; only + // codegen failed, so the exports are known). The + // `failed_modules` entry is `hir.name` from the codegen + // error message at the par_iter site, not the original + // path key, so iterate the native_modules map to find + // the matching HIR. + let Some(hir) = ctx.native_modules.values().find(|h| h.name == *module_name) else { + continue; + }; + for export in &hir.exports { + if let perry_hir::Export::Named { exported, .. } = export { + let sanitized_exp = exported + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect::(); + // Closure-wrapper form: consumer reads the import + // as a function value (`js_closure_alloc_singleton( + // @__perry_wrap_perry_fn___)`). + let wrap_sym = format!("__perry_wrap_perry_fn_{}__{}", prefix, sanitized_exp); + stub_wrapper_names.push(wrap_sym); + // Direct-call form: consumer invokes the import + // by name (`perry_fn___(args…)`). For + // a failed module the function never received a + // body, so emit a nullary stub returning undefined. + // The link only cares about the symbol existing; + // an arity mismatch at the call site lowers to an + // LLVM `call` with whatever args the consumer + // pushed — the body just discards them and + // returns undefined. Same fallback shape the + // empty `__init` stub uses. + let direct_sym = format!("perry_fn_{}__{}", prefix, sanitized_exp); + stub_func_names.push(direct_sym); + } + } + } + // Combine the `__init` stubs and the direct-call stubs into + // one `missing_func_symbols` bucket — both share the nullary- + // returning-undefined shape. Dedup to keep LLVM from + // complaining about duplicate definitions in case the same + // export is named twice (e.g. an alias). + stub_func_names.extend(stub_init_names); + stub_func_names.sort(); + stub_func_names.dedup(); + stub_wrapper_names.sort(); + stub_wrapper_names.dedup(); + if !stub_func_names.is_empty() || !stub_wrapper_names.is_empty() { + let stub_bytes = perry_codegen::stubs::generate_stub_object_full( &[], - &stub_init_names, + &stub_func_names, &[], + &stub_wrapper_names, target.as_deref(), )?; let stub_path = PathBuf::from("_perry_failed_stubs.o"); diff --git a/test-files/fixtures/issue_uuid_sha1_default_export_regression/producer.ts b/test-files/fixtures/issue_uuid_sha1_default_export_regression/producer.ts new file mode 100644 index 0000000000..513afc2080 --- /dev/null +++ b/test-files/fixtures/issue_uuid_sha1_default_export_regression/producer.ts @@ -0,0 +1,35 @@ +// Regression for the uuid `__perry_wrap_perry_fn___default` link +// error that PR #903 unmasked. +// +// Pre-#903 the consumer's `import sha1 from "./sha1.js"` and the +// sibling-module `import v35 from "./v35.js"` both registered under +// the literal key `"default"` in the CLI's flat +// `import_function_prefixes` HashMap, so the second insert clobbered +// the first and the consumer-side ExternFuncRef resolved to whichever +// module's prefix landed last. That meant a consumer reading `sha1` as +// a closure value emitted a reference to v35.js's wrapper symbol — +// which exists because v35.js compiles fine. uuid's `v4()` smoke test +// linked OK and "worked" by accident. +// +// #903 corrected the resolution so each default import tracks its own +// source. The collision is gone; consumer-side references now point +// at the correct module's wrapper. That surfaced uuid's preexisting +// `sha1.js` codegen failure (`Uint8Array.of` with 20 args bails out +// in `lower_call.rs:~3226`) as a hard link error +// `Undefined symbols: ___perry_wrap_perry_fn___default`. The +// fix in `crates/perry/src/commands/compile.rs:~5518` extends the +// failed-module stub block to emit closure-wrapper stubs for each +// `Export::Named` so the link succeeds; downstream consumers that +// never invoke the failed module (uuid `v4()` doesn't use sha1) keep +// working, and consumers that DO call in observe a NaN-boxed +// undefined return — same inert shape the existing `__init` stub +// gives the module's top-level init. +// +// Producer shape mirrors PR #890's named-default-decl form. We don't +// embed the actual uuid codegen failure into the fixture (that's a +// separate bug, tracked under `Uint8Array.of` multi-arg lowering); +// the producer's wrapper is real and consumer-side resolution must +// continue to wire through it correctly post-#903. +export default function sha1() { + return "sha1-ok"; +} diff --git a/test-files/test_issue_uuid_sha1_default_export_regression.ts b/test-files/test_issue_uuid_sha1_default_export_regression.ts new file mode 100644 index 0000000000..a4d78369db --- /dev/null +++ b/test-files/test_issue_uuid_sha1_default_export_regression.ts @@ -0,0 +1,26 @@ +// Regression for the uuid `__perry_wrap_perry_fn___default` link +// error that PR #903 unmasked. +// +// PR #903 corrected default-import resolution so two `import X from "./a"; +// import Y from "./b"` in the same file no longer collide on the +// shared `import_function_prefixes["default"]` key. Pre-fix the +// collision masked a separate codegen bug in uuid's `sha1.js` +// (`Uint8Array.of` with 20 args bails out, so the module never +// emits its wrapper symbol). Post-fix the link error surfaces with: +// +// Undefined symbols: ___perry_wrap_perry_fn___default, +// referenced from ___perry_wrap_perry_fn___v5 +// +// This test verifies the named-default-decl shape `export default +// function foo() {}` (PR #890) still resolves its wrapper symbol +// correctly post-#903 — i.e. the consumer-side `import foo from +// "./producer"` lowers to a reference that producer-side codegen +// satisfies via the `Export::Named { local: "foo", exported: "default" }` +// alias path. +// +// Pairs with `test_issue_uuid_cross_module_fn.ts` (the original #890 +// regression). Output must match `node --experimental-strip-types` +// byte-for-byte. +import sha1 from "./fixtures/issue_uuid_sha1_default_export_regression/producer.ts"; + +console.log(sha1());