Skip to content
17 changes: 15 additions & 2 deletions compiler/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ struct Sym<'a> {
/// O-3: not a name but a prop's argument, stored — its captures are the caller's ("*": all of props)
prop_of: Option<(RId, String)>,
maps_records: bool,
/// a declared equation, and whether it is deferred: a guess may sit over one that is not (AC-3, ST-5c)
eq_decl: Option<bool>,
rows: Option<RowsT<'a>>,
comp: Option<&'a Decl>,
expr: Option<(&'a Expr, EId)>,
Expand Down Expand Up @@ -253,6 +255,7 @@ impl<'a> Sym<'a> {
ret_captures: None,
prop_of: None,
maps_records: false,
eq_decl: None,
rows: None,
comp: None,
expr: None,
Expand Down Expand Up @@ -1166,7 +1169,8 @@ impl<'a> Checker<'a> {
let r = self.region(Some(region), "component", true, Some(name.clone()));
sym.comp_region = Some(r);
}
Decl::Eq { e, key, .. } => {
Decl::Eq { e, key, deferred, .. } => {
sym.eq_decl = Some(*deferred);
sym.maps_records = key.is_none() && maps_into_records(e);
if let ExprKind::Id(n) = &e.k {
if let Some(t) = self.lookup(env, n) {
Expand Down Expand Up @@ -2332,7 +2336,16 @@ impl<'a> Checker<'a> {
self.model.write(tsym, op, !target.path.is_empty());
if let Some(t) = tsym {
let k = self.syms[t].kind;
if k != SymKind::Source {
// AC-3: a guess may sit over an equation (Solid's createOptimistic(fn)); it is not a write of the truth
let guessable = op == "optimistic" && k == SymKind::Eq && self.syms[t].eq_decl.is_some() && !self.syms[t].param && self.syms[t].imported.is_none();
if guessable && self.syms[t].eq_decl == Some(true) {
self.err(
"GUESS_ON_DEFERRED",
"AC-3",
format!("{} is deferred: its value is a lagging copy (V-16), so nothing would confirm a guess on it — guess the equation it lags", target.name),
);
} else if guessable {
} else if k != SymKind::Source {
let what = match k {
SymKind::Eq => "an equation".to_string(),
SymKind::Props => "props".to_string(),
Expand Down
22 changes: 21 additions & 1 deletion compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,11 @@ impl<'p> Gen<'p> {
}
// SF-1d: a server fn, not an accessor
Decl::Eq { name, e, .. } if unwrap_server(e).is_some() => self.set(env, name, val(mg(name))),
Decl::Eq { name, .. } => {
Decl::Eq { name, e, deferred, .. } => {
let mut b = bind(Kind::Acc, mg(name));
if self.guessed_eq(name, e, *deferred) {
b.w = Some(format!("w${name}"));
}
b.prev = self.u.prev_of.contains(name).then(|| format!("p${name}"));
self.set(env, name, b);
}
Expand Down Expand Up @@ -952,6 +955,19 @@ impl<'p> Gen<'p> {
prev(&mut out);
Ok(out)
}
Decl::Eq { name, e, key, deferred, .. } if self.guessed_eq(name, e, *deferred) => {
// AC-3 over an equation: createOptimistic(fn) / createOptimisticStore(fn, seed) — derived, its setter only guesses
let lv = self.loading_value(e, env)?;
let mode = obj(vec![("optimistic", E::Bool(true)), ("structured", E::Bool(self.u.paths.contains(name) || key.is_some())), ("derived", E::Bool(true))]);
let x = self.expr(e, env, &Ctx { tail: true, self_: Some(name.clone()), ..Ctx::default() })?;
let mut args = vec![str(name.clone()), arrow(&["__prev"], x), lv, mode];
if let Some(k) = key {
args.push(str(k.clone()));
}
let mut out = vec![S::Const(pair(), rtc("question", args)), self.reg(name, b, true)];
prev(&mut out);
Ok(out)
}
Decl::Eq { name, e, key, deferred, .. } => {
let fin = |mut out: Vec<S>, g: &Self| {
out.push(g.reg(name, b, false));
Expand Down Expand Up @@ -1516,6 +1532,10 @@ impl<'p> Gen<'p> {
Ok(out)
}
/// A region's body as a child: its declarations, then its view (a function body, so it is owned where it is called).
/// An equation a guess sits over (AC-3): not deferred, not a draft, not a wrapped server fn.
fn guessed_eq(&self, name: &str, e: &Expr, deferred: bool) -> bool {
self.u.optimistic.contains(name) && !deferred && !matches!(&e.k, ExprKind::Draft { .. }) && unwrap_server(e).is_none()
}
fn region_fn(&mut self, decls: &[Decl], view: &[Markup], env: Env, param: Option<String>, narrow: Option<(String, Bind)>, bind_name: Option<String>) -> R<E> {
let inner = self.new_env(Some(env));
let mut pre = vec![];
Expand Down
2 changes: 1 addition & 1 deletion guide/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ by what the screen should do:

| You want | Write |
| --- | --- |
| Show a value now, and take it back if the action fails | `guess x := v` (x is a source with a question: `source x = …`) |
| Show a value now, and take it back if the action fails | `guess x := v` (x is an equation, `x = …`, or a source with a question, `source x = …`) |
| `pending? x` true while the action runs | `affects x` |
| Land a UI write now and keep it (close a menu, clear a message) | `onFlush(() => { x := v })` (EF-8) |
| Record the server's answer | `x := answer` after the `await` |
Expand Down
30 changes: 28 additions & 2 deletions guide/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,9 @@ export default component App() {

### GUESS_NO_QUESTION

A guess is confirmed or replaced by the answer to a question, so `guess` writes a source declared with `=` (a
source with a question, ST-5c). A source seeded with `:=` has no question; write it plainly.
A guess is confirmed or replaced by the answer to a question, so `guess` writes an equation or a source declared
with `=` (a source with a question, ST-5c). A source seeded with `:=` has no question; write it plainly, or with
`onFlush` if it is a reset that should land now (EF-8).

```rx before
export default component App() {
Expand All @@ -481,6 +482,31 @@ export default component App() {
}
```

### GUESS_ON_DEFERRED

A `deferred` equation is a lagging copy of another value (V-16), so nothing would confirm a guess on it. Guess the
value it lags.

```rx before
export default component App() {
source name := "Ann"
shown = name
deferred later = shown
action fix() { guess later := "Zed"; await fetch("x") catch (e) => null }
<button onClick={() => { fix() }}>{later}</button>
}
```

```rx after
export default component App() {
source name := "Ann"
shown = name
deferred later = shown
action fix() { guess shown := "Zed"; await fetch("x") catch (e) => null }
<button onClick={() => { fix() }}>{later}</button>
}
```

### INVALID_REFRESH_TARGET

`refresh x` re-asks `x`'s question (AC-4), so `x` is an equation that awaits or a source declared with `=`. A source
Expand Down
2 changes: 1 addition & 1 deletion guide/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Expr ::= TypeScript's, without assignment, `this`, `function`, `new` on rea
- A failure inside the body must be caught there: `try { … } catch (e) { … }` (R-f-6, AC-7). The catch runs inside the
same change: its writes land with the body's end. A rejected body commits its plain writes and reverts its guesses
(AC-1, DV-2).
- `guess x := e`: a guess on a source with a question (`source x = …`), shown now, kept until the action's change
- `guess x := e`: a guess on an equation (`x = …`) or a source with a question (`source x = …`), shown now, kept until the action's change
commits or `x`'s own answer differs (AC-3, H-11). Guesses on one source hold together.
- `refresh x`: asks `x` again, quietly (no `pending?`; the old value stays up until the new one lands). Inside an
action, the answer lands with the action's commit (AC-4).
Expand Down
9 changes: 8 additions & 1 deletion ref/compile/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ class Gen {
if (d.k === "error" || d.k === "context") env.vars.set(d.name, { kind: "val", js: mg(d.name) });
if (d.k === "source") env.vars.set(d.name, { kind: "src", js: mg(d.name), w: `w$${d.name}`, prev: this.u.prevOf.has(d.name) ? `p$${d.name}` : undefined });
if (d.k === "eq" && unwrapServer(d.e)) { env.vars.set(d.name, { kind: "val", js: mg(d.name) }); continue; } // SF-1d: a server fn, not an accessor
if (d.k === "eq") env.vars.set(d.name, { kind: d.e.k === "call" && d.e.f.k === "id" && env.lookup(d.e.f.name)?.kind === "ifn" ? "acc" : "acc", js: mg(d.name), prev: this.u.prevOf.has(d.name) ? `p$${d.name}` : undefined });
if (d.k === "eq") env.vars.set(d.name, { kind: d.e.k === "call" && d.e.f.k === "id" && env.lookup(d.e.f.name)?.kind === "ifn" ? "acc" : "acc", js: mg(d.name), w: this.guessedEq(d) ? `w$${d.name}` : undefined, prev: this.u.prevOf.has(d.name) ? `p$${d.name}` : undefined });
}
for (const d of decls) if (d.k === "import" && !d.resolved) for (const { name, alias } of d.names) {
const v = !d.from.startsWith(".") && !BUILTINS.has(name) && name !== "log" ? `w$${alias}` : name === "log" ? "__B.log" : d.declaredErrors?.includes(name) ? `__rt.jsError(${J(name)})` : BUILTINS.has(name) ? `__B.${name}` : d.from.endsWith("/api") ? `__rt.api(${J(name)})` : null;
Expand Down Expand Up @@ -313,6 +313,8 @@ class Gen {
}).join("\n");
}
private hoistAll(es: Expr[], env: Env, pad: string): string[] { return es.map(x => this.hoist(null, x, env, pad, false)).filter(Boolean); }
/** An equation a guess sits over (AC-3): not deferred, not a draft, not a wrapped server fn. */
private guessedEq(d: Extract<Decl, { k: "eq" }>): boolean { return this.u.optimistic.has(d.name) && !d.deferred && d.e.k !== "draft" && !unwrapServer(d.e); }
private value1(d: Decl, env: Env, ind: number, b: Bind): string {
const pad = " ".repeat(ind * 2);
const prev = b.prev ? `\n${pad}const ${b.prev} = __rt.pre(${b.js});` : "";
Expand All @@ -323,6 +325,11 @@ class Gen {
return `${pad}const [${b.js}, ${b.w}] = __rt.question(${J(d.name)}, (__prev) => ${this.expr(d.init, env, { imp: false, tail: true, self: d.name })}, ${lv}, ${mode}${d.key ? `, ${J(d.key)}` : ""});\n${pad}__rt.reg(${J(d.name)}, { get: ${b.js}, w: ${b.w} });${prev}`;
}
if (d.k !== "eq") throw new CompileError("unreachable");
if (this.guessedEq(d)) { // AC-3 over an equation: createOptimistic(fn) / createOptimisticStore(fn, seed) — derived, its setter only guesses
const lv = this.loadingValue(d.e, env);
const mode = `{ optimistic: true, structured: ${this.u.paths.has(d.name) || !!d.key}, derived: true }`;
return `${pad}const [${b.js}, ${b.w}] = __rt.question(${J(d.name)}, (__prev) => ${this.expr(d.e, env, { imp: false, tail: true, self: d.name })}, ${lv}, ${mode}${d.key ? `, ${J(d.key)}` : ""});\n${pad}__rt.reg(${J(d.name)}, { get: ${b.js}, w: ${b.w} });${prev}`;
}
const reg = `\n${pad}__rt.reg(${J(d.name)}, { get: ${b.js} });${prev}`;
if (d.e.k === "draft") { // ST-3
const seed = d.e.seed; if (seed.k !== "pre" || seed.name !== d.name || !seed.init) throw new CompileError("a draft's seed must be `pre self -> seed`");
Expand Down
3 changes: 2 additions & 1 deletion ref/compile/rt-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ export function createApp(opts: { api?: Record<string, (...a: any[]) => unknown>
if (typeof init === "object" && init !== null) { const [store] = createStore(init as any); const [get] = createSignal<unknown>(store); return ro(get as Acc); }
const [get] = createSignal(init); return ro(get as Acc);
},
question(name: string, compute: (prev: unknown) => unknown, o: { loadingValue?: unknown } | undefined, mode: { optimistic: boolean; structured: boolean }, key?: string): [Acc, Writer] {
question(name: string, compute: (prev: unknown) => unknown, o: { loadingValue?: unknown } | undefined, mode: { optimistic: boolean; structured: boolean; derived?: boolean }, key?: string): [Acc, Writer] {
const fn = named(name, compute);
if (mode.optimistic && mode.structured) { const [store] = (createOptimisticStore as any)((draft: any) => { const r = fn(draft); if (r instanceof Flight || r instanceof Promise) return (async () => assignDraft(draft, r instanceof Flight ? await r.first : await r))(); assignDraft(draft, r); }, key ? [] : {}, key ? { key } : undefined); return ro(() => store); }
if (mode.optimistic && mode.derived) { const [get] = createOptimistic(fn as any, o as any); return ro(get as Acc); } // AC-3 over an equation: the client's createOptimistic(fn), so the owner trees (and hydration keys) match
if (mode.optimistic) { const [truth] = createSignal(fn as any, o as any); const [get] = createOptimistic(() => (truth as Acc)()); return ro(get as Acc); }
if (mode.structured) {
const [store] = (createStore as any)(async (draft: Record<string, unknown>) => { const r = fn(draft); const v = r instanceof Flight ? await r.first : await r; if (v && typeof v === "object") { for (const k of Object.keys(draft)) if (!(k in (v as object))) delete draft[k]; Object.assign(draft, v); } }, {});
Expand Down
7 changes: 6 additions & 1 deletion ref/compile/rt-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,19 @@ export function createApp(opts: { root: any; api?: Record<string, (...a: any[])
},
/** `source x = e` (ST-5): createSignal(fn); with a guess written to it, a createOptimistic layer over that truth (AC-3);
* written or probed by path, createStore(fn, {}) whose derive writes its draft. */
question(name: string, compute: (prev: unknown) => unknown, opts: { loadingValue?: unknown } | undefined, mode: { optimistic: boolean; structured: boolean }, key?: string): [Acc, Writer] {
question(name: string, compute: (prev: unknown) => unknown, opts: { loadingValue?: unknown } | undefined, mode: { optimistic: boolean; structured: boolean; derived?: boolean }, key?: string): [Acc, Writer] {
const fn = named(name, compute);
if (mode.optimistic && mode.structured) { // AC-3 path targets: createOptimisticStore(fn, seed) — the derive writes its draft; the setter guesses per key
const [store, setStore] = (createOptimisticStore as any)((draft: any) => { const r = fn(draft); if (r instanceof Flight || r instanceof Promise) return (async () => assignDraft(draft, r instanceof Flight ? await r.first : await r))(); assignDraft(draft, r); }, key ? [] : {}, key ? { key } : undefined);
const get = () => store; storeAccs.add(get);
const plain = () => { throw new Error("a plain write to an optimistic store is not lowered yet"); };
return [get, { set: plain, path: plain, guess: (v, keys = []) => setStore((d: any) => { if (keys.length === 0) return typeof v === "function" ? (v as (x: unknown) => unknown)(d) : v; /* a whole guess is the setter\'s return, which Solid diffs shallowly against the view as guesses */ let o = d; for (const k of keys.slice(0, -1)) o = o[k]; const last = keys[keys.length - 1]; o[last] = typeof v === "function" ? (v as (x: unknown) => unknown)(o[last]) : v; }) }];
}
if (mode.optimistic && mode.derived) { // AC-3 over an equation: createOptimistic(fn), whose setter only guesses
const plain = () => { throw new Error("a plain write to an equation"); };
const [get, setGuess] = createOptimistic(fn as any, opts as any);
return [get as Acc, { set: plain, path: plain, guess: setGuess as any }];
}
if (mode.optimistic) {
const [truth, setTruth] = createSignal(fn as any, opts as any);
const [get, setGuess] = createOptimistic(() => truth());
Expand Down
7 changes: 6 additions & 1 deletion ref/compile/rt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,19 @@ export function createApp(opts: { api?: Record<string, (...a: any[]) => unknown>
},
/** `source x = e` (ST-5): createSignal(fn); with a guess written to it, a createOptimistic layer over that truth (AC-3);
* written or probed by path, createStore(fn, {}) whose derive writes its draft. */
question(name: string, compute: (prev: unknown) => unknown, opts: { loadingValue?: unknown } | undefined, mode: { optimistic: boolean; structured: boolean }, key?: string): [Acc, Writer] {
question(name: string, compute: (prev: unknown) => unknown, opts: { loadingValue?: unknown } | undefined, mode: { optimistic: boolean; structured: boolean; derived?: boolean }, key?: string): [Acc, Writer] {
const fn = named(name, compute);
if (mode.optimistic && mode.structured) { // AC-3 path targets: createOptimisticStore(fn, seed) — the derive writes its draft; the setter guesses per key
const [store, setStore] = (createOptimisticStore as any)((draft: any) => { const r = fn(draft); if (r instanceof Flight || r instanceof Promise) return (async () => assignDraft(draft, r instanceof Flight ? await r.first : await r))(); assignDraft(draft, r); }, key ? [] : {}, key ? { key } : undefined);
const get = () => store; storeAccs.add(get);
const plain = () => { throw new Error("a plain write to an optimistic store is not lowered yet"); };
return [get, { set: plain, path: plain, guess: (v, keys = []) => setStore((d: any) => { if (keys.length === 0) return typeof v === "function" ? (v as (x: unknown) => unknown)(d) : v; /* a whole guess is the setter\'s return, which Solid diffs shallowly against the view as guesses */ let o = d; for (const k of keys.slice(0, -1)) o = o[k]; const last = keys[keys.length - 1]; o[last] = typeof v === "function" ? (v as (x: unknown) => unknown)(o[last]) : v; }) }];
}
if (mode.optimistic && mode.derived) { // AC-3 over an equation: createOptimistic(fn), whose setter only guesses
const plain = () => { throw new Error("a plain write to an equation"); };
const [get, setGuess] = createOptimistic(fn as any, opts as any);
return [get as Acc, { set: plain, path: plain, guess: setGuess as any }];
}
if (mode.optimistic) {
const [truth, setTruth] = createSignal(fn as any, opts as any);
const [get, setGuess] = createOptimistic(() => truth());
Expand Down
13 changes: 13 additions & 0 deletions ref/oracle/lower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,19 @@ export class Oracle {
scope.vars.set(d.name, { k: "memo", get: () => proj });
break;
}
if (this.optimisticTargets.has(d.name) && !d.deferred) { // AC-3 over an equation: Solid's createOptimistic(fn) / createOptimisticStore(fn, seed) — derived, its setter only guesses
const compute = this.computeOf(d.name, d.e, scope);
const plain = () => { throw new Unsupported("a plain write to an equation"); };
if (this.pathTargets.has(d.name) || d.key) {
const derive = async (draft: any) => { const r = compute(draft); const v = r instanceof Flight ? await r.first : await r; assignDraft(draft, v); };
const [store, setStore] = (createOptimisticStore as any)(derive, d.key ? [] : {}, d.key ? { key: d.key } : undefined);
scope.vars.set(d.name, { k: "store", get: () => store, set: plain, setPath: plain, question: true, setGuess: setStore });
} else {
const [get, setGuess] = createOptimistic(compute as any, this.loadingValue(d.e, scope) as any);
scope.vars.set(d.name, { k: "sig", get, set: plain as any, setGuess: setGuess as any, question: true, optimistic: true });
}
break;
}
if (d.key) { // ST-4: a keyed projection
const compute = this.computeOf(d.name, d.e, scope);
const src = createMemo(() => compute(undefined) as any, this.loadingValue(d.e, scope) as any); // the answer (sync, or the tail await's) is a memo; the projection reconciles it by key (found by S-ai2-search)
Expand Down
Loading
Loading