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/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",