From 194112ceb4f4a322155c333117fdfd6bf75228b5 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Wed, 13 May 2026 05:05:56 +0200 Subject: [PATCH] fix: #732 Windows WinHTTP linking + #692 'default' undefined symbol (v0.5.892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two contained linker fixes reported on GitHub: - #732 (Windows): `--target windows` failed with `lld-link: undefined symbol: WinHttpOpen` (and seven sibling WinHttp* symbols) referenced from `perry_ui_windows::widgets::image::fetch_url_blocking`. The `windows` crate's `Win32_Networking_WinHttp` feature emits `#[link]` attrs in the rlib but they don't propagate through perry-ui-windows's staticlib crate-type. Added `winhttp.lib` to the Windows arm of link.rs's system-libs list. - #692 (Linux/macOS): `undefined reference to 'default'` at link time when a default import resolves to a module Perry can't compile natively (no perry-stdlib binding, not in compilePackages). HIR registers the binding with sentinel `name = "default"`; codegen's ExternFuncRef call path fell through to the native-library extern fallback and emitted `call double @default(...)`. Routed bare-name `"default"` ExternFuncRef calls (no import_function_prefixes entry) to a new runtime stub `js_unresolved_default_call` that returns undefined with a one-shot diagnostic. Args still lowered for side effects. #678 (V8-fallback `_perry_fn_..._render` unresolved symbols) is architectural and tracked separately — it needs either V8-bridge routing or weak stub emission for JsModule exports. --- CHANGELOG.md | 2 + CLAUDE.md | 2 +- Cargo.lock | 134 +++++++++++----------- Cargo.toml | 2 +- crates/perry-codegen/src/lower_call.rs | 22 ++++ crates/perry-codegen/src/runtime_decls.rs | 4 + crates/perry-runtime/src/object.rs | 25 ++++ crates/perry/src/commands/compile/link.rs | 9 +- 8 files changed, 130 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1080c8b42b..80968e97e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.892 — fix(linker): two linker bugs reported on GitHub. (#732) **Windows: `--target windows` fails to link with `lld-link: error: undefined symbol: __declspec(dllimport) WinHttpOpen` (and seven sibling WinHttp* symbols)**, all referenced from `perry_ui_windows::widgets::image::fetch_url_blocking` — the function that fetches `Image(url, alt)` bytes from a background thread. Root cause: `perry-ui-windows` enables the `windows` crate's `Win32_Networking_WinHttp` feature, which emits `#[link(name = "winhttp")]` attrs in the rlib. Those attrs do NOT propagate through `perry-ui-windows`'s `staticlib` crate-type to perry's final link line — perry invokes `lld-link` / `cc` with its own hand-rolled Windows system-library list and `winhttp.lib` simply wasn't in that list. Fix: append `winhttp.lib` to the Windows arm of `crates/perry/src/commands/compile/link.rs`'s system-libs list, next to the existing `oleaut32.lib` / `iphlpapi.lib` entries. (#692) **Linux/macOS: `undefined reference to 'default'` at link time** — a literal symbol named `default` in the unresolved-extern list, no module prefix, no mangling. Reproducer is a single-file TS importing a default export from a module Perry can't resolve as native (`import sanitizeHtml from "sanitize-html"` when sanitize-html isn't in perry-stdlib / `compilePackages` / a perry.nativeLibrary). Root cause chain: (1) HIR's `register_imported_func` at `crates/perry-hir/src/lower.rs:3727` registers default-import bindings under the local name with the original-name sentinel `"default"`. (2) Identifier lookup in `lower.rs:7175` resolves the local to `Expr::ExternFuncRef { name: "default" }`. (3) Codegen's call path in `crates/perry-codegen/src/lower_call.rs:735+` falls through every special-case (setTimeout/jsx/perry-system/`js_*`-builtin) and the `import_function_prefixes` map (which only contains `NativeCompiled` imports from compile.rs:2814+, NOT V8-fallback `JsModule` imports — so `sanitize-html` has no entry). The fallthrough at line 848+ then emits a direct LLVM `call double @default(...)` and the system linker has no such symbol → `undefined reference to 'default'`. Fix: before the native-library fallthrough, detect `name == "default"` with no `import_function_prefixes` entry — the only producer of bare-`"default"` ExternFuncRefs is the default-import-from-unresolved-module path — and route to a new runtime stub `js_unresolved_default_call` (in `crates/perry-runtime/src/object.rs`) that returns NaN-boxed TAG_UNDEFINED and prints a one-shot diagnostic on first call. Args are lowered for side effects (string interning, closure collection) before the stub call so HIR/transform invariants don't break. Now the binary links; running prints `perry: called a default-imported binding from an unresolved module (returns undefined). The module's default export was not found in perry-stdlib or perry.compilePackages — run \`perry --print-api-manifest\` to see what's supported.` once, then `sanitizeHtml(x)` returns `undefined` rather than crashing the linker. **Validation.** Minimal repro `import sanitizeHtml from "sanitize-html"; console.log(sanitizeHtml("hi"));` pre-fix: `lld-link: undefined symbol: default` (Windows) / `ld: undefined reference to 'default'` (Linux); post-fix: links, runs, prints the diagnostic + `result: undefined`. `cargo build --release -p perry-runtime -p perry-codegen -p perry` clean. **Not addressed in this PR (#678).** `_perry_fn_..._render`-style unresolved symbols for V8-fallback modules referenced from native modules are a separate architectural fix: codegen needs to either route through `js_call_function` when the callee module is V8-fallback (requires threading the source-module ModuleKind to codegen) or emit weak stub symbols for V8-fallback module exports (requires threading the JsModule export surface). Either fix is more invasive than this PR's contained two changes; tracked separately. **Out of scope.** Plumbing the source-module path through `register_imported_func` so default-import calls to *V8-loaded* modules could route to `js_call_function` rather than the undefined stub — viable but requires changes to both HIR and the codegen ExternFuncRef machinery; current stub gracefully degrades for the unresolved case, which is the link-failure reproducer. + ## v0.5.875 — fix(transform): `break` / `continue` inside loops containing `await` no longer hang or skip the update step. **Symptom.** `async function f() { for (let i = 0; i < 3; i++) { const x = await Promise.resolve(i); if (x === 2) break; count++; } return count; }` hung forever; same for `continue` inside the body of any for/while loop that contains an `await`. Surface area: every realistic real-world cancel-on-condition / skip-on-condition loop shape — `for (const item of items) { const result = await process(item); if (result.fatal) break; }`, `while (queue.length) { const job = await queue.shift(); if (job.skip) continue; await run(job); }`. Both shapes simply did not work in async functions until this commit. **Root cause.** `transform_async_to_generator`'s `linearize_body` splits a yielding for/while body into multiple states (init → cond → body-pre-yield → yield → body-post-yield → update → after). User `Stmt::Break` and `Stmt::Continue` inside that body were threaded through unchanged as raw HIR `Break`/`Continue` nodes. At codegen the state machine itself is a `while (true) { if (state==N) { ... } if (state==M) { ... } }` dispatch loop, so a raw `Stmt::Break` exits the dispatch — but the post-dispatch code then reads the scratch iter-result (last written by the in-flight await: `done=false`), returns `AsyncStepChain(stale_promise, step)`, the chain detects the resolved promise's value, queues another `Task::AsyncStep`, the step is re-entered, dispatch sees the state set by the await branch, runs the body again from the post-yield point, hits `break` again, and loops forever on a stale promise. Same shape with `Stmt::Continue`. **Fix.** Two coordinated additions in `linearize_body`'s For and While arms. (1) Before recursing into the body, pre-walk the body's top-level statements and rewrite `Stmt::Break` to `[LocalSet(state_id, BREAK_SENTINEL), Stmt::Continue]` and `Stmt::Continue` to `[LocalSet(state_id, CONTINUE_SENTINEL), Stmt::Continue]`. The walker recurses through If / Try-catch / Switch-cases / Labeled bodies but STOPS at nested loops and closure boundaries — their own break/continue belong to those constructs. (2) For for-loops specifically, restructure the post-body states: push a separate `tail_state` (containing the body's post-yield residual, exits to `update_state`) AND a separate `update_state` (containing ONLY the update expression, exits to `cond_state`). User `continue` jumps to `update_state` (the update-only state); body's natural fall-through goes tail_state → update_state. Without the split, the previous design conflated them and user `continue` would re-execute the residual, looping forever on the same iteration. For while-loops, `continue_target = cond_state` directly (no separate update). (3) After body linearization, walk the newly-created states (plus any trailing `current` residual) and replace `BREAK_SENTINEL` / `CONTINUE_SENTINEL` with the loop's real `after_loop_state` / `update_state` (for-loop) or `cond_state` (while-loop). Sentinel values are `1_000_001.0` / `1_000_002.0` — outside any legitimate state count for any realistic function. Inner loops are walked by their own recursive linearize_body call which fixes their sentinels first, so outer-loop fix-up only sees outer-loop sentinels. **Validation.** 6-case `/tmp/probe_brk_cont.ts` covers (t1) break in for-await, (t2) continue in for-await, (t3) break in for-loop with await body, (t4) continue in for-loop with await body, (t5) while-true with await + break, (t6) nested for-loops with break in inner only — all six byte-equal Bun. 7-case async-patterns suite still 7/7. 4-case throw-after-await suite still 4/4. 5-case catch-return suite still 5/5. 5-case for-await suite now 5/5 (previously 4/5; t4 hung pre-fix). Gap parity 26/28 unchanged. **Out of scope.** Labeled break/continue (`break outer;` / `continue inner;`) — Stmt::LabeledBreak / Stmt::LabeledContinue with custom labels. Real-world JS rarely uses labeled break/continue outside of nested-loop edge cases; the unlabeled form covers 99% of the surface area. Captured as follow-up. DoWhile with yield body is also out of scope — `do { await ...; } while (cond)` would need its own arm in `linearize_body` (currently falls through to the default which loses the loop semantics entirely; orthogonal to this commit). **Version-bump note.** Renumbered to v0.5.875 because 63fd3b7b (Ralph's ci-fix landing after my v0.5.873) claimed v0.5.874 in its commit message without bumping Cargo.toml — same pattern as cb496011 (claimed v0.5.871) and c6cfba98 (claimed v0.5.872). To stay ahead of the implicit collision I skip 874 entirely and ship as 875. ## v0.5.873 — fix(hir): `for await (const x of arr)` on plain-array iterables now awaits each element. **Symptom.** `async function f() { let s = 0; for await (const x of [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]) s += x; return s; }` returned `NaN` instead of `6` — Bun returns `6`. Each iteration bound `x` to the raw `Promise` object, and `s += ` produced `NaN`. The same shape covers any real-world pattern where you iterate an array of promises with `for await`, e.g. `for await (const result of taskPromises) { await persist(result); }`. **Root cause.** Perry has two ForOf lowering paths: an iterator-protocol path (for `for await ... of asyncGen()` / class-iterator receivers) that already wrapped the `__iter.next()` call in `Expr::Await`, AND a standard array-based desugar path (`for (let i = 0; i < __arr.length; ++i) { const x = __arr[i]; … }`) that ignored `for_of_stmt.is_await` entirely. SWC parses `for await` and sets the AST flag correctly; both lowering files (`crates/perry-hir/src/lower.rs` for module-init for-of and `crates/perry-hir/src/lower_decl.rs` for function-body for-of) just didn't read it in the array-desugar arm. Confirmed via temporary `eprintln!("for-of is_await={}", for_of_stmt.is_await);` at the binding-stmts construction site: the debug-print didn't fire from `lower.rs` because the function-body path goes through `lower_decl.rs` (the module-level path only handles module-init for-of). After applying the fix in both files, the simple `for await ... of [...promises]` shape unwraps correctly. **Fix.** At the binding-statements construction site in both files, when `for_of_stmt.is_await` is true, wrap the per-element `Expr::IndexGet { arr, idx }` access in `Expr::Await`. This is the spec-equivalent of "Await the value from the iterator" — for a plain-array iterable that means "Await arr[i]". A Promise element unwraps to its resolved value; a non-Promise value passes through unchanged (per the Await codegen's `js_value_is_promise` check + early-merge). Mirrors the iterator-protocol path that already wraps `__iter.next()` for `async function*` / class-iterator receivers. **Validation.** Single probe `for await (const x of [Promise.resolve(1), …]) sum += x` returns `6` byte-equal Bun. 5-case `/tmp/probe_for_await_full.ts` covers (t1) promise array sum, (t2) mixed promise+raw values, (t3) destructuring of resolved promise of object, (t5) plain numeric array (raw values, await passes through). 4/5 pass. (t4, `for await ... if (x === 2) break;`, hangs — separate pre-existing state-machine gap: `break` inside any loop containing `await` doesn't propagate through the state-machine state graph, verified via `for (let i ...) { await ...; if (...) break; }` which also hangs. Captured as follow-up; outside this commit's scope.) 7-case `/tmp/probe_async_patterns.ts` now 7/7 (was 6/7 with t2 failing). All earlier fixes preserved. Gap parity 26/28 unchanged. **Out of scope.** True async-iterator-protocol invocation (calling `[Symbol.asyncIterator]()` on the iterable, falling back to `[Symbol.iterator]()`) for non-array, non-async-generator iterables — this commit covers the plain-array case which is what real-world code uses; arbitrary user-defined async iterables remain a follow-up. **Version-bump note.** Renumbered to v0.5.873 because c6cfba98 (Ralph's chore-ci commit landing after my v0.5.872 fix) claimed v0.5.872 in its commit message without bumping Cargo.toml, leaving the workspace.package.version at 0.5.872 from my prior commit — this push goes straight to v0.5.873 to avoid further collisions. diff --git a/CLAUDE.md b/CLAUDE.md index ab1fa98b1d..efc5d3aac1 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.891 +**Current Version:** 0.5.892 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index a0cce73b8d..5a6828bc86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4615,7 +4615,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "base64", @@ -4670,14 +4670,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.890" +version = "0.5.892" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "log", @@ -4690,7 +4690,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-hir", @@ -4699,7 +4699,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-hir", @@ -4707,7 +4707,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-dispatch", @@ -4717,7 +4717,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-hir", @@ -4726,7 +4726,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "base64", @@ -4739,7 +4739,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-hir", @@ -4747,7 +4747,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.890" +version = "0.5.892" dependencies = [ "serde", "serde_json", @@ -4755,7 +4755,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.890" +version = "0.5.892" [[package]] name = "perry-doc-fixture-my-bindings" @@ -4766,7 +4766,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "clap", @@ -4781,7 +4781,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.890" +version = "0.5.892" dependencies = [ "argon2", "perry-ffi", @@ -4789,7 +4789,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "reqwest", @@ -4798,7 +4798,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.890" +version = "0.5.892" dependencies = [ "bcrypt 0.17.1", "perry-ffi", @@ -4806,7 +4806,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "rusqlite", @@ -4814,7 +4814,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "scraper", @@ -4822,14 +4822,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.890" +version = "0.5.892" dependencies = [ "chrono", "cron", @@ -4838,7 +4838,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.890" +version = "0.5.892" dependencies = [ "chrono", "perry-ffi", @@ -4846,7 +4846,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "rust_decimal", @@ -4854,7 +4854,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "serde_json", @@ -4862,7 +4862,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -4870,21 +4870,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.890" +version = "0.5.892" dependencies = [ "bytes", "http-body-util", @@ -4898,7 +4898,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.890" +version = "0.5.892" dependencies = [ "lazy_static", "perry-ffi", @@ -4909,7 +4909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.890" +version = "0.5.892" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -4921,7 +4921,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.890" +version = "0.5.892" dependencies = [ "bytes", "http-body-util", @@ -4940,7 +4940,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.890" +version = "0.5.892" dependencies = [ "lazy_static", "perry-ffi", @@ -4950,7 +4950,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.890" +version = "0.5.892" dependencies = [ "base64", "jsonwebtoken", @@ -4961,7 +4961,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.890" +version = "0.5.892" dependencies = [ "lru", "perry-ffi", @@ -4969,7 +4969,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.890" +version = "0.5.892" dependencies = [ "chrono", "perry-ffi", @@ -4977,7 +4977,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.890" +version = "0.5.892" dependencies = [ "bson", "futures-util", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.890" +version = "0.5.892" dependencies = [ "chrono", "perry-ffi", @@ -4999,7 +4999,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.890" +version = "0.5.892" dependencies = [ "nanoid", "perry-ffi", @@ -5008,7 +5008,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "rustls", @@ -5019,7 +5019,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.890" +version = "0.5.892" dependencies = [ "lettre", "perry-ffi", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "sqlx", @@ -5038,7 +5038,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.890" +version = "0.5.892" dependencies = [ "governor", "perry-ffi", @@ -5046,7 +5046,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.890" +version = "0.5.892" dependencies = [ "base64", "image", @@ -5055,14 +5055,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.890" +version = "0.5.892" dependencies = [ "lazy_static", "perry-ffi", @@ -5070,7 +5070,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "uuid", @@ -5078,7 +5078,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.890" +version = "0.5.892" dependencies = [ "perry-ffi", "regex", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.890" +version = "0.5.892" dependencies = [ "futures-util", "lazy_static", @@ -5099,7 +5099,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.890" +version = "0.5.892" dependencies = [ "flate2", "perry-ffi", @@ -5107,7 +5107,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.890" +version = "0.5.892" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5116,7 +5116,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-api-manifest", @@ -5130,7 +5130,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "deno_core", @@ -5149,7 +5149,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-diagnostics", @@ -5161,7 +5161,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "base64", @@ -5185,7 +5185,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.890" +version = "0.5.892" dependencies = [ "aes", "aes-gcm", @@ -5253,7 +5253,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "perry-hir", @@ -5263,7 +5263,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.890" +version = "0.5.892" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5271,11 +5271,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.890" +version = "0.5.892" [[package]] name = "perry-ui-android" -version = "0.5.890" +version = "0.5.892" dependencies = [ "itoa", "jni", @@ -5290,7 +5290,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.890" +version = "0.5.892" dependencies = [ "rand 0.8.6", "serde", @@ -5300,7 +5300,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.890" +version = "0.5.892" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5319,7 +5319,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.890" +version = "0.5.892" dependencies = [ "block2", "libc", @@ -5334,7 +5334,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.890" +version = "0.5.892" dependencies = [ "block2", "libc", @@ -5352,11 +5352,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.890" +version = "0.5.892" [[package]] name = "perry-ui-tvos" -version = "0.5.890" +version = "0.5.892" dependencies = [ "block2", "libc", @@ -5371,7 +5371,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.890" +version = "0.5.892" dependencies = [ "block2", "libc", @@ -5386,7 +5386,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.890" +version = "0.5.892" dependencies = [ "block2", "libc", @@ -5399,7 +5399,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.890" +version = "0.5.892" dependencies = [ "libc", "perry-runtime", @@ -5413,7 +5413,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.890" +version = "0.5.892" dependencies = [ "base64", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index a02d2e70c1..cd39f93142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.891" +version = "0.5.892" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-codegen/src/lower_call.rs b/crates/perry-codegen/src/lower_call.rs index 6d616dbc96..625af8d913 100644 --- a/crates/perry-codegen/src/lower_call.rs +++ b/crates/perry-codegen/src/lower_call.rs @@ -840,6 +840,28 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R lowered.iter().map(|s| (DOUBLE, s.as_str())).collect(); return Ok(ctx.block().call(DOUBLE, name, &arg_slices)); } + // Issue #692: default-import call against an unresolved module. + // `import sanitizeHtml from "sanitize-html"` (when sanitize-html + // didn't resolve to a NativeCompiled module / perry-stdlib + // binding) lowers `sanitizeHtml(x)` to `Call { callee: + // ExternFuncRef { name: "default" } }` — the HIR's + // register_imported_func uses the literal `"default"` as the + // exported-name marker for default imports (lower.rs:3727). + // Without a source_prefix, the catch-all below emitted a direct + // LLVM call to the bare symbol `default`, and the system linker + // failed with `undefined reference to 'default'`. Route to the + // runtime stub instead: lower args for side effects (so closure + // collection / string interning still happens), then call + // `js_unresolved_default_call` which returns NaN-boxed undefined + // and prints a one-shot diagnostic at runtime. The program now + // links; the user gets a clear runtime signal rather than a + // cryptic linker error. + if name == "default" && !ctx.import_function_prefixes.contains_key(name) { + for a in args { + let _ = lower_expr(ctx, a)?; + } + return Ok(ctx.block().call(DOUBLE, "js_unresolved_default_call", &[])); + } // Native library functions (bloom_draw_rect, bloom_init_window, // etc.) that aren't in the import map — emit a direct call so // the linker resolves them against the linked native .a library. diff --git a/crates/perry-codegen/src/runtime_decls.rs b/crates/perry-codegen/src/runtime_decls.rs index 6436b99c47..73a015eb63 100644 --- a/crates/perry-codegen/src/runtime_decls.rs +++ b/crates/perry-codegen/src/runtime_decls.rs @@ -824,6 +824,10 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { // cleanly resolves to undefined (instead of TAG_TRUE → "boolean" / // "(boolean).method is not a function"). module.declare_function("js_unresolved_namespace_stub", DOUBLE, &[]); + // Issue #692: stub for default-imported callables from unresolved modules — + // returns NaN-boxed undefined and prints a one-shot diagnostic, so the + // program links instead of failing with `undefined reference to 'default'`. + module.declare_function("js_unresolved_default_call", DOUBLE, &[]); // Issue #611: real persistent globalThis singleton. Returns a // NaN-boxed POINTER to a per-process ObjectHeader so // `globalThis[k] = v` then `globalThis[k]` round-trips correctly. diff --git a/crates/perry-runtime/src/object.rs b/crates/perry-runtime/src/object.rs index 101f618d27..d097362da2 100644 --- a/crates/perry-runtime/src/object.rs +++ b/crates/perry-runtime/src/object.rs @@ -500,6 +500,31 @@ pub extern "C" fn js_unresolved_namespace_stub() -> f64 { f64::from_bits(crate::JSValue::pointer(null_obj_ptr).bits()) } +/// Issue #692: default-import calls against unresolved modules +/// (`import jwt from "jsonwebtoken"; jwt.sign(...)` when no perry-stdlib +/// binding matched the method, or `import sanitizeHtml from +/// "sanitize-html"; sanitizeHtml(x)` when sanitize-html doesn't resolve +/// to a NativeCompiled module) used to lower to an LLVM extern named +/// literally `default`, which the system linker can't resolve — +/// surfaced as `undefined reference to 'default'`. Route those calls +/// here so the binary links; the runtime stub prints a one-shot +/// diagnostic and returns NaN-boxed undefined. The user gets a clear +/// signal at first call rather than a cryptic link error. +#[no_mangle] +pub extern "C" fn js_unresolved_default_call() -> f64 { + use std::sync::atomic::{AtomicBool, Ordering}; + static WARNED: AtomicBool = AtomicBool::new(false); + if !WARNED.swap(true, Ordering::Relaxed) { + eprintln!( + "perry: called a default-imported binding from an unresolved module \ + (returns undefined). The module's default export was not found in \ + perry-stdlib or perry.compilePackages — run `perry --print-api-manifest` \ + to see what's supported." + ); + } + f64::from_bits(0x7FFC_0000_0000_0001) // TAG_UNDEFINED +} + static NULL_OBJECT_BYTES: NullObjectBytes = NullObjectBytes { object_type: 1, class_id: 0, diff --git a/crates/perry/src/commands/compile/link.rs b/crates/perry/src/commands/compile/link.rs index 0838e0e332..2f6404e32b 100644 --- a/crates/perry/src/commands/compile/link.rs +++ b/crates/perry/src/commands/compile/link.rs @@ -1256,7 +1256,14 @@ pub(super) fn build_and_run_link( .arg("oleaut32.lib") .arg("propsys.lib") .arg("runtimeobject.lib") - .arg("iphlpapi.lib"); + .arg("iphlpapi.lib") + // winhttp.lib — perry-ui-windows::widgets::image::fetch_url_blocking + // uses WinHttpOpen/Connect/OpenRequest/SendRequest/ReceiveResponse + // to fetch Image(url) bytes. The `windows` crate's `Win32_Networking_WinHttp` + // feature emits #[link] attrs in the rlib, but those don't propagate + // through perry-ui-windows's `staticlib` crate-type to perry's final + // link line. Closes #732. + .arg("winhttp.lib"); } else { // macOS frameworks for runtime (sysinfo, etc.) and V8. // Gate on `!is_harmonyos` so the macOS host doesn't leak its