From b569203eb7501577e043b0c7c4a3c4834f3e70dc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 18:34:30 +0000 Subject: [PATCH 1/3] Release 0.2.0: relative JS imports, markup else if, rx-tsc on npm - A relative JS import is an ES import in product builds (MO-8c); `./api` stays the injected API. No package.json `#imports` alias needed. - `else if` in markup control (spec/07), as a nested `if` in the else. - Bump compiler, runtime, language server (ships rx-tsc) and the extension to 0.2.0; CHANGELOG.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWPLMLsJjr7WiwaUgvd --- CHANGELOG.md | 26 ++++++++++++++++++ compiler/Cargo.lock | 2 +- compiler/Cargo.toml | 2 +- compiler/npm/darwin-arm64/package.json | 2 +- compiler/npm/darwin-x64/package.json | 2 +- compiler/npm/linux-arm64-gnu/package.json | 2 +- compiler/npm/linux-x64-gnu/package.json | 2 +- compiler/npm/wasm32-wasi/package.json | 2 +- compiler/npm/win32-x64-msvc/package.json | 2 +- compiler/package-lock.json | 4 +-- compiler/package.json | 2 +- compiler/src/emit.rs | 14 +++++++--- compiler/src/parse.rs | 16 +++++++++-- compiler/test/node.mjs | 19 +++++++++++++ guide/reference.md | 3 +- ref/src/parse.ts | 11 ++++++-- runtime/package-lock.json | 4 +-- runtime/package.json | 2 +- scenarios/S-surface-4.md | 29 ++++++++++++++++++++ spec/07-surface.md | 2 +- spec/09-modules.md | 4 +++ tooling/rx-language-server/package-lock.json | 4 +-- tooling/rx-language-server/package.json | 2 +- tooling/vscode-rx/README.md | 2 +- tooling/vscode-rx/package-lock.json | 4 +-- tooling/vscode-rx/package.json | 2 +- 26 files changed, 133 insertions(+), 33 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 scenarios/S-surface-4.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..456418ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +## 0.2.0 + +`@reactivescript/compiler`, `@reactivescript/runtime`, `@reactivescript/language-server` and the editor extension, +together. + +**Breaking** + +- `optimistic` is now `guess` (`guess todos := …`), and errors clear with `onFlush` (#47). `optimistic` is no longer + a keyword. +- A new check, `GUESS_WHILE_LOADING` (AC-3a): a guess written before its value has been shown is an error (#53). + +**New** + +- `rx-tsc`, in `@reactivescript/language-server`: TypeScript's `tsc` with `.rx` files in the program, for CI (#41). +- `else if` in markup: `{if a { … } else if b { … } else { … }}`. +- A guess over an equation, as Solid's `createOptimistic(fn)` (#48). +- Destructuring (RG-4b): live in declarations, a snapshot in `let`, with rest (#49). +- Parse errors are coded and name the JS or React habit they come from; every code has a page in + `guide/diagnostics.md`, linked from Vite, `rx-tsc` and the editor (#42). + +**Fixed** + +- A relative JS import (`import pure { f } from "./util.js"`) compiles to an ES import. It used to fail as "not in + the scenario prelude", so a package.json `#imports` alias was needed. `./api` is still the app's injected API. diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock index 3dd7699c..d4625159 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -988,7 +988,7 @@ dependencies = [ [[package]] name = "reactivescript" -version = "0.1.0" +version = "0.2.0" dependencies = [ "indexmap", "napi", diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index ca31e6a3..ce695c02 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactivescript" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "ReactiveScript's compiler (spec/11): parse, check, resolve, and lower through Solid's solidjs-compiler" license = "MIT" diff --git a/compiler/npm/darwin-arm64/package.json b/compiler/npm/darwin-arm64/package.json index d3fbeb38..209c93f9 100644 --- a/compiler/npm/darwin-arm64/package.json +++ b/compiler/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/compiler-darwin-arm64", - "version": "0.1.0", + "version": "0.2.0", "cpu": [ "arm64" ], diff --git a/compiler/npm/darwin-x64/package.json b/compiler/npm/darwin-x64/package.json index 50914968..dc363f05 100644 --- a/compiler/npm/darwin-x64/package.json +++ b/compiler/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/compiler-darwin-x64", - "version": "0.1.0", + "version": "0.2.0", "cpu": [ "x64" ], diff --git a/compiler/npm/linux-arm64-gnu/package.json b/compiler/npm/linux-arm64-gnu/package.json index 71ddb22d..0e622b21 100644 --- a/compiler/npm/linux-arm64-gnu/package.json +++ b/compiler/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/compiler-linux-arm64-gnu", - "version": "0.1.0", + "version": "0.2.0", "cpu": [ "arm64" ], diff --git a/compiler/npm/linux-x64-gnu/package.json b/compiler/npm/linux-x64-gnu/package.json index c88c07f6..3dc129d6 100644 --- a/compiler/npm/linux-x64-gnu/package.json +++ b/compiler/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/compiler-linux-x64-gnu", - "version": "0.1.0", + "version": "0.2.0", "cpu": [ "x64" ], diff --git a/compiler/npm/wasm32-wasi/package.json b/compiler/npm/wasm32-wasi/package.json index addba861..12182657 100644 --- a/compiler/npm/wasm32-wasi/package.json +++ b/compiler/npm/wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/compiler-wasm32-wasi", - "version": "0.1.0", + "version": "0.2.0", "main": "compiler.wasi.cjs", "browser": "compiler.wasi-browser.js", "files": [ diff --git a/compiler/npm/win32-x64-msvc/package.json b/compiler/npm/win32-x64-msvc/package.json index 52afde77..5236eabb 100644 --- a/compiler/npm/win32-x64-msvc/package.json +++ b/compiler/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/compiler-win32-x64-msvc", - "version": "0.1.0", + "version": "0.2.0", "cpu": [ "x64" ], diff --git a/compiler/package-lock.json b/compiler/package-lock.json index a96492fe..bd91af54 100644 --- a/compiler/package-lock.json +++ b/compiler/package-lock.json @@ -1,12 +1,12 @@ { "name": "@reactivescript/compiler", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@reactivescript/compiler", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@emnapi/core": "^1.11.3", diff --git a/compiler/package.json b/compiler/package.json index 2eb3b286..262e369d 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -1,7 +1,7 @@ { "name": "@reactivescript/compiler", "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "repository": { "type": "git", diff --git a/compiler/src/emit.rs b/compiler/src/emit.rs index 0ed92891..378e1778 100644 --- a/compiler/src/emit.rs +++ b/compiler/src/emit.rs @@ -255,6 +255,12 @@ struct Gen<'p> { } impl<'p> Gen<'p> { + /// A JS import that becomes a real ES import (MO-8): a package's always; a relative one too when the modules go to a + /// bundler (`by_source`, the product), where `./util.js` is the app's own file. `./api` stays the app's injected API + /// (`createApp({ api })`, CP-4), and in the scenario runners every relative import is the harness's. + fn js_import(&self, from: &str) -> bool { + !from.starts_with('.') || (self.by_source && !from.ends_with("/api")) + } fn new(u: Usage, set: &'p ModuleSet) -> Self { Gen { u, @@ -338,7 +344,7 @@ impl<'p> Gen<'p> { // a package's JS (`@solidjs/web/server-functions`): a real import at module level, where a hoisted server fn's wrappers are for d in decls { if let Decl::Import { resolved: None, from, names, .. } = d { - if from.starts_with('.') { + if !self.js_import(from) { continue; } let ns: Vec<(String, String)> = names.iter().filter(|n| !builtin(&n.name) && n.name != "log").map(|n| (n.name.clone(), format!("w${}", n.alias))).collect(); @@ -361,7 +367,7 @@ impl<'p> Gen<'p> { ExprKind::ServerFn { params, body, .. } => self.server_hoist(name, params, body, true), ExprKind::Call { f, args } if matches!(&f.k, ExprKind::Id(_)) => { let ExprKind::Id(fname) = &f.k else { unreachable!() }; - let found = self.mod_decls.iter().any(|d| matches!(d, Decl::Import { resolved: None, from, names, .. } if !from.starts_with('.') && names.iter().any(|n| &n.alias == fname))); + let found = self.mod_decls.iter().any(|d| matches!(d, Decl::Import { resolved: None, from, names, .. } if self.js_import(from) && names.iter().any(|n| &n.alias == fname))); if !found { return fail(format!("{name}: a server fn's wrapper is a package's function (here, {fname})")); } @@ -402,7 +408,7 @@ impl<'p> Gen<'p> { if n.name == "log" || builtin(&n.name) { continue; } - if !from.starts_with('.') { + if self.js_import(from) { self.set(env, &n.alias, val(format!("w${}", n.alias))); continue; } @@ -642,7 +648,7 @@ impl<'p> Gen<'p> { let n0 = out.len(); for n in names { let (name, alias) = (&n.name, &n.alias); - let v = if !from.starts_with('.') && !builtin(name) && name != "log" { + let v = if self.js_import(from) && !builtin(name) && name != "log" { id(format!("w${alias}")) } else if name == "log" { dot(id("__B"), "log") diff --git a/compiler/src/parse.rs b/compiler/src/parse.rs index e31d4ae1..a2ecb6aa 100644 --- a/compiler/src/parse.rs +++ b/compiler/src/parse.rs @@ -1888,7 +1888,7 @@ fn parse_markup(c: &mut Cursor, close: Close) -> R> { } } -/// Control forms of a view: `if c { … } else { … }`, `for x in e { … }`, `loading … { … }`, `errored …`, `portal …`. +/// Control forms of a view: `if c { … } else if d { … } else { … }`, `for x in e { … }`, `loading … { … }`, `errored …`, `portal …`. fn parse_control(c: &mut Cursor) -> R> { let save = c.i; let mk = |kind| Some(Markup { kind, line: None, span: Span::NONE }); @@ -1906,8 +1906,18 @@ fn parse_control(c: &mut Cursor) -> R> { let (mut f_decls, mut f) = (vec![], vec![]); if c.keyword("else") { c.ws(true); - c.expect("{")?; - (f_decls, f) = parse_block_body(c)?; + let at = c.i; + if c.keyword("if") { + // `else if c { … }` is `else { if c { … } }` + c.i = at; + match parse_control(c)? { + Some(next) => f = vec![next], + None => return c.fail("a block after `else if`: else if c { … }"), + } + } else { + c.expect("{")?; + (f_decls, f) = parse_block_body(c)?; + } } return Ok(mk(MarkupKind::Mif { c: cond, t, t_decls, f, f_decls })); } diff --git a/compiler/test/node.mjs b/compiler/test/node.mjs index fe064062..15984a05 100644 --- a/compiler/test/node.mjs +++ b/compiler/test/node.mjs @@ -3,6 +3,8 @@ import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import path from "node:path"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; const { app } = createRequire(import.meta.url)("../index.js"); const examples = fileURLToPath(new URL("../../examples/", import.meta.url)); @@ -17,4 +19,21 @@ for (const [entry, target, spa] of [["todo.rx", "dom"], ["todo.rx", "ssr"], ["sh } const bad = JSON.parse(app(path.join(examples, "missing.rx"))); if (!bad.error) { fail++; console.log("✗ a missing entry is not an error"); } else console.log("✓ a missing entry is an error"); +// a relative JS import is the app's own file (MO-8): it stays an ES import, no package.json `#imports` needed; `./api` +// stays the injected API (CP-4) +const dir = mkdtempSync(path.join(tmpdir(), "rx-import-")); +writeFileSync(path.join(dir, "util.js"), "export const greet = n => `hi ${n}`;\n"); +writeFileSync(path.join(dir, "main.rx"), [ + 'import pure { greet } from "./util.js"', + 'import { save } from "./api"', + 'export default component App() {', + ' source n := 1', + '

{ save(n) }}>{greet(n)}

', + '}', +].join("\n")); +const rel = JSON.parse(app(path.join(dir, "main.rx"), "dom", null, true)); +const code = Object.values(rel.modules ?? {}).map(m => m.code).join("\n"); +const relOk = !rel.error && !rel.diagnostics && /from "\.\/util\.js"/.test(code) && !/from "\.\/api"/.test(code); +if (!relOk) fail++; +console.log(`${relOk ? "✓" : "✗"} a relative JS import stays an ES import${relOk ? "" : `: ${JSON.stringify(rel).slice(0, 300)}`}`); process.exit(fail ? 1 : 0); diff --git a/guide/reference.md b/guide/reference.md index e691484b..8dc645c5 100644 --- a/guide/reference.md +++ b/guide/reference.md @@ -21,14 +21,13 @@ Decl' ::= "source" Name Type? ("key" Name)? (":=" | "=") Expr -- `:=` s | "component" Name "(" Name Type? ")" "{" Decl* Markup* "}" | "error" Name ("(" Params ")")? ("=>" Expr)? | "context" Name (":" Type)? ("=" Expr)? | "provide" Name "=" Expr -Control ::= "if" Expr "{" … "}" ("else" "{" … "}")? -- no `else if`: nest an `if` in the else +Control ::= "if" Expr "{" … "}" ("else" ("{" … "}" | "if" …))? -- `else if` chains, in markup too | "for" Name "in" Expr "{" … "}" | "loading" ("on" "(" Expr ")")? "fallback" "{" … "}" "{" … "}" | "errored" "fallback" ("(" Name (":" Classes)? ")")? "{" … "}" "{" … "}" Stmt ::= Target ":=" Expr | "guess" Target ":=" Expr | "refresh" Name | "affects" Name | ("let" | "const") Name "=" Expr | "await" Expr | ("let" | "const") Name "=" "await" Expr | "if" "(" Expr ")" … ("else" …)? | "try" … "catch" … "finally" … | "throw" Expr | "return" Expr? | Expr - -- imperative `else if` works; markup `{if}` has no `else if` Expr ::= TypeScript's, without assignment, `this`, `function`, `new` on reactive values | "await" Expr | "pending?" Expr | "latest" Expr | "peek" "(" Expr ")" | "use" Name | Expr "catch" ("(" Name (":" Classes)? ")")? "=>" Expr -- the loosest form: parenthesise to catch one arm diff --git a/ref/src/parse.ts b/ref/src/parse.ts index 0ab607bf..51887551 100644 --- a/ref/src/parse.ts +++ b/ref/src/parse.ts @@ -619,7 +619,7 @@ function parseMarkup(c: Cursor, closeTag: string | null): Markup[] { if (s) out.push({ k: "text", s }); } } -/** Control forms of a view: `if c { … } else { … }`, `for x in e { … }`, `loading … { … }`. Bare at block level, braced inside elements. */ +/** Control forms of a view: `if c { … } else if d { … } else { … }`, `for x in e { … }`, `loading … { … }`. Bare at block level, braced inside elements. */ function parseControl(c: Cursor): Markup | null { const save = c.i; if (c.keyword("if")) { @@ -628,7 +628,14 @@ function parseControl(c: Cursor): Markup | null { c.expect("{"); const t = parseBlockBody(c); c.ws(); let f: { decls: Decl[]; body: Markup[] } = { decls: [], body: [] }; - if (c.keyword("else")) { c.ws(); c.expect("{"); f = parseBlockBody(c); } + if (c.keyword("else")) { + c.ws(); const at = c.i; + if (c.keyword("if")) { // `else if c { … }` is `else { if c { … } }` + c.i = at; const next = parseControl(c); + if (!next) c.fail("a block after `else if`: else if c { … }"); + f = { decls: [], body: [next] }; + } else { c.expect("{"); f = parseBlockBody(c); } + } return { k: "mif", c: cond, t: t.body, tDecls: t.decls, f: f.body, fDecls: f.decls }; } if (c.keyword("loading")) { diff --git a/runtime/package-lock.json b/runtime/package-lock.json index 32a864ce..7f8159b8 100644 --- a/runtime/package-lock.json +++ b/runtime/package-lock.json @@ -1,12 +1,12 @@ { "name": "@reactivescript/runtime", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@reactivescript/runtime", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@solidjs/signals": "2.0.0-rc.21", diff --git a/runtime/package.json b/runtime/package.json index 6fa4a5b7..e5431bbf 100644 --- a/runtime/package.json +++ b/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@reactivescript/runtime", "description": "ReactiveScript's runtime (spec/11 CP-4a): createApp and the __rt helpers compiled .rx calls, over Solid, for the browser and the server", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "repository": { "type": "git", diff --git a/scenarios/S-surface-4.md b/scenarios/S-surface-4.md new file mode 100644 index 00000000..48d14af8 --- /dev/null +++ b/scenarios/S-surface-4.md @@ -0,0 +1,29 @@ +# S-surface-4 — `else if` in markup + +Rules: 07 §5 (Control). + +`else if c { … }` chains a markup conditional: it is `else { if c { … } }`, at +block level and braced inside an element. + +## Program + +```rx +export default component App() { + source n := 3 + +
+

{if n > 2 { many } else if n > 0 { some } else { none }}

+ {if n > 2 { 3+ } else if n === 1 { one } else { other }} + +
+} +``` + +## Frames + +| instant | action | frame | +| ------- | --------------------- | -------------- | +| 0 | mount | `many3+-` | +| 1 | handler `n := n - 1` | `someother-` | +| 2 | handler `n := n - 1` | `someone-` | +| 3 | handler `n := n - 1` | `noneother-` | diff --git a/spec/07-surface.md b/spec/07-surface.md index 185007f4..4f13591e 100644 --- a/spec/07-surface.md +++ b/spec/07-surface.md @@ -444,7 +444,7 @@ ObjPattern ::= "{" (Name (":" Bind | ("=" Expr)?)) ,* ("..." Name)? "}" -- an Bind ::= (Name | Pattern) ("=" Expr)? -- a default applies when the field is `undefined` RBlock ::= Decl* Markup* Markup ::= JSXElement | "{" Expr "}" | Control -Control ::= "if" Expr "{" RBlock "}" ("else" "{" RBlock "}")? +Control ::= "if" Expr "{" RBlock "}" ("else" ("{" RBlock "}" | Control))? -- `else if c {…}` is `else { if c {…} }` | "for" Name "in" Expr "{" RBlock "}" | "loading" ("on" "(" Expr ")")? "fallback" "{" RBlock "}" "{" RBlock "}" | "errored" "fallback" ("(" Name (":" Classes)? ")")? "{" RBlock "}" "{" RBlock "}" diff --git a/spec/09-modules.md b/spec/09-modules.md index 8333995d..8c05cc54 100644 --- a/spec/09-modules.md +++ b/spec/09-modules.md @@ -172,6 +172,10 @@ export component Save(props: { v: unrendered action go catches(nothing) }) unless narrowed at the import. The declaration is unchecked against the JS, as `pure` is (CN-5): it is the boundary's one statement, written once instead of at every import (S-module-2). +- **MO-8c.** A JS import, relative (`./util.js`) or a package's, stays an + ES import in the compiled module, for the bundler to resolve; no + `package.json` `#imports` alias is needed. The one exception is `./api`, + which is the app's injected API (`createApp({ api })`, CP-4). ## MO-9 — A signature read in place of its module diff --git a/tooling/rx-language-server/package-lock.json b/tooling/rx-language-server/package-lock.json index f3d59eb7..9b5b3790 100644 --- a/tooling/rx-language-server/package-lock.json +++ b/tooling/rx-language-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@reactivescript/language-server", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@reactivescript/language-server", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@volar/language-core": "2.4.30", diff --git a/tooling/rx-language-server/package.json b/tooling/rx-language-server/package.json index 448d573e..b0518a48 100644 --- a/tooling/rx-language-server/package.json +++ b/tooling/rx-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@reactivescript/language-server", - "version": "0.1.0", + "version": "0.2.0", "description": "The .rx language server (spec/11 CP-8): Volar over the crate's TSX projection, TypeScript's answers mapped back onto .rx, the crate's own diagnostics; rx-tsc type-checks .rx from the command line.", "license": "MIT", "repository": { diff --git a/tooling/vscode-rx/README.md b/tooling/vscode-rx/README.md index b8170811..294bd6d6 100644 --- a/tooling/vscode-rx/README.md +++ b/tooling/vscode-rx/README.md @@ -27,7 +27,7 @@ artifact of a run of the repository's `editor` workflow, or build one (below), then run **Extensions: Install from VSIX…** in the command palette, or ```sh -code --install-extension reactivescript-0.1.0.vsix +code --install-extension reactivescript-0.2.0.vsix ``` The project needs a `tsconfig.json` with Solid's JSX (`"jsx": "preserve"`, diff --git a/tooling/vscode-rx/package-lock.json b/tooling/vscode-rx/package-lock.json index 4add093e..fa17e156 100644 --- a/tooling/vscode-rx/package-lock.json +++ b/tooling/vscode-rx/package-lock.json @@ -1,12 +1,12 @@ { "name": "reactivescript", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reactivescript", - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { "highlight.js": "^11.11.1", diff --git a/tooling/vscode-rx/package.json b/tooling/vscode-rx/package.json index 2f5cf15f..de5466c3 100644 --- a/tooling/vscode-rx/package.json +++ b/tooling/vscode-rx/package.json @@ -2,7 +2,7 @@ "name": "reactivescript", "displayName": "ReactiveScript", "description": "ReactiveScript (.rx, .d.rx): highlighting, and TypeScript's types, hover, go to definition, completion and errors through the .rx language server, with the compiler's own diagnostics; ```rx blocks highlighted in Markdown.", - "version": "0.1.1", + "version": "0.2.0", "publisher": "solidjs", "license": "MIT", "icon": "icon.png", From 586021f4032ebf007498627bc3b9352b07e0eedf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 18:41:44 +0000 Subject: [PATCH 2/3] WASI build: don't link crt1-reactor.o twice on Rust 1.100 Rust 1.100.0 (released today) links crt1-reactor.o into a wasm32-wasip1 cdylib itself, so the copy build.rs adds defined `_initialize` twice and the wasm32-wasi package failed to link. Add it only before 1.100. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWPLMLsJjr7WiwaUgvd --- compiler/build.rs | 53 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/compiler/build.rs b/compiler/build.rs index 08cacfd7..d153b065 100644 --- a/compiler/build.rs +++ b/compiler/build.rs @@ -67,21 +67,12 @@ fn setup_wasi_emnapi_v1(target: &str) { println!("cargo:rustc-link-arg=--no-check-features"); let rustc = env::var_os("RUSTC").expect("RUSTC must be set by Cargo"); - let sysroot = rustc_sysroot(&rustc); - let crt_reactor_path = sysroot - .join("lib") - .join("rustlib") - .join(target) - .join("lib") - .join("self-contained") - .join("crt1-reactor.o"); - assert!( - crt_reactor_path.is_file(), - "failed to locate crt1-reactor.o for {target} at {}", - crt_reactor_path.display() - ); - println!("cargo:rustc-link-arg={}", crt_reactor_path.display()); - println!("cargo:rustc-link-arg=--export=_initialize"); + // Rust 1.100 links crt1-reactor.o into a WASI cdylib itself; linking it again defines `_initialize` twice. + if rustc_minor(&rustc) >= 100 { + println!("cargo:rustc-link-arg=--export=_initialize"); + } else { + link_crt_reactor(&rustc, target); + } if let Ok(wasi_sdk_path) = env::var("WASI_SDK_PATH") { let wasi_target = if has_threads { @@ -101,6 +92,38 @@ fn setup_wasi_emnapi_v1(target: &str) { } } +fn link_crt_reactor(rustc: &std::ffi::OsStr, target: &str) { + let sysroot = rustc_sysroot(rustc); + let crt_reactor_path = sysroot + .join("lib") + .join("rustlib") + .join(target) + .join("lib") + .join("self-contained") + .join("crt1-reactor.o"); + assert!( + crt_reactor_path.is_file(), + "failed to locate crt1-reactor.o for {target} at {}", + crt_reactor_path.display() + ); + println!("cargo:rustc-link-arg={}", crt_reactor_path.display()); + println!("cargo:rustc-link-arg=--export=_initialize"); +} + +/// rustc's minor version (`rustc 1.100.0 (…)` is 100). +fn rustc_minor(rustc: &std::ffi::OsStr) -> u32 { + let output = Command::new(rustc) + .arg("--version") + .output() + .unwrap_or_else(|err| panic!("failed to execute rustc --version: {err}")); + let text = String::from_utf8_lossy(&output.stdout); + text.split_whitespace() + .nth(1) + .and_then(|v| v.split('.').nth(1)) + .and_then(|m| m.parse().ok()) + .unwrap_or_else(|| panic!("unexpected rustc --version: {text}")) +} + fn rustc_sysroot(rustc: &std::ffi::OsStr) -> PathBuf { let output = Command::new(rustc) .args(["--print", "sysroot"]) From 2727a052be24d982a227f5a2a0a28b756d741819 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 18:50:29 +0000 Subject: [PATCH 3/3] Pin the compiler builds to Rust 1.99.1 Rust 1.100.0 (today) breaks the wasm32-wasip1-threads package: its wasi-libc defines __wasilibc_futex_wait_atomic_wait with a signature emnapi 1.x does not match (an invalid module), and it links crt1-reactor.o itself. Reverts the build.rs workaround for the second, which the first makes moot. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWPLMLsJjr7WiwaUgvd --- .github/workflows/compiler.yml | 6 +++- compiler/build.rs | 53 ++++++++++------------------------ 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/.github/workflows/compiler.yml b/.github/workflows/compiler.yml index 8e82393e..0faa5d84 100644 --- a/.github/workflows/compiler.yml +++ b/.github/workflows/compiler.yml @@ -34,8 +34,12 @@ jobs: shell: bash steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable + # Pinned: Rust 1.100's wasi-libc defines __wasilibc_futex_wait_atomic_wait with a signature emnapi 1.x's + # libemnapi-napi-rs-mt.a does not match, and links crt1-reactor.o itself, so the WASI package breaks. Lift once + # emnapi follows. + - uses: dtolnay/rust-toolchain@master with: + toolchain: 1.99.1 targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 with: diff --git a/compiler/build.rs b/compiler/build.rs index d153b065..08cacfd7 100644 --- a/compiler/build.rs +++ b/compiler/build.rs @@ -67,12 +67,21 @@ fn setup_wasi_emnapi_v1(target: &str) { println!("cargo:rustc-link-arg=--no-check-features"); let rustc = env::var_os("RUSTC").expect("RUSTC must be set by Cargo"); - // Rust 1.100 links crt1-reactor.o into a WASI cdylib itself; linking it again defines `_initialize` twice. - if rustc_minor(&rustc) >= 100 { - println!("cargo:rustc-link-arg=--export=_initialize"); - } else { - link_crt_reactor(&rustc, target); - } + let sysroot = rustc_sysroot(&rustc); + let crt_reactor_path = sysroot + .join("lib") + .join("rustlib") + .join(target) + .join("lib") + .join("self-contained") + .join("crt1-reactor.o"); + assert!( + crt_reactor_path.is_file(), + "failed to locate crt1-reactor.o for {target} at {}", + crt_reactor_path.display() + ); + println!("cargo:rustc-link-arg={}", crt_reactor_path.display()); + println!("cargo:rustc-link-arg=--export=_initialize"); if let Ok(wasi_sdk_path) = env::var("WASI_SDK_PATH") { let wasi_target = if has_threads { @@ -92,38 +101,6 @@ fn setup_wasi_emnapi_v1(target: &str) { } } -fn link_crt_reactor(rustc: &std::ffi::OsStr, target: &str) { - let sysroot = rustc_sysroot(rustc); - let crt_reactor_path = sysroot - .join("lib") - .join("rustlib") - .join(target) - .join("lib") - .join("self-contained") - .join("crt1-reactor.o"); - assert!( - crt_reactor_path.is_file(), - "failed to locate crt1-reactor.o for {target} at {}", - crt_reactor_path.display() - ); - println!("cargo:rustc-link-arg={}", crt_reactor_path.display()); - println!("cargo:rustc-link-arg=--export=_initialize"); -} - -/// rustc's minor version (`rustc 1.100.0 (…)` is 100). -fn rustc_minor(rustc: &std::ffi::OsStr) -> u32 { - let output = Command::new(rustc) - .arg("--version") - .output() - .unwrap_or_else(|err| panic!("failed to execute rustc --version: {err}")); - let text = String::from_utf8_lossy(&output.stdout); - text.split_whitespace() - .nth(1) - .and_then(|v| v.split('.').nth(1)) - .and_then(|m| m.parse().ok()) - .unwrap_or_else(|| panic!("unexpected rustc --version: {text}")) -} - fn rustc_sysroot(rustc: &std::ffi::OsStr) -> PathBuf { let output = Command::new(rustc) .args(["--print", "sysroot"])