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
4 changes: 4 additions & 0 deletions compiler/src/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import type { JSX } from "@solidjs/web/jsx-runtime";
export declare function await_<T>(v: T): Awaited<T>;
export declare function awaitS<T>(v: T): T extends AsyncIterable<infer U> ? U : Awaited<T>;
/** 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<T> = 0 extends (1 & T) ? false : true extends (T extends PromiseLike<any> | AsyncIterable<any> ? true : false) ? true : false;
export declare function eq<T>(v: T, ok: Async<T> 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<T, I>(v: T, init: I): T | I;
export declare function pre<T>(init?: T): T;
export declare function pending(v: unknown): boolean;
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, .. } => {
Expand Down
8 changes: 6 additions & 2 deletions ref/types/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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_<T>(v: T): Awaited<T>;
export declare function awaitS<T>(v: T): T extends AsyncIterable<infer U> ? U : Awaited<T>;
/** 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<T> = 0 extends (1 & T) ? false : true extends (T extends PromiseLike<any> | AsyncIterable<any> ? true : false) ? true : false;
export declare function eq<T>(v: T, ok: Async<T> 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<T, I>(v: T, init: I): T | I;
export declare function pre<T>(init?: T): T;
export declare function pending(v: unknown): boolean;
Expand Down Expand Up @@ -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") {
Expand Down
2 changes: 1 addition & 1 deletion ref/types/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")}`);
Expand Down
31 changes: 30 additions & 1 deletion scenarios/S-types-1.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)

<p>{id}</p>
}
```

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

<p>{id}</p>
}
```

A promise on one branch is enough: on that branch Solid consumes it.
19 changes: 16 additions & 3 deletions spec/04-rows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
4 changes: 4 additions & 0 deletions spec/13-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions spec/99-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading