Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/compiler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion compiler/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion compiler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-darwin-arm64",
"version": "0.1.0",
"version": "0.2.0",
"cpu": [
"arm64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-darwin-x64",
"version": "0.1.0",
"version": "0.2.0",
"cpu": [
"x64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-linux-arm64-gnu",
"version": "0.1.0",
"version": "0.2.0",
"cpu": [
"arm64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-linux-x64-gnu",
"version": "0.1.0",
"version": "0.2.0",
"cpu": [
"x64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/wasm32-wasi/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/win32-x64-msvc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-win32-x64-msvc",
"version": "0.1.0",
"version": "0.2.0",
"cpu": [
"x64"
],
Expand Down
4 changes: 2 additions & 2 deletions compiler/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion compiler/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
14 changes: 10 additions & 4 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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})"));
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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")
Expand Down
16 changes: 13 additions & 3 deletions compiler/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1888,7 +1888,7 @@ fn parse_markup(c: &mut Cursor, close: Close) -> R<Vec<Markup>> {
}
}

/// 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<Option<Markup>> {
let save = c.i;
let mk = |kind| Some(Markup { kind, line: None, span: Span::NONE });
Expand All @@ -1906,8 +1906,18 @@ fn parse_control(c: &mut Cursor) -> R<Option<Markup>> {
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 }));
}
Expand Down
19 changes: 19 additions & 0 deletions compiler/test/node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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',
' <p onClick={() => { save(n) }}>{greet(n)}</p>',
'}',
].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);
3 changes: 1 addition & 2 deletions guide/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions ref/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand All @@ -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")) {
Expand Down
4 changes: 2 additions & 2 deletions runtime/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion runtime/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
29 changes: 29 additions & 0 deletions scenarios/S-surface-4.md
Original file line number Diff line number Diff line change
@@ -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

<main>
<p>{if n > 2 { <b>many</b> } else if n > 0 { <i>some</i> } else { <u>none</u> }}</p>
{if n > 2 { <span>3+</span> } else if n === 1 { <span>one</span> } else { <span>other</span> }}
<button onClick={() => { n := n - 1 }}>-</button>
</main>
}
```

## 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-` |
2 changes: 1 addition & 1 deletion spec/07-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "}"
Expand Down
4 changes: 4 additions & 0 deletions spec/09-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions tooling/rx-language-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tooling/rx-language-server/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion tooling/vscode-rx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`,
Expand Down
Loading
Loading