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