diff --git a/compiler/src/prelude.ts b/compiler/src/prelude.ts index feabdae..f7867a8 100644 --- a/compiler/src/prelude.ts +++ b/compiler/src/prelude.ts @@ -2,6 +2,10 @@ import type { JSX } from "@solidjs/web/jsx-runtime"; export declare function await_(v: T): Awaited; export declare function awaitS(v: T): T extends AsyncIterable ? U : Awaited; +/** R-1: Solid's memo consumes a promise or a stream its function returns, so an equation's value is never one — `await` says + * where it enters (`any`: not known here; Solid still consumes it). */ +type Async = 0 extends (1 & T) ? false : true extends (T extends PromiseLike | AsyncIterable ? true : false) ? true : false; +export declare function eq(v: T, ok: Async extends true ? "MISSING_AWAIT: an equation's value is a promise or a stream, which Solid consumes; write await e (or await* e for a stream)" : true): T; export declare function first(v: T, init: I): T | I; export declare function pre(init?: T): T; export declare function pending(v: unknown): boolean; diff --git a/compiler/src/project.rs b/compiler/src/project.rs index 3fac266..889603c 100644 --- a/compiler/src/project.rs +++ b/compiler/src/project.rs @@ -496,7 +496,7 @@ impl Proj<'_> { Decl::Eq { name, e, line, ty, .. } => { let nm = self.name(name, sp, &mut at); let a = self.ann(ty, sp, &mut at); - let v = self.expr(e, false, Some(name)); + let v = Code::of("__.eq(").c(self.expr(e, false, Some(name))).t(", true)"); // R-1: no promise without `await` self.w.emit(Code::of(ind).t(ex(d)).t("const ").c(nm).c(a).t(" = ").c(v).t(";").from(sp), *line); } Decl::Provide { name, e, line, .. } => { diff --git a/ref/types/project.ts b/ref/types/project.ts index 4d6b990..57cce21 100644 --- a/ref/types/project.ts +++ b/ref/types/project.ts @@ -2,7 +2,7 @@ // `projectTsrxForTypecheck` does for `.tsrx` — and TypeScript's diagnostics are reported on `.rx` lines. Types stay // TypeScript's (07 §2); the projection only says what each form is, as a value, to TypeScript: // a source or an equation is a `const` of its value's type (reads are values, 02 V-1); a write is `__.set(target, v)`, -// which checks `v` against the target; an equation's `await` is `__.await(e)`, its answer's type; a view is JSX over +// which checks `v` against the target; an equation's `await` is `__.await(e)`, its answer's type, and an equation is `__.eq(e)`, never a promise (R-1); a view is JSX over // Solid's JSX types, so a component's props and what a reader may insert are checked where they are written. // The projection is for checking only: it is never run, and its order is the dependency order (E-7). import type { Decl, Expr, Markup, Program, Stmt, Target } from "../src/ast.ts"; @@ -25,6 +25,10 @@ export const PRELUDE = `// ReactiveScript's forms, as TypeScript sees them (type import type { JSX } from "@solidjs/web/jsx-runtime"; export declare function await_(v: T): Awaited; export declare function awaitS(v: T): T extends AsyncIterable ? U : Awaited; +/** R-1: Solid's memo consumes a promise or a stream its function returns, so an equation's value is never one — \`await\` says + * where it enters (\`any\`: not known here; Solid still consumes it). */ +type Async = 0 extends (1 & T) ? false : true extends (T extends PromiseLike | AsyncIterable ? true : false) ? true : false; +export declare function eq(v: T, ok: Async extends true ? "MISSING_AWAIT: an equation's value is a promise or a stream, which Solid consumes; write await e (or await* e for a stream)" : true): T; export declare function first(v: T, init: I): T | I; export declare function pre(init?: T): T; export declare function pending(v: unknown): boolean; @@ -161,7 +165,7 @@ class Proj { } for (const d of this.order(ds)) { if (d.k === "source") this.w.emit(`${ind}${ex(d)}const ${d.name}${ann(d.type)} = ${typeText(d.type) ? `__.typed<${typeText(d.type)}>(${this.expr(d.init, false)})` : `__.src(${this.expr(d.init, false)})`};`, d.line); - if (d.k === "eq") this.w.emit(`${ind}${ex(d)}const ${d.name}${ann(d.type)} = ${this.expr(d.e, false, d.name)};`, d.line); + if (d.k === "eq") this.w.emit(`${ind}${ex(d)}const ${d.name}${ann(d.type)} = __.eq(${this.expr(d.e, false, d.name)}, true);`, d.line); // R-1: no promise without `await` if (d.k === "provide") this.w.emit(`${ind}__.provide(${d.name}, ${this.expr(d.e, false)});`, d.line); } for (const d of ds) if (d.k === "effect") { diff --git a/ref/types/run.ts b/ref/types/run.ts index 7ffd9cd..e06ac25 100644 --- a/ref/types/run.ts +++ b/ref/types/run.ts @@ -64,7 +64,7 @@ for (const u of units) { const got: string[] = []; for (const [f, p] of u.files) for (const d of byFile.get(f) ?? []) { const line = d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line : 0; - const msg0 = ts.flattenDiagnosticMessageText(d.messageText, " "); const own = /"(WIRE_VALUE): ([^"]*)"/.exec(msg0); // a rule the projection states as a type + const msg0 = ts.flattenDiagnosticMessageText(d.messageText, " "); const own = /"(WIRE_VALUE|MISSING_AWAIT): ([^"]*)"/.exec(msg0); // a rule the projection states as a type if (own) { got.push(`${own[1]} ${p.rx.split("/").pop()}:${p.lines[line] ?? "?"}: ${own[2]}`); continue; } got.push(`TS${d.code} ${p.rx.split("/").pop()}:${p.lines[line] ?? "?"}: ${ts.flattenDiagnosticMessageText(d.messageText, " ").slice(0, Number(process.env.MSG ?? 220))}`); if (process.env.DEBUG) console.log(`--- ${f}\n${p.code.split("\n").map((l, i) => `${String(i + 1).padStart(3)}${i === line ? ">" : " "} ${l}`).join("\n")}`); diff --git a/scenarios/S-types-1.md b/scenarios/S-types-1.md index 0e97c20..948ef61 100644 --- a/scenarios/S-types-1.md +++ b/scenarios/S-types-1.md @@ -1,6 +1,6 @@ # S-types-1 — Types: the program as TypeScript sees it -Rules exercised: TY-1–TY-4 (08 §TY), 07 §2. +Rules exercised: TY-1–TY-5 (08 §TY), 07 §2, 04 R-1a. ## Modules @@ -170,3 +170,32 @@ export default component App() { SF-6a, statically: a `Date` is an instance of a class, so the result is not data. (Untyped, the check is the run time's, S-sf-1.) + +``` +-- types: MISSING_AWAIT +import { fetchUser } from "./api" +export default component App() { + source id := 1 + user = fetchUser(id) + +

{id}

+} +``` + +R-1: the equation lowers to a memo, and Solid's memo consumes the promise +its function returns, so `user` would be pending at run time while the +source says it is a value. The value is never a promise: write +`await fetchUser(id)`. + +``` +-- types: MISSING_AWAIT +import { fetchUser } from "./api" +export default component App() { + source id := 1 + user = id > 0 ? fetchUser(id) : null + +

{id}

+} +``` + +A promise on one branch is enough: on that branch Solid consumes it. diff --git a/spec/04-rows.md b/spec/04-rows.md index b30927e..00cd3e2 100644 --- a/spec/04-rows.md +++ b/spec/04-rows.md @@ -43,9 +43,21 @@ and where they may not go. - **R-1 (Entry).** `pending` enters only through `await e` in reactive scope. `await` is the row marker, not a scheduler: if `e` is already a value, `await e` is `e` in the same instant with no tick; if `e` is a promise or an - async iterable, the equation is pending until the first answer lands. A - Promise-valued expression without `await` is an ordinary value of type - Promise and is never pending. + async iterable, the equation is pending until the first answer lands. +- **R-1a (No promise without `await`).** [ruled 2026-10-28] An equation's + value is never a promise or an async iterable. It lowers to a memo, and + Solid's memo consumes one its function returns, so `x = fetchUser(id)` + would be pending at run time while the source says it is a value. Where + the type is known, an equation whose value may be a promise or a stream + (on any branch) is `MISSING_AWAIT`, checked by the type layer (13 TY-5b): + write `await e` or `await* e`. A promise inside a value + (`{ p: fetchUser(id) }`, `[a, b]`) is not consumed and is passed as one. + Where the type is `any`, the check cannot see it and Solid still consumes + the promise; the rows (R-3) and the AC checks then miss that entry. Where + more could be done: refuse an `any`-typed call into a JS import in an + equation without `await`; noted, not adopted. (Replaces "a Promise-valued + expression without `await` is an ordinary value of type Promise and is + never pending", which the lowering contradicted.) - **R-2 (Flow).** An equation that reads a pending value is pending. A reader of a pending value is a pending reader (05 decides what that does to the screen). An effect whose compute half is pending does not run its apply @@ -244,6 +256,7 @@ tracked. Inspired by solid-yield's typed failure color; the checks are | `PENDING_ASYNC_UNTRACKED_READ` | U | R-8 | | `PENDING_ASYNC_FORBIDDEN_SCOPE` | U | R-8 | | `SYNC_NODE_RECEIVED_ASYNC` | U | R-1 | +| `MISSING_AWAIT` (new) | S | R-1a: an equation whose value may be a promise or a stream (13 TY-5b) | | `ASYNC_WATERFALL` | S | an `await` whose inputs depend on another `await` (R-2) | | `UNTRACKED_ASYNC_HANDLER` (structural half) | S | a handler that awaits with no write before and no `action` | | `UNHANDLED_FAILURE` (new) | S | R-f-5 | diff --git a/spec/13-types.md b/spec/13-types.md index 7aa2a89..6e07c68 100644 --- a/spec/13-types.md +++ b/spec/13-types.md @@ -75,6 +75,10 @@ does for `.tsrx`. Nothing here is a second type system. checked to be data: a function, or an instance of a class (a `Date`), where the types are known is `WIRE_VALUE` at compile time. Where they are `any` — an untyped parameter — the run-time check stands. +- **TY-5b (R-1a statically).** An equation is projected as `__.eq(e)`, + whose type is `e`'s and which refuses a promise or an async iterable on + any branch: `MISSING_AWAIT`. Where the type is `any` the check is silent + (04 R-1a). (Ruled 2026-10-28; S-types-1.) ## Found by the type layer (2026-10-25) diff --git a/spec/99-diagnostics.md b/spec/99-diagnostics.md index 9088bc3..04aa67c 100644 --- a/spec/99-diagnostics.md +++ b/spec/99-diagnostics.md @@ -133,6 +133,7 @@ named. | `CONTENT_MODEL` | S | 10 SV-7d: nesting the HTML parser restructures, across templates (component roots, branches, placed children) | S-server-4 | | `IMPORT_ROW_MISMATCH` (SF-5) | S | `server`/`client` on an import that a `.d.rx` declares otherwise | S-sf-1 | | `WIRE_VALUE` | S/R | 12 SF-6a: at compile time where the types are known (13 TY-5a), else at run time | S-sf-1, S-types-1 | +| `MISSING_AWAIT` (new) | S | 04 R-1a: an equation whose value may be a promise or a stream, which Solid's memo would consume; write `await e` (13 TY-5b) | S-types-1 | | `TSnnnn` | S | 13 TY-1: TypeScript's errors over the projection, on `.rx` lines | S-types-1 | ## The advisory rows (ruled 2026-10-23, in part)