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
39 changes: 39 additions & 0 deletions compiler/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ impl Diagnostic {
}

const BUILTINS: &[&str] = &["len", "str", "map", "filter", "sum", "fail", "fetch", "every"];
/// EF-5's cleanup shapes: a `return` of a closure anywhere in the body, a closure as its last expression, or an expression body that is one.
fn returns_closure(f: &Expr) -> bool {
fn in_stmts(ss: &[Stmt]) -> bool {
ss.iter().any(|s| match s {
Stmt::Return(Some(Expr::Fn { .. })) => true,
Stmt::Sif { t, f, .. } => in_stmts(t) || in_stmts(f),
Stmt::Try { body, handler, fin, .. } => in_stmts(body) || handler.as_deref().is_some_and(in_stmts) || fin.as_deref().is_some_and(in_stmts),
_ => false,
})
}
match f {
Expr::Fn { stmts: None, body, .. } => matches!(**body, Expr::Fn { .. }),
Expr::Fn { stmts: Some(ss), .. } => in_stmts(ss) || matches!(ss.last(), Some(Stmt::Expr(Expr::Fn { .. }))),
_ => false,
}
}

fn is_handler(attr: &str) -> bool {
let b = attr.as_bytes();
b.len() > 2 && b[0] == b'o' && b[1] == b'n' && (b[2].is_ascii_uppercase() || b[2] == b':')
Expand Down Expand Up @@ -2020,6 +2037,28 @@ impl<'a> Checker<'a> {
self.expr(&args[0], env, Ctx { server: ctx.server, ..Ctx::reactive() }, None);
return;
}
// EF-7, EF-8: Solid's onSettled / onFlush, functions of the language
if (fname == "onSettled" || fname == "onFlush") && self.lookup(env, fname).is_none() {
let settled = fname == "onSettled";
let rule = if settled { "EF-7" } else { "EF-8" };
if !ctx.imperative {
self.err("IO_IN_REACTIVE", rule, if settled { "onSettled(…) schedules work from imperative scope (a handler, an apply block, an action); in a component body the mount hook is `effect () { … }` (EF-4)".into() } else { "onFlush(…) is called from imperative scope (a handler, an apply block, an action): it starts a run of its own after this flush".into() });
}
let ok = args.len() == 1 && matches!(&args[0], Expr::Fn { params, .. } if params.is_empty());
if !ok {
self.err("HOOK_CALLBACK", rule, format!("{} takes a closure, `{}(() => {{ … }})`: its body is the run it schedules", fname, fname));
for a in args {
self.expr(a, env, ctx, None);
}
return;
}
if settled && returns_closure(&args[0]) {
self.err("SETTLED_CLEANUP_UNOWNED", "EF-7", "an onSettled callback returns a cleanup, but it has no region to bind it to: setup with teardown is `effect () { … }` (EF-4, EF-5)".into());
}
// the callback is imperative scope
self.expr(&args[0], env, Ctx { server: ctx.server, ..Ctx::imperative() }, None);
return;
}
let s = self.lookup(env, fname);
let (io, kind, side, io_via, instantiating) = match s {
Some(s) => (self.syms[s].io, Some(self.syms[s].kind), self.syms[s].side, self.syms[s].io_via.clone(), self.syms[s].instantiating),
Expand Down
10 changes: 10 additions & 0 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,16 @@ impl<'p> Gen<'p> {
return Ok(rtc("until", vec![thunk(self.expr(body, env, &Ctx { dynamic: ctx.dynamic, ..Ctx::default() })?)]));
}
}
// EF-7, EF-8: Solid's own
if let Expr::Id(n) = &**f {
if (n == "onSettled" || n == "onFlush") && self.lookup(env, n).is_none() {
let mut xs = vec![];
for a in args {
xs.push(self.expr(a, env, &sub)?);
}
return Ok(rtc(n, xs));
}
}
let callee = if let Expr::Member { o, p, .. } = &**f { E::Member(Box::new(self.expr(o, env, &sub)?), p.clone(), true) } else { self.expr(f, env, &sub)? };
let mut xs = vec![];
for a in args {
Expand Down
1 change: 1 addition & 0 deletions ref/compile/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ class Gen {
case "stylemap": return this.classMap(e.css, e.line); // 07 CSS-5
case "call": {
if (e.f.k === "id" && e.f.name === "until" && !env.lookup("until") && e.args[0]?.k === "fn") { const p = e.args[0]; return `__rt.until(() => ${this.expr(p.body, env, { imp: false, dyn: ctx.dyn })})`; } // AC-6: Solid's until; the predicate is reactive scope
if (e.f.k === "id" && (e.f.name === "onSettled" || e.f.name === "onFlush") && !env.lookup(e.f.name)) return `__rt.${e.f.name}(${e.args.map(go).join(", ")})`; // EF-7, EF-8: Solid's own
if (e.f.k === "member") return `${go(e.f.o)}?.${e.f.p}(${e.args.map(go).join(", ")})`;
return `${go(e.f)}(${e.args.map(go).join(", ")})`;
}
Expand Down
2 changes: 2 additions & 0 deletions ref/compile/rt-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ export function createApp(opts: { api?: Record<string, (...a: any[]) => unknown>
hoisted(_name: string, _get: Acc) {},
refresh: onServer("refresh"),
affects: onServer("affects"),
onSettled: onServer("onSettled"), // EF-7: imperative scope only, and no imperative run happens here (SV-2a)
onFlush: onServer("onFlush"), // EF-8
preOr: (v: unknown, init: () => unknown) => v === undefined ? init() : v,

module(key: unknown, factory: (rt: unknown) => Record<string, unknown>) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; },
Expand Down
4 changes: 3 additions & 1 deletion ref/compile/rt-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getObserver } from "solid-js";
import {
createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, createErrorBoundary, createContext,
Show, For, Loading, Errored, createComponent,
flush, untrack, latest, isPending, action, refresh, affects, until, NotReadyError, getOwner, runWithOwner, sharedConfig,
flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, getOwner, runWithOwner, sharedConfig,
} from "solid-js";
import { getContext, setContext, $TARGET } from "@solidjs/signals";
import { hydrate, render } from "@solidjs/web";
Expand Down Expand Up @@ -230,6 +230,8 @@ export function createApp(opts: { root: any; api?: Record<string, (...a: any[])
},
hoisted(name: string, get: Acc) { let l = hoistedBy.get(name); if (!l) hoistedBy.set(name, (l = [])); l.push(get); },
until: (p: () => unknown) => until(p), // AC-6
onSettled: (f: () => void) => onSettled(f), // EF-7
onFlush: (f: () => void) => onFlush(f), // EF-8
refresh(name: string, get: Acc) { const key = lastKey.get(name); if (key) flights.delete(key); for (const h of hoistedBy.get(name) ?? []) refresh(h as any); refresh((storeAccs.has(get) ? get() : get) as any); },
affects: (get: Acc) => affects(get as any),
preOr: (v: unknown, init: () => unknown) => v === undefined ? init() : v,
Expand Down
4 changes: 3 additions & 1 deletion ref/compile/rt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getObserver } from "@solidjs/signals";
import { Show, For, Loading, Errored } from "solid-js";
import {
createSignal, createMemo, createEffect, createRenderEffect, createRoot, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred,
flush, untrack, latest, isPending, action, refresh, affects, until, NotReadyError, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary,
flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary,
} from "@solidjs/signals";

export { flush };
Expand Down Expand Up @@ -238,6 +238,8 @@ export function createApp(opts: { api?: Record<string, (...a: any[]) => unknown>
},
hoisted(name: string, get: Acc) { let l = hoistedBy.get(name); if (!l) hoistedBy.set(name, (l = [])); l.push(get); },
until: (p: () => unknown) => until(p), // AC-6
onSettled: (f: () => void) => onSettled(f), // EF-7
onFlush: (f: () => void) => onFlush(f), // EF-8
refresh(name: string, get: Acc) { const key = lastKey.get(name); if (key) flights.delete(key); for (const h of hoistedBy.get(name) ?? []) refresh(h as any); refresh((storeAccs.has(get) ? get() : get) as any); },
affects: (get: Acc) => affects(get as any),
preOr: (v: unknown, init: () => unknown) => v === undefined ? init() : v,
Expand Down
2 changes: 1 addition & 1 deletion ref/compile/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function runTable(sc: Scenario, t: Scenario["tables"][number], base: Scena
const src = t.program ?? sc.program;
let rows = t.rows; let own = 0;
if (t !== base && base && rows.length && rows[0].instant > 0) { const pre = base.rows.filter(r => r.instant < rows[0].instant); own = pre.length; rows = [...pre, ...rows]; }
const chains: typeof rows[] = []; for (const r of rows) { if (r.action === "(effect)" && chains.length) chains[chains.length - 1].push(r); else chains.push([r]); }
const chains: typeof rows[] = []; for (const r of rows) { if ((r.action === "(effect)" || r.action === "(flush)") && chains.length) chains[chains.length - 1].push(r); else chains.push([r]); }
resetErrorHalt();
let entry: string, views: Map<string, string>;
try {
Expand Down
3 changes: 2 additions & 1 deletion ref/oracle/lower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Show, For, Loading, Errored } from "solid-js";
import { getObserver } from "@solidjs/signals";
import {
createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred,
createRoot, createRenderEffect, flush, untrack, latest, isPending, action, refresh, affects, until, NotReadyError, resetErrorHalt, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary
createRoot, createRenderEffect, flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, resetErrorHalt, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary
} from "@solidjs/signals";

const noFlight = (v: unknown) => { if (v instanceof Flight || v instanceof Promise) throw new Unsupported("`await` not in tail position (the oracle lowers a tail `await` to the memo's result)"); };
Expand Down Expand Up @@ -416,6 +416,7 @@ export class Oracle {
case "call": {
let f: any, self: any;
if (e.f.k === "id" && e.f.name === "until" && !env?.has("until") && !scope.lookup("until")) { const p = e.args[0]; if (p?.k !== "fn") throw new Error("UNTIL_PREDICATE"); return until(() => this.ev(p.body, scope, env)); } // AC-6: Solid's until
if (e.f.k === "id" && (e.f.name === "onSettled" || e.f.name === "onFlush") && !env?.has(e.f.name) && !scope.lookup(e.f.name)) { const cb = this.ev(e.args[0], scope, env) as () => void; return (e.f.name === "onSettled" ? onSettled : onFlush)(cb); } // EF-7, EF-8: Solid's own
if (e.f.k === "member") { self = this.ev(e.f.o, scope, env); f = self == null ? undefined : self[e.f.p]; } else f = this.ev(e.f, scope, env);
const args = e.args.map(a => this.ev(a, scope, env));
if (f === BUILTINS.fetch) { const fl = this.flight(String(args[0])); return process.env.ORACLE_PROMISES ? fl.first : fl; }
Expand Down
4 changes: 2 additions & 2 deletions ref/oracle/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ async function runTable(sc: Scenario, t: Scenario["tables"][number], base: Scena
const src = t.program ?? sc.program;
let rows = t.rows; let own = 0;
if (t !== base && base && rows.length && rows[0].instant > 0) { const pre = base.rows.filter(r => r.instant < rows[0].instant); own = pre.length; rows = [...pre, ...rows]; }
// chains: a row followed by its `(effect)` rows is one flush here
const chains: typeof rows[] = []; for (const r of rows) { if (r.action === "(effect)" && chains.length) chains[chains.length - 1].push(r); else chains.push([r]); }
// chains: a row followed by its `(effect)` and `(flush)` rows (EF-8) is one step here
const chains: typeof rows[] = []; for (const r of rows) { if ((r.action === "(effect)" || r.action === "(flush)") && chains.length) chains[chains.length - 1].push(r); else chains.push([r]); }
let o: Oracle;
resetErrorHalt(); (await import("./lower.ts")).resetScopes(); Unsupported.last = "";
const handlerStmts = rows.filter(r => r.action.startsWith("handler")).map(r => { try { return parseStmts(r.action.slice(7)); } catch { return []; } });
Expand Down
16 changes: 16 additions & 0 deletions ref/src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ const noFails = (): Fails => ({ cls: new Set(), props: [] });
export const keyName = (k: string) => k.split("#").pop()!.replace(/@\d+$/, "");
const addF = (a: Fails, b: Fails) => { for (const x of b.cls) a.cls.add(x); a.props.push(...b.props); return a; };
const catchOf = (classes?: string[]): Catch => classes ? new Set(classes) : "all";
/** EF-5's cleanup shapes: a `return` of a closure anywhere in the body, a closure as its last expression, or an expression body that is one. */
function returnsClosure(f: Extract<Expr, { k: "fn" }>): boolean {
const inStmts = (ss: Stmt[]): boolean => ss.some(s => (s.k === "return" && s.e?.k === "fn") || (s.k === "sif" && (inStmts(s.t) || inStmts(s.f))) || (s.k === "try" && (inStmts(s.body) || inStmts(s.handler ?? []) || inStmts(s.fin ?? []))));
if (!f.stmts) return f.body.k === "fn";
const last = f.stmts[f.stmts.length - 1];
return inStmts(f.stmts) || (last?.k === "expr" && last.e.k === "fn");
}
/** A short rendering of an expression, for messages. */
function show(e: Expr): string {
switch (e.k) {
Expand Down Expand Up @@ -621,6 +628,15 @@ class Checker {
this.expr(p, env, { scope: "reactive", server: ctx.server }); // the predicate is reactive scope: it reads, tracked; it does not write
return;
}
if (e.f.k === "id" && (e.f.name === "onSettled" || e.f.name === "onFlush") && !env.lookup(e.f.name)) { // EF-7, EF-8: Solid's onSettled / onFlush, functions of the language
const name = e.f.name;
if (ctx.scope === "reactive") this.err("IO_IN_REACTIVE", name === "onSettled" ? "EF-7" : "EF-8", name === "onSettled" ? "onSettled(…) schedules work from imperative scope (a handler, an apply block, an action); in a component body the mount hook is `effect () { … }` (EF-4)" : "onFlush(…) is called from imperative scope (a handler, an apply block, an action): it starts a run of its own after this flush");
const p = e.args[0];
if (e.args.length !== 1 || !p || p.k !== "fn" || p.params.length) { this.err("HOOK_CALLBACK", name === "onSettled" ? "EF-7" : "EF-8", `${name} takes a closure, \`${name}(() => { … })\`: its body is the run it schedules`); e.args.forEach(go); return; }
if (name === "onSettled" && returnsClosure(p)) this.err("SETTLED_CLEANUP_UNOWNED", "EF-7", "an onSettled callback returns a cleanup, but it has no region to bind it to: setup with teardown is `effect () { … }` (EF-4, EF-5)");
this.expr(p, env, { scope: "imperative", server: ctx.server }); // the callback is imperative scope
return;
}
if (e.f.k === "id") {
const s = env.lookup(e.f.name);
if (ctx.scope === "reactive" && (s?.io || (!s && e.f.name === "log"))) {
Expand Down
Loading
Loading