From c2c6409dead2a5988c8940fc29b3dac5f8a92e1a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 03:44:06 +0000 Subject: [PATCH 1/3] Rename the `optimistic` statement to `guess` `guess x := e` replaces `optimistic x := e` (07 decision 18, ruled 2026-11-11). Semantics and lowering are unchanged (createOptimistic / createOptimisticStore); only the surface word moves. - Parsers (ref/src/parse.ts, compiler/src/parse.rs): `guess` is the keyword; `optimistic` stays reserved and is a parse error coded OPTIMISTIC whose hint names `guess` (guide/diagnostics.md, S-parse-hints). - Codes: OPTIMISTIC_OUTSIDE_HOLD -> GUESS_OUTSIDE_HOLD, OPTIMISTIC_NO_QUESTION -> GUESS_NO_QUESTION, OPTIMISTIC_REVERTED -> GUESS_REVERTED; HELD_WRITE / STALE_WRITE_BACK / AC-1b messages say `guess`. - Spec, guide, scenarios, examples, eval reference apps and the editor grammar use `guess`. eval/results and preregistrations are left as historical records. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWP7LmQhib6M9zGnmBH --- compiler/src/ast.rs | 2 +- compiler/src/check.rs | 8 ++-- compiler/src/infer.rs | 2 +- compiler/src/parse.rs | 9 +++- eval/README.md | 2 +- eval/reference/chain1/rx/comments.rx | 4 +- eval/reference/chain1/rx/issues.rx | 14 +++--- eval/reference/chain1/rx/labels.rx | 8 ++-- eval/reference/chain1/rx/settings.rx | 4 +- eval/reference/chain2/rx/comments.rx | 4 +- eval/reference/chain2/rx/issues.rx | 14 +++--- eval/reference/chain2/rx/labels.rx | 8 ++-- eval/reference/chain2/rx/settings.rx | 4 +- eval/reference/chain3/rx/comments.rx | 4 +- eval/reference/chain3/rx/issues.rx | 14 +++--- eval/reference/chain3/rx/labels.rx | 8 ++-- eval/reference/chain3/rx/settings.rx | 4 +- eval/reference/chain4/rx/comments.rx | 4 +- eval/reference/chain4/rx/issues.rx | 18 ++++---- eval/reference/chain4/rx/labels.rx | 8 ++-- eval/reference/chain4/rx/settings.rx | 4 +- eval/reference/close/rx/main.rx | 8 ++-- eval/reference/comments/rx/main.rx | 8 ++-- eval/reference/count/rx/comments.rx | 4 +- eval/reference/count/rx/issues.rx | 14 +++--- eval/reference/count/rx/settings.rx | 4 +- eval/reference/desk/rx/comments.rx | 4 +- eval/reference/desk/rx/issues.rx | 14 +++--- eval/reference/desk/rx/settings.rx | 4 +- eval/reference/labels/rx/comments.rx | 4 +- eval/reference/labels/rx/issues.rx | 14 +++--- eval/reference/labels/rx/labels.rx | 8 ++-- eval/reference/labels/rx/settings.rx | 4 +- eval/reference/related/rx/comments.rx | 4 +- eval/reference/related/rx/issues.rx | 14 +++--- eval/reference/related/rx/settings.rx | 4 +- eval/reference/rename/rx/main.rx | 10 ++--- eval/reference/settings/rx/settings.rx | 4 +- eval/reference/tracker/rx/issues.rx | 4 +- examples/ai/feed.rx | 8 ++-- examples/ai2/settings/NOTES.md | 4 +- examples/ai2/settings/settings.rx | 4 +- examples/ai2/tracker/NOTES.md | 2 +- examples/ai2/tracker/issues.rx | 2 +- examples/cart.rx | 2 +- examples/notes/data.rx | 2 +- examples/shop/cart.rx | 2 +- examples/todos.rx | 12 ++--- examples/users.rx | 4 +- guide/async.md | 28 ++++++------ guide/diagnostics.md | 45 +++++++++++++++---- guide/reference.md | 4 +- ref/compile/rt-web.ts | 6 +-- ref/compile/rt.ts | 6 +-- ref/oracle/README.md | 6 +-- ref/oracle/lower.ts | 2 +- ref/src/check.ts | 8 ++-- ref/src/infer.ts | 2 +- ref/src/model.ts | 2 +- ref/src/parse.ts | 7 ++- ref/src/runtime.ts | 4 +- scenarios/S-action-1.md | 6 +-- scenarios/S-action-2.md | 2 +- scenarios/S-action-3.md | 2 +- scenarios/S-action-4.md | 2 +- scenarios/S-ai-feed.md | 4 +- scenarios/S-ai2-tracker.md | 2 +- scenarios/S-check-1.md | 8 ++-- scenarios/S-effect-store-1.md | 2 +- scenarios/S-guess-reask-refresh.md | 4 +- scenarios/S-infer-1.md | 12 ++--- scenarios/S-infer-2.md | 2 +- scenarios/S-infer-3.md | 6 +-- scenarios/S-infer-6.md | 2 +- scenarios/S-latest-reask.md | 2 +- scenarios/S-parse-hints.md | 8 ++++ scenarios/S-remount-refresh.md | 2 +- scenarios/S-solid-26.md | 2 +- scenarios/S-solid-32.md | 8 ++-- scenarios/S-solid-33.md | 2 +- scenarios/S-solid-34.md | 2 +- scenarios/S-solid-35.md | 6 +-- scenarios/S-solid-44.md | 2 +- scenarios/S-store-6.md | 10 ++--- scenarios/S-store-7.md | 2 +- scenarios/S-store-8.md | 8 ++-- spec/05-holds.md | 4 +- spec/06-stores.md | 24 +++++----- spec/07-surface.md | 22 ++++++--- spec/08-constraints.md | 4 +- spec/09-modules.md | 4 +- spec/11-compiler.md | 2 +- spec/99-diagnostics.md | 7 +-- tooling/vscode-rx/markdown.js | 2 +- tooling/vscode-rx/syntaxes/rx.tmLanguage.json | 2 +- 95 files changed, 337 insertions(+), 281 deletions(-) diff --git a/compiler/src/ast.rs b/compiler/src/ast.rs index 1c2828fe..46bcbe77 100644 --- a/compiler/src/ast.rs +++ b/compiler/src/ast.rs @@ -110,7 +110,7 @@ pub enum PathSeg { pub struct Target { pub name: String, pub path: Vec, - /// `use name := …` (RG-7, O-4). `None`: not written by this form (an `optimistic` target). + /// `use name := …` (RG-7, O-4). `None`: not written by this form (a `guess` target). pub ctx: Option, } impl Target { diff --git a/compiler/src/check.rs b/compiler/src/check.rs index 1fe90f97..7a26a9a5 100644 --- a/compiler/src/check.rs +++ b/compiler/src/check.rs @@ -2262,9 +2262,9 @@ impl<'a> Checker<'a> { for f in run_findings(stmts, action, bound, inferred) { let n = &f.name; match f.code { - Code::HeldWrite => self.err("HELD_WRITE", "AC-1c", format!("{n} := … comes before an `await` in a body that is an action because it writes and awaits (AC-1b), so it is held until the body ends (AC-1): for state that shows while the work runs, `optimistic {n} := …` (a source with a question, ST-5c); for a reset that lands now, `onFlush(() => {{ {n} := … }})` (EF-8); to hold it on purpose, declare the action (`action name() {{ … }}`); or write it after the `await`")), + Code::HeldWrite => self.err("HELD_WRITE", "AC-1c", format!("{n} := … comes before an `await` in a body that is an action because it writes and awaits (AC-1b), so it is held until the body ends (AC-1): for state that shows while the work runs, `guess {n} := …` (a source with a question, ST-5c); for a reset that lands now, `onFlush(() => {{ {n} := … }})` (EF-8); to hold it on purpose, declare the action (`action name() {{ … }}`); or write it after the `await`")), Code::StaleOwnRead => self.err("STALE_OWN_READ", "AC-1d", format!("{n} is read after this change wrote it: the read sees the committed value, not the write (W-4) — keep the value you wrote in a local (`let v = …; {n} := v`) and read that, or write functionally (`{n} := v => …`, W-6)")), - Code::StaleWriteBack => self.err("STALE_WRITE_BACK", "AC-1e", format!("{n} := … after an `await` is computed from {n} as it was read before that `await`: whatever changed {n} in between (another action, a reload) is overwritten — write functionally (`{n} := v => …`, W-6), which applies to {n} as it stands at the write, or guess it (`optimistic`, AC-3) and let the answer replace the guess")), + Code::StaleWriteBack => self.err("STALE_WRITE_BACK", "AC-1e", format!("{n} := … after an `await` is computed from {n} as it was read before that `await`: whatever changed {n} in between (another action, a reload) is overwritten — write functionally (`{n} := v => …`, W-6), which applies to {n} as it stands at the write, or guess it (`guess`, AC-3) and let the answer replace the guess")), } } } @@ -2351,12 +2351,12 @@ impl<'a> Checker<'a> { } if op == "optimistic" { if !ctx.in_action { - self.err("OPTIMISTIC_OUTSIDE_HOLD", "AC-3", "optimistic outside an action: a guess needs a hold to live in — an apply block is never an action (its return is the cleanup, EF-5): declare one, `action save(v) { optimistic … }`, and call it from the block".into()); + self.err("GUESS_OUTSIDE_HOLD", "AC-3", "a guess outside an action: it needs a hold to live in — an apply block is never an action (its return is the cleanup, EF-5): declare one, `action save(v) { guess … }`, and call it from the block".into()); } if let Some(t) = tsym { if self.syms[t].kind == SymKind::Source && !self.syms[t].question { self.err( - "OPTIMISTIC_NO_QUESTION", + "GUESS_NO_QUESTION", "AC-3", format!("{} is declared with := — a guess needs a question to confirm it (source {} = …, ST-5c)", target.name, target.name), ); diff --git a/compiler/src/infer.rs b/compiler/src/infer.rs index 6bbb076e..02413af7 100644 --- a/compiler/src/infer.rs +++ b/compiler/src/infer.rs @@ -18,7 +18,7 @@ pub enum Trigger { pub fn trigger_text(t: Trigger) -> &'static str { match t { Trigger::WriteAwait => "it writes and awaits", - Trigger::Optimistic => "it has `optimistic`", + Trigger::Optimistic => "it has `guess`", Trigger::Affects => "it has `affects`", } } diff --git a/compiler/src/parse.rs b/compiler/src/parse.rs index 3393da02..370feb38 100644 --- a/compiler/src/parse.rs +++ b/compiler/src/parse.rs @@ -42,7 +42,7 @@ re!(LINE_COMMENT, r"//.*$"); static KEYWORDS: LazyLock> = LazyLock::new(|| { [ - "source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "optimistic", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", + "source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "guess", "optimistic", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", "null", "provide", "context", "use", "import", "export", "from", "pure", "return", "type", "interface", "try", "catch", "finally", "throw", "portal", ] .into_iter() @@ -262,6 +262,7 @@ re!(JS_APIS, r"\b(useState|useReducer|useMemo|useCallback|useEffect|useRef|creat re!(JS_FUNCTION, r"^(export\s+)?(default\s+)?(async\s+)?function\b"); re!(JS_LOCAL, r"^(const|let|var)\b"); re!(JS_RETURN, r"^return\b"); +re!(OLD_OPTIMISTIC, r"(^|[{;]\s*)optimistic\s+[A-Za-z_$]"); re!(NAME_BEFORE, r"([A-Za-z_$][A-Za-z0-9_$.]*)\s*$"); re!(ARROW_BEFORE, r"=>\s*[A-Za-z_$][A-Za-z0-9_$.]*\s*$"); re!(OP_ASSIGN, r"^(\+\+|--|(\*\*|\|\||&&|\?\?|[-+*/%])=)"); @@ -278,6 +279,9 @@ pub fn hint(src: &str, i: usize, msg: &str) -> Option<(&'static str, String)> { let line = src[start..end].trim(); let before = &src[src[..i].rfind('\n').map(|j| j + 1).unwrap_or(0)..i]; let name = NAME_BEFORE.captures(before).map(|c| c[1].to_string()).unwrap_or_else(|| "x".into()); + if OLD_OPTIMISTIC.is_match(line) { + return Some(("OPTIMISTIC", "`optimistic` is spelled `guess`: `guess x := e` shows a guess until the action's answer replaces it (AC-3); it lowers to `createOptimistic`".into())); + } if msg.starts_with("W-2") && rest.starts_with(":=") && ARROW_BEFORE.is_match(before) { return Some(("REACTIVE_WRITE_IN_OWNED_SCOPE", format!("an arrow that writes takes a braced body, `() => {{ {} := … }}`: an expression-bodied arrow is a pure function (SC-3)", name))); } @@ -926,6 +930,9 @@ fn parse_stmt(c: &mut Cursor) -> R { c.ws(true); let start = c.i; if c.keyword("optimistic") { + return c.fail("AC-3: `optimistic` is not a statement"); + } + if c.keyword("guess") { c.ws(true); let name = c.ident_or("target")?; let path = parse_path(c)?; diff --git a/eval/README.md b/eval/README.md index 645cb461..ef04e4a6 100644 --- a/eval/README.md +++ b/eval/README.md @@ -86,7 +86,7 @@ predicted every frame right; the briefs here are stricter. - `tracker`: `assignToMe` cleared its error with a plain write (`assignError := ""`) inside an action that awaits, and a plain write in an async action is held until the action ends (AC-1), so the old error stayed up until the server answered. The fix is the `settings` app's own idiom: `assignError` a question source, - `optimistic assignError := ""`. + `guess assignError := ""`. ## Layout diff --git a/eval/reference/chain1/rx/comments.rx b/eval/reference/chain1/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/chain1/rx/comments.rx +++ b/eval/reference/chain1/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/chain1/rx/issues.rx b/eval/reference/chain1/rx/issues.rx index 73d5be5a..2579778c 100644 --- a/eval/reference/chain1/rx/issues.rx +++ b/eval/reference/chain1/rx/issues.rx @@ -21,10 +21,10 @@ export component IssuesPage() { source closeError: string = "" action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -33,14 +33,14 @@ export component IssuesPage() { } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -51,9 +51,9 @@ export component IssuesPage() { } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/chain1/rx/labels.rx b/eval/reference/chain1/rx/labels.rx index f1c9c11f..e37c0edc 100644 --- a/eval/reference/chain1/rx/labels.rx +++ b/eval/reference/chain1/rx/labels.rx @@ -19,9 +19,9 @@ export component Labels(props: { id: number }) { effect (props.id) { labelError := "" } action add(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic adding := [...adding, { id: id, label: label }] + guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) refresh labels } catch (e) { @@ -30,9 +30,9 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic removing := [...removing, { id: id, label: label }] + guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) refresh labels } catch (e) { diff --git a/eval/reference/chain1/rx/settings.rx b/eval/reference/chain1/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/chain1/rx/settings.rx +++ b/eval/reference/chain1/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/chain2/rx/comments.rx b/eval/reference/chain2/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/chain2/rx/comments.rx +++ b/eval/reference/chain2/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/chain2/rx/issues.rx b/eval/reference/chain2/rx/issues.rx index c59ae010..300add69 100644 --- a/eval/reference/chain2/rx/issues.rx +++ b/eval/reference/chain2/rx/issues.rx @@ -30,10 +30,10 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -42,14 +42,14 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -60,9 +60,9 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/chain2/rx/labels.rx b/eval/reference/chain2/rx/labels.rx index f1c9c11f..e37c0edc 100644 --- a/eval/reference/chain2/rx/labels.rx +++ b/eval/reference/chain2/rx/labels.rx @@ -19,9 +19,9 @@ export component Labels(props: { id: number }) { effect (props.id) { labelError := "" } action add(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic adding := [...adding, { id: id, label: label }] + guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) refresh labels } catch (e) { @@ -30,9 +30,9 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic removing := [...removing, { id: id, label: label }] + guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) refresh labels } catch (e) { diff --git a/eval/reference/chain2/rx/settings.rx b/eval/reference/chain2/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/chain2/rx/settings.rx +++ b/eval/reference/chain2/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/chain3/rx/comments.rx b/eval/reference/chain3/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/chain3/rx/comments.rx +++ b/eval/reference/chain3/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/chain3/rx/issues.rx b/eval/reference/chain3/rx/issues.rx index 728712ef..90d04f88 100644 --- a/eval/reference/chain3/rx/issues.rx +++ b/eval/reference/chain3/rx/issues.rx @@ -31,10 +31,10 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -43,14 +43,14 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -61,9 +61,9 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/chain3/rx/labels.rx b/eval/reference/chain3/rx/labels.rx index f1c9c11f..e37c0edc 100644 --- a/eval/reference/chain3/rx/labels.rx +++ b/eval/reference/chain3/rx/labels.rx @@ -19,9 +19,9 @@ export component Labels(props: { id: number }) { effect (props.id) { labelError := "" } action add(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic adding := [...adding, { id: id, label: label }] + guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) refresh labels } catch (e) { @@ -30,9 +30,9 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic removing := [...removing, { id: id, label: label }] + guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) refresh labels } catch (e) { diff --git a/eval/reference/chain3/rx/settings.rx b/eval/reference/chain3/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/chain3/rx/settings.rx +++ b/eval/reference/chain3/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/chain4/rx/comments.rx b/eval/reference/chain4/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/chain4/rx/comments.rx +++ b/eval/reference/chain4/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/chain4/rx/issues.rx b/eval/reference/chain4/rx/issues.rx index 51d7dc72..488ee123 100644 --- a/eval/reference/chain4/rx/issues.rx +++ b/eval/reference/chain4/rx/issues.rx @@ -34,13 +34,13 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel action assignTo(id: number, who: string, fromRow: boolean) { if (fromRow) { - optimistic rowError := "" + guess rowError := "" } else { - optimistic assignError := "" + guess assignError := "" } try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -53,14 +53,14 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -72,12 +72,12 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel action closeOne(id: number, fromRow: boolean) { if (fromRow) { - optimistic rowError := "" + guess rowError := "" } else { - optimistic closeError := "" + guess closeError := "" } try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/chain4/rx/labels.rx b/eval/reference/chain4/rx/labels.rx index f1c9c11f..e37c0edc 100644 --- a/eval/reference/chain4/rx/labels.rx +++ b/eval/reference/chain4/rx/labels.rx @@ -19,9 +19,9 @@ export component Labels(props: { id: number }) { effect (props.id) { labelError := "" } action add(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic adding := [...adding, { id: id, label: label }] + guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) refresh labels } catch (e) { @@ -30,9 +30,9 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic removing := [...removing, { id: id, label: label }] + guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) refresh labels } catch (e) { diff --git a/eval/reference/chain4/rx/settings.rx b/eval/reference/chain4/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/chain4/rx/settings.rx +++ b/eval/reference/chain4/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/close/rx/main.rx b/eval/reference/close/rx/main.rx index 7768b2cc..1f04fbb0 100644 --- a/eval/reference/close/rx/main.rx +++ b/eval/reference/close/rx/main.rx @@ -14,9 +14,9 @@ export default component App() { source assignError: string = "" source closeError: string = "" action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := keep(issues, t => t.id != id) + guess issues := keep(issues, t => t.id != id) await closeIssue(id) refresh issues } catch (e) { @@ -25,10 +25,10 @@ export default component App() { } action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = indexOf(issues, t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { diff --git a/eval/reference/comments/rx/main.rx b/eval/reference/comments/rx/main.rx index a48b9b88..85d7afb3 100644 --- a/eval/reference/comments/rx/main.rx +++ b/eval/reference/comments/rx/main.rx @@ -17,9 +17,9 @@ export default component App() { source comments = selected == null ? [] : await fetchComments(selected) action postComment(id: number, author: string, text: string) { - optimistic postError := "" + guess postError := "" try { - optimistic comments := [...comments, { author: author, text: `${text} (sending…)` }] + guess comments := [...comments, { author: author, text: `${text} (sending…)` }] await addComment(id, author, text) refresh comments } catch (e) { @@ -29,10 +29,10 @@ export default component App() { action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = indexOf(issues, t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { diff --git a/eval/reference/count/rx/comments.rx b/eval/reference/count/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/count/rx/comments.rx +++ b/eval/reference/count/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/count/rx/issues.rx b/eval/reference/count/rx/issues.rx index 4e32d57a..5825e67e 100644 --- a/eval/reference/count/rx/issues.rx +++ b/eval/reference/count/rx/issues.rx @@ -29,10 +29,10 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -41,14 +41,14 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -59,9 +59,9 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/count/rx/settings.rx b/eval/reference/count/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/count/rx/settings.rx +++ b/eval/reference/count/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/desk/rx/comments.rx b/eval/reference/desk/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/desk/rx/comments.rx +++ b/eval/reference/desk/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/desk/rx/issues.rx b/eval/reference/desk/rx/issues.rx index 35ab63fe..36fc5323 100644 --- a/eval/reference/desk/rx/issues.rx +++ b/eval/reference/desk/rx/issues.rx @@ -20,10 +20,10 @@ export component IssuesPage() { source closeError: string = "" action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -32,14 +32,14 @@ export component IssuesPage() { } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -50,9 +50,9 @@ export component IssuesPage() { } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/desk/rx/settings.rx b/eval/reference/desk/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/desk/rx/settings.rx +++ b/eval/reference/desk/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/labels/rx/comments.rx b/eval/reference/labels/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/labels/rx/comments.rx +++ b/eval/reference/labels/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/labels/rx/issues.rx b/eval/reference/labels/rx/issues.rx index 73d5be5a..2579778c 100644 --- a/eval/reference/labels/rx/issues.rx +++ b/eval/reference/labels/rx/issues.rx @@ -21,10 +21,10 @@ export component IssuesPage() { source closeError: string = "" action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -33,14 +33,14 @@ export component IssuesPage() { } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -51,9 +51,9 @@ export component IssuesPage() { } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/labels/rx/labels.rx b/eval/reference/labels/rx/labels.rx index f1c9c11f..e37c0edc 100644 --- a/eval/reference/labels/rx/labels.rx +++ b/eval/reference/labels/rx/labels.rx @@ -19,9 +19,9 @@ export component Labels(props: { id: number }) { effect (props.id) { labelError := "" } action add(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic adding := [...adding, { id: id, label: label }] + guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) refresh labels } catch (e) { @@ -30,9 +30,9 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - optimistic labelError := "" + guess labelError := "" try { - optimistic removing := [...removing, { id: id, label: label }] + guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) refresh labels } catch (e) { diff --git a/eval/reference/labels/rx/settings.rx b/eval/reference/labels/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/labels/rx/settings.rx +++ b/eval/reference/labels/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/related/rx/comments.rx b/eval/reference/related/rx/comments.rx index 3e9b7b2b..5476dd55 100644 --- a/eval/reference/related/rx/comments.rx +++ b/eval/reference/related/rx/comments.rx @@ -15,9 +15,9 @@ export component Comments(props: { id: number; user: string }) { effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - optimistic postError := null + guess postError := null try { - optimistic sending := [...sending, { id: id, author: author, text: text }] + guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) refresh comments } catch (e) { diff --git a/eval/reference/related/rx/issues.rx b/eval/reference/related/rx/issues.rx index 7cd65f6f..878833c3 100644 --- a/eval/reference/related/rx/issues.rx +++ b/eval/reference/related/rx/issues.rx @@ -21,10 +21,10 @@ export component IssuesPage(props: { onOpenHelp: (query: string, id: number) => source closeError: string = "" action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = issues.findIndex(t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { @@ -33,14 +33,14 @@ export component IssuesPage(props: { onOpenHelp: (query: string, id: number) => } action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -51,9 +51,9 @@ export component IssuesPage(props: { onOpenHelp: (query: string, id: number) => } action closeOne(id: number) { - optimistic closeError := "" + guess closeError := "" try { - optimistic issues := issues.filter(t => t.id != id) + guess issues := issues.filter(t => t.id != id) await closeIssue(id) refresh issues } catch (e) { diff --git a/eval/reference/related/rx/settings.rx b/eval/reference/related/rx/settings.rx index 0df82318..6c45198c 100644 --- a/eval/reference/related/rx/settings.rx +++ b/eval/reference/related/rx/settings.rx @@ -16,8 +16,8 @@ export component SettingsPage() { source saveError: string = "" action save(next: Settings) { - optimistic settings := next - optimistic saveError := "" + guess settings := next + guess saveError := "" affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/rename/rx/main.rx b/eval/reference/rename/rx/main.rx index a3f53de2..8bf3450f 100644 --- a/eval/reference/rename/rx/main.rx +++ b/eval/reference/rename/rx/main.rx @@ -17,14 +17,14 @@ export default component App() { source detail = selected == null ? null : found(await fetchIssue(selected), selected) action renameTo(id: number, title: string) { - optimistic renameError := "" + guess renameError := "" try { let i = indexOf(issues, t => t.id == id) if (i >= 0) { - optimistic issues[i].title := title + guess issues[i].title := title } if (detail != null && detail.id == id) { - optimistic detail.title := title + guess detail.title := title } await renameIssue(id, title) refresh issues @@ -35,10 +35,10 @@ export default component App() { } action assignTo(id: number, who: string) { - optimistic assignError := "" + guess assignError := "" try { let i = indexOf(issues, t => t.id == id) - optimistic issues[i].assignee := who + guess issues[i].assignee := who await assign(id, who) refresh issues } catch (e) { diff --git a/eval/reference/settings/rx/settings.rx b/eval/reference/settings/rx/settings.rx index 6a834e8c..f57f9cab 100644 --- a/eval/reference/settings/rx/settings.rx +++ b/eval/reference/settings/rx/settings.rx @@ -29,8 +29,8 @@ export action retryLoad() { } export action save(next: Settings) { - optimistic settings := next // the page shows what is being saved, now (H-11) - optimistic saveError := "" // and stops showing the previous failure + guess settings := next // the page shows what is being saved, now (H-11) + guess saveError := "" // and stops showing the previous failure affects settings // `pending? settings` is true until the save ends (AC-5) try { let saved = await saveSettings(next) diff --git a/eval/reference/tracker/rx/issues.rx b/eval/reference/tracker/rx/issues.rx index 298bb2a2..61cb3b84 100644 --- a/eval/reference/tracker/rx/issues.rx +++ b/eval/reference/tracker/rx/issues.rx @@ -42,8 +42,8 @@ export action select(id: number) { } export action assignToMe(id: number, user: string) { - optimistic assignError := "" - optimistic issues := list => withAssignee(list, id, user) + guess assignError := "" + guess issues := list => withAssignee(list, id, user) try { await assign(id, user) issues := list => withAssignee(list, id, user) diff --git a/examples/ai/feed.rx b/examples/ai/feed.rx index be7e5eb3..5079941d 100644 --- a/examples/ai/feed.rx +++ b/examples/ai/feed.rx @@ -22,7 +22,7 @@ source messages: Message[] = await* streamMessages(selected) -> [] // ST-5 + S msgs key id = messages // ST-4: each message keyed by id source draftText = "" // ST-5a: a constant question — provisional UI state a guess can clear -source sending = false // same shape: `optimistic sending := true` reverts when the hold ends +source sending = false // same shape: `guess sending := true` reverts when the hold ends source error := "" effect (len(msgs)) { log("count", $0) } // EF: runs when the committed count changes @@ -33,9 +33,9 @@ action send() { let text = draftText // snapshots (SC-2) let room = selected let msg = { id: len(messages) + 1, from: "me", text } - optimistic draftText := "" // AC-3: the guesses show now, display-ahead - optimistic sending := true - optimistic messages := m => [...m, msg] // W-6: a functional guess appends + guess draftText := "" // AC-3: the guesses show now, display-ahead + guess sending := true + guess messages := m => [...m, msg] // W-6: a functional guess appends await sendMessage(room, text) // AC-1: a hold-safe suspension draftText := "" // the truth, after the await (07 §5) } catch (e) { diff --git a/examples/ai2/settings/NOTES.md b/examples/ai2/settings/NOTES.md index a18ce4ae..e13e6ac1 100644 --- a/examples/ai2/settings/NOTES.md +++ b/examples/ai2/settings/NOTES.md @@ -34,7 +34,7 @@ Modules: `theme.rx` (contexts only) ← `settings.rx` (server truth + actions), frames would read `compactProfile [compact]Display name…`. A real page would not have these. - **`saveError` needs both a guess and a plain write on success.** I wrote - `optimistic saveError := ""` so a new save clears the old message at once. + `guess saveError := ""` so a new save clears the old message at once. But the source's truth is the last *plain* write (`"quota exceeded"`), so without `saveError := ""` after a successful `await`, the guess would revert and the old error would come back. It is correct per H-11, and easy to get @@ -85,7 +85,7 @@ Modules: `theme.rx` (contexts only) ← `settings.rx` (server truth + actions), a value replaces the error". I assumed that happens automatically when the refreshed flight lands, even though the content readers were behind the fallback (S-solid-42 is cited for this). -5. **A guess closing an `if` branch** (`optimistic saveError := ""` hiding +5. **A guess closing an `if` branch** (`guess saveError := ""` hiding "Save failed" at once). H-7 says "a branch a lane condition opens is a mainline mount and attaches now". It does not explicitly say the same for a branch a lane condition *closes*. I assumed it is symmetric. diff --git a/examples/ai2/settings/settings.rx b/examples/ai2/settings/settings.rx index 6a834e8c..f57f9cab 100644 --- a/examples/ai2/settings/settings.rx +++ b/examples/ai2/settings/settings.rx @@ -29,8 +29,8 @@ export action retryLoad() { } export action save(next: Settings) { - optimistic settings := next // the page shows what is being saved, now (H-11) - optimistic saveError := "" // and stops showing the previous failure + guess settings := next // the page shows what is being saved, now (H-11) + guess saveError := "" // and stops showing the previous failure affects settings // `pending? settings` is true until the save ends (AC-5) try { let saved = await saveSettings(next) diff --git a/examples/ai2/tracker/NOTES.md b/examples/ai2/tracker/NOTES.md index ab99f09b..5b1747b6 100644 --- a/examples/ai2/tracker/NOTES.md +++ b/examples/ai2/tracker/NOTES.md @@ -40,7 +40,7 @@ the list, and actions as the only writers. read (see Ambiguities 1). I'd want either a sync action to be stated to be an ordinary instant, or a lighter exported-setter form. - **The truth after a successful assign repeats the guess.** - `optimistic issues := f` followed later by `issues := f` with the same `f`. + `guess issues := f` followed later by `issues := f` with the same `f`. H-11 makes this necessary: without the plain write, the guess is "corrected by the truth at hand" when the body ends, which reverts a successful assignment. The ideal is `refresh issues` (the server is the truth), but diff --git a/examples/ai2/tracker/issues.rx b/examples/ai2/tracker/issues.rx index f0cdbd7d..30e051b8 100644 --- a/examples/ai2/tracker/issues.rx +++ b/examples/ai2/tracker/issues.rx @@ -41,7 +41,7 @@ export action select(id: number) { export action assignToMe(id: number, user: string) { assignError := "" - optimistic issues := list => withAssignee(list, id, user) + guess issues := list => withAssignee(list, id, user) try { await assign(id, user) issues := list => withAssignee(list, id, user) diff --git a/examples/cart.rx b/examples/cart.rx index 2346c366..daf070bb 100644 --- a/examples/cart.rx +++ b/examples/cart.rx @@ -33,7 +33,7 @@ source orderError: string := "" action placeOrder() { if (count === 0) return let items = cart // SC-2: a snapshot of the lines being ordered - optimistic cart := [] // AC-3: the cart empties now, display-ahead + guess cart := [] // AC-3: the cart empties now, display-ahead affects cart // AC-5: `pending? cart` while the order is in flight orderError := "" try { diff --git a/examples/notes/data.rx b/examples/notes/data.rx index cade54c0..bc8ebc31 100644 --- a/examples/notes/data.rx +++ b/examples/notes/data.rx @@ -34,7 +34,7 @@ export action open(id: number) { route := { page: "note", id: id }; saveError := export action back() { route := { page: "list" } } export action save(id: number, title: string, body: string) { saveError := "" - optimistic notes := ns => ns.map(n => n.id == id ? { ...n, title: title, body: body } : n) + guess notes := ns => ns.map(n => n.id == id ? { ...n, title: title, body: body } : n) try { try { let saved = await saveNote(id, title, body) diff --git a/examples/shop/cart.rx b/examples/shop/cart.rx index 57d5a988..d2b0d3d0 100644 --- a/examples/shop/cart.rx +++ b/examples/shop/cart.rx @@ -25,7 +25,7 @@ export action setQty(id: number, n: number) { export action placeOrder() { if (count === 0) return let items = cart // SC-2: a snapshot of the lines being ordered - optimistic cart := [] // AC-3: the cart empties now, display-ahead + guess cart := [] // AC-3: the cart empties now, display-ahead affects cart // AC-5: `pending? cart` while the order is in flight orderError := "" try { diff --git a/examples/todos.rx b/examples/todos.rx index e9d52c8b..856af618 100644 --- a/examples/todos.rx +++ b/examples/todos.rx @@ -1,7 +1,7 @@ // TodoMVC against a server, built in layers (solid next's examples/todos). Each layer has its own lifetime and is // derived from the one below it; the screen reads only the top one. // -// 3. guesses `optimistic` writes on `todos` rows and on `adding`: shown now, gone when their action ends (AC-3) +// 3. guesses `guess` writes on `todos` rows and on `adding`: shown now, gone when their action ends (AC-3) // 2. client `todos`: what the client owns over (1), here the failures awaiting a retry; the client writes it and // a new answer from (1) re-derives it (ST-5a). The failures themselves outlive that, so they are a // plain source `todos` is derived from @@ -43,7 +43,7 @@ anyDone = live.some(t => t.done) // guess. A new row has no index yet, so an add guesses `adding`, a list of its own. action add(id: string, title: string) { try { - optimistic adding := as => [...as, { id: id, title: title, done: false, gone: false, saving: true, failure: null }] + guess adding := as => [...as, { id: id, title: title, done: false, gone: false, saving: true, failure: null }] await addTodo(id, title) failures := fs => without(fs, id) } catch (e) { @@ -54,8 +54,8 @@ action add(id: string, title: string) { action toggle(id: string, done: boolean) { try { let i = todos.findIndex(t => t.id === id) - optimistic todos[i].done := done - optimistic todos[i].saving := true + guess todos[i].done := done + guess todos[i].saving := true await toggleTodo(id, done) failures := fs => without(fs, id) } catch (e) { @@ -66,7 +66,7 @@ action toggle(id: string, done: boolean) { action remove(id: string) { try { let i = todos.findIndex(t => t.id === id) - optimistic todos[i].gone := true + guess todos[i].gone := true await removeTodo(id) failures := fs => without(fs, id) } catch (e) { @@ -77,7 +77,7 @@ action remove(id: string) { // A bulk write that fails fans out to one failure per row, so each row gets its own retry. action clearDone(ids: string[]) { try { - optimistic todos := ts => ts.map(t => t.done ? { ...t, gone: true } : t) + guess todos := ts => ts.map(t => t.done ? { ...t, gone: true } : t) await removeTodos(ids) failures := fs => fs.filter(f => !ids.includes(f.id)) } catch (e) { diff --git a/examples/users.rx b/examples/users.rx index 35a594b0..2ac1cbd9 100644 --- a/examples/users.rx +++ b/examples/users.rx @@ -16,8 +16,8 @@ total = len(users) status = pending? users ? "loading…" : `${total} users` action rename(id: number, name: string) { - optimistic users := us => us.map(u => u.id === id ? { ...u, name } : u) // AC-3: shown now, verdict-inert - optimistic editing := null // the editor closes now; the plain write below lands at the commit + guess users := us => us.map(u => u.id === id ? { ...u, name } : u) // AC-3: shown now, verdict-inert + guess editing := null // the editor closes now; the plain write below lands at the commit editing := null saveError := "" try { diff --git a/guide/async.md b/guide/async.md index 03fe5f35..08f981e0 100644 --- a/guide/async.md +++ b/guide/async.md @@ -64,8 +64,8 @@ export source profile: Profile = await loadProfile() // a source with a questi export source saveError: string = "" export action save(next: Profile) { - optimistic profile := next // shows now; reverts by itself if the save fails - optimistic saveError := "" // the old error goes now + guess profile := next // shows now; reverts by itself if the save fails + guess saveError := "" // the old error goes now affects profile // `pending? profile` is true until the save ends try { let answer = await saveProfile(next) @@ -76,7 +76,7 @@ export action save(next: Profile) { } ``` -A handler can be the action itself, with no keyword: an arrow that writes `optimistic`, declares `affects`, or both +A handler can be the action itself, with no keyword: an arrow that writes `guess`, declares `affects`, or both writes and awaits is an action (AC-1b). Declare a named `action` when you call it from more than one place. An action's body is **one change**: every plain write in it, before or after an `await`, is held until the body @@ -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 | `optimistic 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 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` | @@ -101,7 +101,7 @@ export source todos: Todo[] key id = await loadTodos() export action markDone(id: number) { try { let i = todos.findIndex(t => t.id == id) // -1 when absent - optimistic todos[i].done := true // this row only + guess todos[i].done := true // this row only await saveDone(id) refresh todos // the server's list confirms the guess } catch (e) { … } @@ -121,12 +121,12 @@ flight, everything shown from `todos` waits for that action to end, including an The screen is never wrong in between: it shows the guesses, then the truth, in one frame. This is the design (H-11), not a bug, and moving the guess to a source of its own doesn't change it: the rows that show it read `todos`. -A guess on a whole list (`optimistic adding := as => [...as, x]`) is one change with every other guess on that list: +A guess on a whole list (`guess adding := as => [...as, x]`) is one change with every other guess on that list: two adds in flight end together, so a declined one stays until the other ends. That's the same rule, not something to work around. - **Show that something is in flight**, so the longer wait isn't silent: a row flag set by the guess - (`optimistic todos[i].saving := true`), or `pending? todos` with `affects todos`. + (`guess todos[i].saving := true`), or `pending? todos` with `affects todos`. - **Don't rebuild guesses by hand** (a plain list of pending ids with your own revert) to end them one by one: it loses the revert and the hold that keep the screen consistent. @@ -144,7 +144,7 @@ shownAdding = adding.filter(a => a.photo == photo) // shown after `tags` export action addTag(p: number, tag: string) { try { - optimistic adding := [...adding, { photo: p, tag: tag }] + guess adding := [...adding, { photo: p, tag: tag }] await saveTag(p, tag) refresh tags // the server's list now has the tag; the guess goes with the action } catch (e) { … } @@ -165,7 +165,7 @@ and the screen reads only the top: (ST-5a). The client writes it (an edit, a draft, a flag, a failure to show), and a new answer from below resets it. Anything that must outlive that reset (in the todos app, the failures awaiting a retry) is a plain source this layer is derived from. -3. **Guesses.** `optimistic` writes on layer 2, made by an action and gone when the action ends. +3. **Guesses.** `guess` writes on layer 2, made by an action and gone when the action ends. `examples/todos.rx` is the whole app: @@ -177,7 +177,7 @@ source adding: Row[] key id = [] // 3. adds in flight export action add(id: string, title: string) { try { - optimistic adding := as => [...as, { id: id, title: title, saving: true }] + guess adding := as => [...as, { id: id, title: title, saving: true }] await addTodo(id, title) failures := fs => without(fs, id) } catch (e) { @@ -198,7 +198,7 @@ export action add(id: string, title: string) { ## 4. Things that look like bugs and aren't -- **A plain write in an action doesn't show until the server answers.** That's the hold. Use `optimistic` or +- **A plain write in an action doesn't show until the server answers.** That's the hold. Use `guess` or `onFlush` (the table above). Don't move the code into a plain handler to escape it. - **A declined row guess stays until another action's re-ask lands.** While an action's `refresh` of the list is in flight, the list's rows wait for that action (H-11), declined guesses included. That's by design; see "When @@ -210,7 +210,7 @@ export action add(id: string, title: string) { `loading on(k)` above the `errored`. - **The server accepted, and the screen went back to the old value.** The action guessed on one question and refreshed another, or none. A guess ends with its action; only a new answer for the question it guessed on shows - what the server took. Refresh every question you guessed on (`optimistic detail.title := t` needs `refresh detail`, + what the server took. Refresh every question you guessed on (`guess detail.title := t` needs `refresh detail`, not only `refresh issues`). - **A write landed and nothing showed it until the reload answered.** A guess needs a value to guess on. A rename sent while the detail is still loading, or for a row the list does not show yet (`findIndex` gave -1), guesses nothing, @@ -220,7 +220,7 @@ export action add(id: string, title: string) { ## 5. Plain handlers A handler that writes and awaits is an action (AC-1b), so it never lands a write early by accident. A write before -its `await` is a compile error (`HELD_WRITE`), because it would wait for the server: make it `optimistic` if it shows +its `await` is a compile error (`HELD_WRITE`), because it would wait for the server: make it `guess` if it shows while the work runs, `onFlush` if it is a reset, or declare the action if it should wait. Plain handlers are for local state: typing, toggles, selection. @@ -239,7 +239,7 @@ a value; an effect is for sending a value somewhere else (a parent's `on…` han the block does not run until the answer lands (R-2). The effect doesn't hold the screen while it waits. - **Failing inputs**: give an input that can fail a `catch` (`effect (x catch (e) => fallback) { … }`), so the block gets a plain value it can test for. -- **Guesses**: an input computed from an `optimistic` write fires as soon as the guess shows, and again with the old +- **Guesses**: an input computed from a `guess` write fires as soon as the guess shows, and again with the old value if the guess reverts (EF-2). It doesn't wait for the action to end. - **Lifetime**: an effect lives as long as its component. When the component goes away the effect stops, and its cleanup (a function the block returns) runs; nothing it would have seen later reaches it. State that must outlive diff --git a/guide/diagnostics.md b/guide/diagnostics.md index 1e0b26d3..3b8177c5 100644 --- a/guide/diagnostics.md +++ b/guide/diagnostics.md @@ -97,6 +97,33 @@ export default component App() { } ``` +### OPTIMISTIC + +A guess is written `guess x := e` (AC-3). The keyword was `optimistic` before 0.2; it lowers to the same +`createOptimistic`, and shows until the action's answer replaces it. + +```rx before +export default component App() { + source liked = false + action like() { + optimistic liked := true + refresh liked + } + +} +``` + +```rx after +export default component App() { + source liked = false + action like() { + guess liked := true + refresh liked + } + +} +``` + ### ASSIGNMENT A source is written with `:=` as a statement, in a handler or an action (W-1). There is no `=`, `+=` or `++` on state. @@ -214,7 +241,7 @@ export default component App() { A write after an `await` that is computed from a read before it overwrites whatever changed the source in between (another action, a reload). Write functionally, so the write applies to the value as it stands (W-6), or guess it with -`optimistic` and let the answer replace the guess (AC-3). +`guess` and let the answer replace the guess (AC-3). ```rx before export default component App() { @@ -379,7 +406,7 @@ export default component App() { ### HELD_WRITE A handler that writes and awaits is an action (AC-1b), and an action's writes land when its body ends (AC-1). So a -write before the `await` does not show while the work runs. Say which you meant: `optimistic x := …` for state that +write before the `await` does not show while the work runs. Say which you meant: `guess x := …` for state that shows while the work runs (on a source with a question, ST-5c); `onFlush(() => { x := … })` for a reset that lands now (EF-8); a declared `action` to hold it on purpose; or write it after the `await`. `async.md` ("What lands together, and what waits") is the long form. @@ -433,7 +460,7 @@ export default component App() { } ``` -### OPTIMISTIC_OUTSIDE_HOLD +### GUESS_OUTSIDE_HOLD A guess lives in an action's hold until the server answers it (AC-3). An effect's apply block is never an action, so declare one and call it from the block. @@ -442,7 +469,7 @@ declare one and call it from the block. export default component App() { source draft := "a" source saved = "a" - effect (draft) { optimistic saved := $0 } + effect (draft) { guess saved := $0 }

{saved}

} ``` @@ -452,7 +479,7 @@ export default component App() { source draft := "a" source saved = "a" action save(v: string) { - optimistic saved := v + guess saved := v await fetch("save:" + v) catch (e) => null } effect (draft) { save($0) } @@ -460,15 +487,15 @@ export default component App() { } ``` -### OPTIMISTIC_NO_QUESTION +### GUESS_NO_QUESTION -A guess is confirmed or replaced by the answer to a question, so `optimistic` writes a source declared with `=` (a +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. ```rx before export default component App() { source shown := "Ann" - action fix() { optimistic shown := "Zed"; await fetch("x") catch (e) => null } + action fix() { guess shown := "Zed"; await fetch("x") catch (e) => null } } ``` @@ -476,7 +503,7 @@ export default component App() { ```rx after export default component App() { source shown = await fetch("name") catch (e) => "Ann" - action fix() { optimistic shown := "Zed"; await fetch("x") catch (e) => null } + action fix() { guess shown := "Zed"; await fetch("x") catch (e) => null } } ``` diff --git a/guide/reference.md b/guide/reference.md index a36c9355..5a0c36f9 100644 --- a/guide/reference.md +++ b/guide/reference.md @@ -25,7 +25,7 @@ Control ::= "if" Expr "{" … "}" ("else" "{" … "}")? -- no `e | "for" Name "in" Expr "{" … "}" | "loading" ("on" "(" Expr ")")? "fallback" "{" … "}" "{" … "}" | "errored" "fallback" ("(" Name (":" Classes)? ")")? "{" … "}" "{" … "}" -Stmt ::= Target ":=" Expr | "optimistic" Target ":=" Expr | "refresh" Name | "affects" Name +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` @@ -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). -- `optimistic 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 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). diff --git a/ref/compile/rt-web.ts b/ref/compile/rt-web.ts index 849d001d..3b26a40b 100644 --- a/ref/compile/rt-web.ts +++ b/ref/compile/rt-web.ts @@ -162,12 +162,12 @@ export function createApp(opts: { root: any; api?: Record { if (typeof v === "function") { setStore(v as any); set(store); } else if (typeof v === "object" && v !== null) { setStore(() => v as any); set(store); } else set(v); }, path: (keys, v) => setStore((d: any) => { let o = d; for (const k of keys.slice(0, -1)) o = o[k]; o[keys[keys.length - 1]] = v; }), - guess: () => { throw new Error("OPTIMISTIC_NO_QUESTION"); }, + guess: () => { throw new Error("GUESS_NO_QUESTION"); }, }; return [get, w]; } const [get, set] = createSignal(init); - return [get, { set: set as (v: unknown) => void, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("OPTIMISTIC_NO_QUESTION"); } }]; + return [get, { set: set as (v: unknown) => void, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("GUESS_NO_QUESTION"); } }]; }, /** `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. */ @@ -191,7 +191,7 @@ export function createApp(opts: { root: any; api?: Record { throw new Error("a root write to a derived store is not lowered yet"); }, path: (keys, v) => setStore((d: any) => { let o = d; for (const k of keys.slice(0, -1)) o = o[k]; o[keys[keys.length - 1]] = v; }), guess: () => { throw new Error("optimistic over a structured question source is not lowered yet"); } }]; } const [get, set] = createSignal(fn as any, opts as any); - return [get, { set: set as any, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("OPTIMISTIC_NO_QUESTION"); } }]; + return [get, { set: set as any, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("GUESS_NO_QUESTION"); } }]; }, memo: (name: string | null, fn: (prev: unknown) => unknown, opts?: { loadingValue?: unknown }): Acc => createMemo(named(name, fn) as any, opts as any) as Acc, deferred: (name: string, fn: (prev: unknown) => unknown, opts?: { loadingValue?: unknown }): Acc => createDeferred(named(name, fn) as any, opts as any) as Acc, diff --git a/ref/compile/rt.ts b/ref/compile/rt.ts index ad40b5f5..342753ac 100644 --- a/ref/compile/rt.ts +++ b/ref/compile/rt.ts @@ -172,12 +172,12 @@ export function createApp(opts: { api?: Record unknown> const w: Writer = { set: v => { if (typeof v === "function") { setStore(v as any); set(store); } else if (typeof v === "object" && v !== null) { setStore(() => v as any); set(store); } else set(v); }, path: (keys, v) => setStore((d: any) => { let o = d; for (const k of keys.slice(0, -1)) o = o[k]; o[keys[keys.length - 1]] = v; }), - guess: () => { throw new Error("OPTIMISTIC_NO_QUESTION"); }, + guess: () => { throw new Error("GUESS_NO_QUESTION"); }, }; return [get, w]; } const [get, set] = createSignal(init); - return [get, { set: set as (v: unknown) => void, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("OPTIMISTIC_NO_QUESTION"); } }]; + return [get, { set: set as (v: unknown) => void, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("GUESS_NO_QUESTION"); } }]; }, /** `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. */ @@ -201,7 +201,7 @@ export function createApp(opts: { api?: Record unknown> return [get, { set: () => { throw new Error("a root write to a derived store is not lowered yet"); }, path: (keys, v) => setStore((d: any) => { let o = d; for (const k of keys.slice(0, -1)) o = o[k]; o[keys[keys.length - 1]] = v; }), guess: () => { throw new Error("optimistic over a structured question source is not lowered yet"); } }]; } const [get, set] = createSignal(fn as any, opts as any); - return [get, { set: set as any, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("OPTIMISTIC_NO_QUESTION"); } }]; + return [get, { set: set as any, path: () => { throw new Error("a path write to an atomic source"); }, guess: () => { throw new Error("GUESS_NO_QUESTION"); } }]; }, memo: (name: string | null, fn: (prev: unknown) => unknown, opts?: { loadingValue?: unknown }): Acc => createMemo(named(name, fn) as any, opts as any) as Acc, deferred: (name: string, fn: (prev: unknown) => unknown, opts?: { loadingValue?: unknown }): Acc => createDeferred(named(name, fn) as any, opts as any) as Acc, diff --git a/ref/oracle/README.md b/ref/oracle/README.md index cd271f1b..7a8f9ed8 100644 --- a/ref/oracle/README.md +++ b/ref/oracle/README.md @@ -8,7 +8,7 @@ an interpreter (`lower.ts`): | form | lowering | | --- | --- | | `source x := e` | `createSignal`; structured (object/array) → `createStore` behind a signal slot (ST-1, ST-2a) | -| `source x = e` | `createSignal(fn)`; `createOptimistic(fn)` when an `optimistic x :=` exists; `createStore(fn, {})` when written or probed by path (ST-5) | +| `source x = e` | `createSignal(fn)`; `createOptimistic(fn)` when an `guess x :=` exists; `createStore(fn, {})` when written or probed by path (ST-5) | | `x = e`, `await e [-> v]` | `createMemo(fn, { loadingValue })`; the memo returns the flight (a promise/async iterable) | | `deferred x = e` | `createDeferred` | | `x = draft(pre x -> seed) { … }` | `createProjection((draft) => …, seed)` (ST-3) | @@ -18,8 +18,8 @@ an interpreter (`lower.ts`): | `peek`, `latest`, `pending?` | `untrack`, `latest(fn)`, `isPending(fn)` | | `{if}` `{for}` `{loading}` `{errored}` | `Show`, `For`, `Loading`, `Errored` from `solid-js` | | `action`, inline `action { }` | `action(function* …)` — a statement-level `await` is a `yield` of the flight | -| `optimistic`, `refresh`, `affects` | the setter of a `createOptimistic`, `refresh(x)`, `affects(x)` | -| `source x = e` written both plainly and optimistically | `createSignal(fn)` as the truth + `createOptimistic(() => truth())` as the guess layer; plain writes go to the truth, `optimistic` to the layer | +| `guess`, `refresh`, `affects` | the setter of a `createOptimistic`, `refresh(x)`, `affects(x)` | +| `source x = e` written both plainly and optimistically | `createSignal(fn)` as the truth + `createOptimistic(() => truth())` as the guess layer; plain writes go to the truth, `guess` to the layer | | nested `await` | a tail-position `await` is the memo's result (the memo returns the flight); any other position is unsupported | | `provide` / `use` | `setContext` / `getContext` against the declaring owner; a provided source is the source binding (RG-7c) | | `x := f` | the setter's updater form (W-6) | diff --git a/ref/oracle/lower.ts b/ref/oracle/lower.ts index b26eae1c..331f66ab 100644 --- a/ref/oracle/lower.ts +++ b/ref/oracle/lower.ts @@ -471,7 +471,7 @@ export class Oracle { if (b.k === "store") { const path = s.target.path.map(p => typeof p === "string" ? p : String(untrack(() => this.ev(p, scope, env)))); if (s.k === "optimistic") { - if (!b.setGuess) throw new Error("OPTIMISTIC_NO_QUESTION"); + if (!b.setGuess) throw new Error("GUESS_NO_QUESTION"); b.setGuess((d: any) => { if (path.length === 0) { assignDraft(d, typeof v === "function" ? (v as (x: unknown) => unknown)(d) : v); return; } let o = d; for (const k of path.slice(0, -1)) o = o[k]; const last = path[path.length - 1]; o[last] = typeof v === "function" ? (v as (x: unknown) => unknown)(o[last]) : v; }); // W-6 per key return; } diff --git a/ref/src/check.ts b/ref/src/check.ts index 089b00b3..7b0113be 100644 --- a/ref/src/check.ts +++ b/ref/src/check.ts @@ -804,8 +804,8 @@ class Checker { /** AC-1c, AC-1d, AC-1e over one imperative run (ref/src/infer.ts). */ private runChecks(stmts: Stmt[], action: boolean, bound: string[], inferred = false) { for (const f of runFindings(stmts, action, bound, inferred)) { - if (f.code === "HELD_WRITE") this.err("HELD_WRITE", "AC-1c", `${f.name} := … comes before an \`await\` in a body that is an action because it writes and awaits (AC-1b), so it is held until the body ends (AC-1): for state that shows while the work runs, \`optimistic ${f.name} := …\` (a source with a question, ST-5c); for a reset that lands now, \`onFlush(() => { ${f.name} := … })\` (EF-8); to hold it on purpose, declare the action (\`action name() { … }\`); or write it after the \`await\``); - else if (f.code === "STALE_WRITE_BACK") this.err("STALE_WRITE_BACK", "AC-1e", `${f.name} := … after an \`await\` is computed from ${f.name} as it was read before that \`await\`: whatever changed ${f.name} in between (another action, a reload) is overwritten — write functionally (\`${f.name} := v => …\`, W-6), which applies to ${f.name} as it stands at the write, or guess it (\`optimistic\`, AC-3) and let the answer replace the guess`); + if (f.code === "HELD_WRITE") this.err("HELD_WRITE", "AC-1c", `${f.name} := … comes before an \`await\` in a body that is an action because it writes and awaits (AC-1b), so it is held until the body ends (AC-1): for state that shows while the work runs, \`guess ${f.name} := …\` (a source with a question, ST-5c); for a reset that lands now, \`onFlush(() => { ${f.name} := … })\` (EF-8); to hold it on purpose, declare the action (\`action name() { … }\`); or write it after the \`await\``); + else if (f.code === "STALE_WRITE_BACK") this.err("STALE_WRITE_BACK", "AC-1e", `${f.name} := … after an \`await\` is computed from ${f.name} as it was read before that \`await\`: whatever changed ${f.name} in between (another action, a reload) is overwritten — write functionally (\`${f.name} := v => …\`, W-6), which applies to ${f.name} as it stands at the write, or guess it (\`guess\`, AC-3) and let the answer replace the guess`); else this.err("STALE_OWN_READ", "AC-1d", `${f.name} is read after this change wrote it: the read sees the committed value, not the write (W-4) — keep the value you wrote in a local (\`let v = …; ${f.name} := v\`) and read that, or write functionally (\`${f.name} := v => …\`, W-6)`); } } @@ -841,8 +841,8 @@ class Checker { else if (target?.imported) this.err("WRITE_TO_NON_SOURCE", "MO-2", `${s.target.name} is a source of ${target.imported}: a module boundary is a value boundary — write it through an action ${target.imported} exports`); } if (s.k === "optimistic") { - if (!ctx.inAction) this.err("OPTIMISTIC_OUTSIDE_HOLD", "AC-3", "optimistic outside an action: a guess needs a hold to live in — an apply block is never an action (its return is the cleanup, EF-5): declare one, `action save(v) { optimistic … }`, and call it from the block"); - if (target && target.kind === "source" && !target.question) this.err("OPTIMISTIC_NO_QUESTION", "AC-3", `${s.target.name} is declared with := — a guess needs a question to confirm it (source ${s.target.name} = …, ST-5c)`); + if (!ctx.inAction) this.err("GUESS_OUTSIDE_HOLD", "AC-3", "a guess outside an action: it needs a hold to live in — an apply block is never an action (its return is the cleanup, EF-5): declare one, `action save(v) { guess … }`, and call it from the block"); + if (target && target.kind === "source" && !target.question) this.err("GUESS_NO_QUESTION", "AC-3", `${s.target.name} is declared with := — a guess needs a question to confirm it (source ${s.target.name} = …, ST-5c)`); } s.target.path.forEach(p => typeof p !== "string" && this.expr(p, env, ctx)); this.expr(s.e, env, ctx); diff --git a/ref/src/infer.ts b/ref/src/infer.ts index ebd6a947..1cf2957c 100644 --- a/ref/src/infer.ts +++ b/ref/src/infer.ts @@ -41,7 +41,7 @@ export function inferredAction(stmts: Stmt[]): Trigger | null { go(stmts, new Map()); return guess ?? (write && wait ? "write+await" : null); } -export const triggerText = (t: Trigger): string => t === "write+await" ? "it writes and awaits" : `it has \`${t}\``; +export const triggerText = (t: Trigger): string => t === "write+await" ? "it writes and awaits" : `it has \`${t === "optimistic" ? "guess" : t}\``; export type Finding = { code: "HELD_WRITE" | "STALE_OWN_READ" | "STALE_WRITE_BACK"; name: string }; type Path = string[]; // keys from the source; "*" a computed key diff --git a/ref/src/model.ts b/ref/src/model.ts index e4924907..7ea4ee91 100644 --- a/ref/src/model.ts +++ b/ref/src/model.ts @@ -12,7 +12,7 @@ // tracked reactive scope (SC-1): the computation depends on it // snapshot imperative scope (SC-2), `peek` (E-5), a source's seed (E-4), a value parameter's argument (E-10) // pre `pre x` (C-2): the previous instant's value -// and `probe: "latest" | "pending"` under `latest` / `pending?` (R-9, R-12). A write is `x := e` (W-1), `optimistic` +// and `probe: "latest" | "pending"` under `latest` / `pending?` (R-9, R-12). A write is `x := e` (W-1), `guess` // (AC-3), `refresh` (AC-4) or `affects` (AC-5); `use p := e` writes the context's provided source (RG-7c). A call names a // fn, an action, a server fn or a JS import called by name. diff --git a/ref/src/parse.ts b/ref/src/parse.ts index bdc5143d..319bea21 100644 --- a/ref/src/parse.ts +++ b/ref/src/parse.ts @@ -15,6 +15,8 @@ export function hint(src: string, i: number, msg: string): [string, string] | nu const line = src.slice(lineStart(p), (src.indexOf("\n", p) + 1 || src.length + 1) - 1).trim(); const before = src.slice(lineStart(i), i); const name = /([A-Za-z_$][A-Za-z0-9_$.]*)\s*$/.exec(before)?.[1] ?? "x"; + if (/(^|[{;]\s*)optimistic\s+[A-Za-z_$]/.test(line)) + return ["OPTIMISTIC", "`optimistic` is spelled `guess`: `guess x := e` shows a guess until the action's answer replaces it (AC-3); it lowers to `createOptimistic`"]; if (msg.startsWith("W-2") && rest.startsWith(":=") && /=>\s*[A-Za-z_$][A-Za-z0-9_$.]*\s*$/.test(before)) return ["REACTIVE_WRITE_IN_OWNED_SCOPE", `an arrow that writes takes a braced body, \`() => { ${name} := … }\`: an expression-bodied arrow is a pure function (SC-3)`]; if (/^(\+\+|--|(\*\*|\|\||&&|\?\?|[-+*\/%])=|=(?![=>]))/.test(rest)) @@ -110,7 +112,7 @@ function parseClasses(c: Cursor): string[] | undefined { return out.some(n => n === "Error" || n === "unknown" || n === "any") ? undefined : out; } -const KEYWORDS = new Set(["source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "optimistic", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", "null", "provide", "context", "use", "import", "export", "from", "pure", "return", "type", "interface", "try", "catch", "finally", "throw", "portal"]); +const KEYWORDS = new Set(["source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "guess", "optimistic", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", "null", "provide", "context", "use", "import", "export", "from", "pure", "return", "type", "interface", "try", "catch", "finally", "throw", "portal"]); export function parseProgram(src: string): Program { const c = new Cursor(src); @@ -288,7 +290,8 @@ export function parseStmts(src: string): Stmt[] { function parseStmt(c: Cursor): Stmt { c.ws(); - if (c.keyword("optimistic")) { + if (c.keyword("optimistic")) c.fail("AC-3: `optimistic` is not a statement"); + if (c.keyword("guess")) { c.ws(); const name = c.ident() ?? c.fail("target"); const path: Target["path"] = []; for (;;) { if (c.eat(".")) path.push(c.ident() ?? c.fail("member")); else if (c.eat("[")) { c.ws(); path.push(parseExpr(c)); c.ws(); c.expect("]"); } else break; } c.ws(); c.expect(":="); c.ws(); return { k: "optimistic", target: { name, path }, e: parseExpr(c) }; diff --git a/ref/src/runtime.ts b/ref/src/runtime.ts index c0eee1bc..d60894d6 100644 --- a/ref/src/runtime.ts +++ b/ref/src/runtime.ts @@ -1100,9 +1100,9 @@ export class Runtime { else if (s.k === "refresh") { const n = this.nodeNamed(s.name, region); if (!n || (n.kind !== "eq") || !(n.question || n.flight || n.compute)) throw new RuleError("INVALID_REFRESH_TARGET", `${s.name} has no question to re-ask`); n.refreshInAction = !!run.action; this.pendingRefresh.push(n); } else if (s.k === "affects") { const n = this.nodeNamed(s.name, region); if (!n) throw new RuleError("INVALID_AFFECTS_TARGET", `${s.name} is not reactive`); this.pendingAffects.push(n); } else if (s.k === "optimistic") { - if (!run.action) throw new RuleError("OPTIMISTIC_OUTSIDE_HOLD", "optimistic outside an action body"); + if (!run.action) throw new RuleError("GUESS_OUTSIDE_HOLD", "a guess outside an action body"); const n = this.nodeNamed(s.target.name, region); - if (!n || !n.question) throw new RuleError("OPTIMISTIC_NO_QUESTION", `${s.target.name} is not a source with a question (source x = e)`); + if (!n || !n.question) throw new RuleError("GUESS_NO_QUESTION", `${s.target.name} is not a source with a question (source x = e)`); const path = s.target.path.map(p => typeof p === "string" ? p : String(this.ev(p, region, env))); this.pendingGuesses.push({ node: n, path, value: this.ev(s.e, region, env) }); } else if (s.k === "write") { diff --git a/scenarios/S-action-1.md b/scenarios/S-action-1.md index c303067d..35a130b1 100644 --- a/scenarios/S-action-1.md +++ b/scenarios/S-action-1.md @@ -10,7 +10,7 @@ export default component App() { source status := "" source orders := 0 action placeOrder() { - optimistic cart := [] + guess cart := [] status := "placing" try { let receipt = await fetch("checkout") @@ -50,7 +50,7 @@ export default component App() { source cart = [1, 2] source status := "" action placeOrder() { - optimistic cart := [] + guess cart := [] status := "placing" try { let receipt = await fetch("checkout") } catch (e) { status := "declined" } fail("lost the receipt") @@ -96,7 +96,7 @@ export default component App() { source cart = [1, 2] source status := "" action placeOrder() { - optimistic cart := [] + guess cart := [] status := "placing" let receipt = await fetch("checkout") cart := [] diff --git a/scenarios/S-action-2.md b/scenarios/S-action-2.md index beb3cb3f..ab4995b1 100644 --- a/scenarios/S-action-2.md +++ b/scenarios/S-action-2.md @@ -12,7 +12,7 @@ export default component App() { source status := "" action rename(n) { try { - optimistic shown := n + guess shown := n await fetch("send") // fire-and-forget: the answer is not the confirmation await until(() => shown == n) // the confirmation is the truth arriving status := "confirmed" diff --git a/scenarios/S-action-3.md b/scenarios/S-action-3.md index 7efa5f47..647d3fcc 100644 --- a/scenarios/S-action-3.md +++ b/scenarios/S-action-3.md @@ -10,7 +10,7 @@ export default component App() { source n := 0 source guess = n action save() { - optimistic guess := value => value + 1 + guess guess := value => value + 1 try { await fetch("r1") } catch (error) {} }

{guess}

diff --git a/scenarios/S-action-4.md b/scenarios/S-action-4.md index 6072498b..ca12c0d9 100644 --- a/scenarios/S-action-4.md +++ b/scenarios/S-action-4.md @@ -10,7 +10,7 @@ export default component App() { source n := 0 source guess = n action save(amount) { - optimistic guess := value => value + amount + guess guess := value => value + amount try { let got = await fetch("r" + amount) n := value => value + got diff --git a/scenarios/S-ai-feed.md b/scenarios/S-ai-feed.md index 1c92ae65..cfa8a0bb 100644 --- a/scenarios/S-ai-feed.md +++ b/scenarios/S-ai-feed.md @@ -62,11 +62,11 @@ change and lands with the body's end (AC-7). ## Rejected variants ```rx --- reject: OPTIMISTIC_NO_QUESTION +-- reject: GUESS_NO_QUESTION -- handler: pick() export default component App() { source selected := 1 - action pick() { optimistic selected := 2; await fetch("x") } + action pick() { guess selected := 2; await fetch("x") }

{selected}

} diff --git a/scenarios/S-ai2-tracker.md b/scenarios/S-ai2-tracker.md index 481976ef..cb42669d 100644 --- a/scenarios/S-ai2-tracker.md +++ b/scenarios/S-ai2-tracker.md @@ -47,4 +47,4 @@ Predicted from the spec and not run (the test allows the checker only on `main.r | delete the `errored fallback(e) { … }` wrapper in `DetailPanel` | R-f-5 | `UNHANDLED_FAILURE` (`detail` fails `Error` or `NotFound`) | | in `assignToMe`, `await assign(id, user)` outside `try … catch` | R-f-6 | `UNCAUGHT_ACTION_FAILURE` | | in `ui.rx`, a handler `() => { selected := issue.id }` | MO-2a | `WRITE_TO_NON_SOURCE` | -| `optimistic assignError := "…"` in an action (`assignError` is `:=`, no question) | AC-3 / ST-5c | `OPTIMISTIC_NO_QUESTION` | +| `guess assignError := "…"` in an action (`assignError` is `:=`, no question) | AC-3 / ST-5c | `GUESS_NO_QUESTION` | diff --git a/scenarios/S-check-1.md b/scenarios/S-check-1.md index 9cab6844..32496d2c 100644 --- a/scenarios/S-check-1.md +++ b/scenarios/S-check-1.md @@ -199,21 +199,21 @@ export default component App() { ``` ```rx --- reject: OPTIMISTIC_OUTSIDE_HOLD +-- reject: GUESS_OUTSIDE_HOLD export default component App() { source name := "Ann" source shown = name - effect (name) { optimistic shown := "Zed" } + effect (name) { guess shown := "Zed" }

{shown}

} ``` ```rx --- reject: OPTIMISTIC_NO_QUESTION +-- reject: GUESS_NO_QUESTION export default component App() { source shown := "Ann" - action fix() { optimistic shown := "Zed"; await fetch("x") } + action fix() { guess shown := "Zed"; await fetch("x") }

{shown}

} diff --git a/scenarios/S-effect-store-1.md b/scenarios/S-effect-store-1.md index 4521d5eb..22a5419c 100644 --- a/scenarios/S-effect-store-1.md +++ b/scenarios/S-effect-store-1.md @@ -8,7 +8,7 @@ Rules exercised: EF-1, EF-2, H-11, ST-4, AC-3, AC-4. export default component App() { source rows key id = await fetch("l") effect (rows) { log(rows_title($0)) } - action ren() { try { optimistic rows[0].title := "x"; await fetch("save"); refresh rows } catch (e) {} } + action ren() { try { guess rows[0].title := "x"; await fetch("save"); refresh rows } catch (e) {} } errored fallback(e) {

error

} { loading fallback {

loading

} { {for t in rows { {t.title} }} } } } fn rows_title(rs) => rs.map(r => r.title).join(",") diff --git a/scenarios/S-guess-reask-refresh.md b/scenarios/S-guess-reask-refresh.md index aaffb6bf..7d208cb8 100644 --- a/scenarios/S-guess-reask-refresh.md +++ b/scenarios/S-guess-reask-refresh.md @@ -7,7 +7,7 @@ Rules exercised: AC-3, AC-4, H-11, D-34. ```rx export default component App() { source detail = await fetch("d") - action rename(t) { try { optimistic detail.title := t; await fetch("save"); refresh detail } catch (e) {} } + action rename(t) { try { guess detail.title := t; await fetch("save"); refresh detail } catch (e) {} } errored fallback(e) {

error

} { loading fallback {

loading

} { {detail.title} } } } ``` @@ -39,7 +39,7 @@ effect's view too. ```rx export default component App() { source detail = await fetch("d") - action rename(t) { try { optimistic detail.title := t; await fetch("save"); refresh detail } catch (e) {} } + action rename(t) { try { guess detail.title := t; await fetch("save"); refresh detail } catch (e) {} } effect (detail.title catch (e) => "err") { log($0) }

x

} diff --git a/scenarios/S-infer-1.md b/scenarios/S-infer-1.md index e4747eda..96606399 100644 --- a/scenarios/S-infer-1.md +++ b/scenarios/S-infer-1.md @@ -10,7 +10,7 @@ export default component App() { source error := "" + } ``` @@ -61,11 +61,11 @@ An inferred action catches what its body can fail with, like a declared one (R-f-6): nothing awaits a handler. ```rx --- reject: OPTIMISTIC_OUTSIDE_HOLD +-- reject: GUESS_OUTSIDE_HOLD export default component App() { source draft := "a" source saved = "a" - effect (draft) { optimistic saved := $0 } + effect (draft) { guess saved := $0 }

{saved}

} ``` diff --git a/scenarios/S-infer-2.md b/scenarios/S-infer-2.md index 8fdbd96f..e5bbafb1 100644 --- a/scenarios/S-infer-2.md +++ b/scenarios/S-infer-2.md @@ -74,7 +74,7 @@ export default component App() { Written as a plain handler, this showed `saving` at once. As the action the body now is, `saving` would wait for the server. The fixes are named -by what the write is for: `optimistic` for state that shows while the work +by what the write is for: `guess` for state that shows while the work runs, `onFlush` for a reset, a declared action to hold it on purpose. ```rx diff --git a/scenarios/S-infer-3.md b/scenarios/S-infer-3.md index c1f72260..ba39b636 100644 --- a/scenarios/S-infer-3.md +++ b/scenarios/S-infer-3.md @@ -9,7 +9,7 @@ export default component App() { source liked = "no" action like() { try { let r = await fetch("like"); liked := r } catch (e) {} } - + } ``` @@ -22,7 +22,7 @@ the same instant's change (D-7), so the guess lives until `like` ends. | instant | action | frame | | ------- | ---------------------------------------------- | ----- | | 0 | mount | `no` | -| 1 | handler `optimistic liked := "yes"; like()` | `yes` | +| 1 | handler `guess liked := "yes"; like()` | `yes` | | 2 | resolve "like" "yes" | `yes` | ## Variant: the server declines @@ -30,5 +30,5 @@ the same instant's change (D-7), so the guess lives until `like` ends. | instant | action | frame | | ------- | ---------------------------------------------- | ----- | | 0 | mount | `no` | -| 1 | handler `optimistic liked := "yes"; like()` | `yes` | +| 1 | handler `guess liked := "yes"; like()` | `yes` | | 2 | reject "like" "offline" | `no` | diff --git a/scenarios/S-infer-6.md b/scenarios/S-infer-6.md index 9e798c80..a19202b6 100644 --- a/scenarios/S-infer-6.md +++ b/scenarios/S-infer-6.md @@ -56,7 +56,7 @@ export default component App() { source items := [] action add(x: string) { let before = items - optimistic items := [...before, x + "…"] + guess items := [...before, x + "…"] let r = await fetch(x) catch (e) => "" items := [...before, x] } diff --git a/scenarios/S-latest-reask.md b/scenarios/S-latest-reask.md index e0c8cb8f..e63603e2 100644 --- a/scenarios/S-latest-reask.md +++ b/scenarios/S-latest-reask.md @@ -7,7 +7,7 @@ Rules exercised: R-12, H-7, H-11, AC-3, AC-4, ST-4. ```rx export default component App() { source rows key id = await fetch("l") - action close(id) { try { optimistic rows := rows.filter(r => r.id != id); await fetch("close"); refresh rows } catch (e) {} } + action close(id) { try { guess rows := rows.filter(r => r.id != id); await fetch("close"); refresh rows } catch (e) {} } errored fallback(e) {

error

} { loading fallback {

loading

} {

{rows.length} / {(latest rows).length}

} } } ``` diff --git a/scenarios/S-parse-hints.md b/scenarios/S-parse-hints.md index 5b49203f..6ef2f673 100644 --- a/scenarios/S-parse-hints.md +++ b/scenarios/S-parse-hints.md @@ -54,6 +54,14 @@ export default component App() { } ``` +``` +-- reject: OPTIMISTIC +export default component App() { + source liked = false + +} +``` + ``` -- reject: JS_API export default component App() { diff --git a/scenarios/S-remount-refresh.md b/scenarios/S-remount-refresh.md index ad6f200e..27fdc99b 100644 --- a/scenarios/S-remount-refresh.md +++ b/scenarios/S-remount-refresh.md @@ -7,7 +7,7 @@ Rules exercised: AC-4, H-11, ST-4, O-2. ```rx component List() { source rows key id = tag(await fetch("l")) - action ren() { try { optimistic rows[0].title := "x"; await fetch("save"); refresh rows } catch (e) {} } + action ren() { try { guess rows[0].title := "x"; await fetch("save"); refresh rows } catch (e) {} } errored fallback(e) {

error

} { loading fallback {

loading

} { {for r in rows { {r.title} }} } } } fn tag(rs) => rs.map(r => ({ id: r.id, title: r.title, seen: true })) diff --git a/scenarios/S-solid-26.md b/scenarios/S-solid-26.md index 22309cb6..da1d9826 100644 --- a/scenarios/S-solid-26.md +++ b/scenarios/S-solid-26.md @@ -43,7 +43,7 @@ export default component App() { source id := 1 user = await fetch("u:" + id) source shown = user.name - action nick(n) { try { optimistic shown := n; await fetch("gate") } catch {} } + action nick(n) { try { guess shown := n; await fetch("gate") } catch {} } errored fallback(e) {

error: {e.message}

} { {loading fallback {

…

} {

{shown}

{pending? shown ? "updating" : ""}

}} diff --git a/scenarios/S-solid-32.md b/scenarios/S-solid-32.md index 5652150e..7c350675 100644 --- a/scenarios/S-solid-32.md +++ b/scenarios/S-solid-32.md @@ -9,7 +9,7 @@ export default component App() { source name := "Ann" source shown = name source saved := false - action rename(n) { try { optimistic shown := n; saved := true; await fetch("save"); name := n } catch {} } + action rename(n) { try { guess shown := n; saved := true; await fetch("save"); name := n } catch {} }

{shown} / {saved ? "saved" : "unsaved"} / {pending? shown ? "p" : "-"} {pending? saved ? "p" : "-"}

} @@ -43,7 +43,7 @@ was a plain write and commits (AC-1, DV-2). | instant | action | frame | | ------- | ----------------------------------- | ---------------------- | -| 1 | handler `optimistic shown := "Zed"` | `Ann / unsaved / - -` | +| 1 | handler `guess shown := "Zed"` | `Ann / unsaved / - -` | A guess outside a hold does not exist (D-33). A handler with a guess is an action (AC-1b), so this compiles; the action's change opens and commits in @@ -54,11 +54,11 @@ The `effect` form, which is never an action, is still rejected (S-check-1). ```rx --- reject: OPTIMISTIC_NO_QUESTION +-- reject: GUESS_NO_QUESTION -- handler: fix() export default component App() { source shown := "Ann" - action fix() { try { optimistic shown := "Zed"; await fetch("x") } catch {} } + action fix() { try { guess shown := "Zed"; await fetch("x") } catch {} }

{shown}

} diff --git a/scenarios/S-solid-33.md b/scenarios/S-solid-33.md index 0d5ddab6..850d1013 100644 --- a/scenarios/S-solid-33.md +++ b/scenarios/S-solid-33.md @@ -9,7 +9,7 @@ export default component App() { source value := 0 source double = await fetch("d:" + str(value)) related = await fetch("r:" + str(double)) - action bump() { value := 1; optimistic double := 3 } + action bump() { value := 1; guess double := 3 } errored fallback(e) {

error: {e.message}

} { {loading fallback {

loading

} {

double={double} async={related}

}} diff --git a/scenarios/S-solid-34.md b/scenarios/S-solid-34.md index 02d25d98..7f3f6b32 100644 --- a/scenarios/S-solid-34.md +++ b/scenarios/S-solid-34.md @@ -8,7 +8,7 @@ Rules: AC-3, R-4, H-11. export default component App() { source id := 0 source cat = await fetch("cat:" + str(id)) - action pick(n, name) { try { id := n; optimistic cat := name; await fetch("w" + str(n)) } catch {} } + action pick(n, name) { try { id := n; guess cat := name; await fetch("w" + str(n)) } catch {} } errored fallback(e) {

error: {e.message}

} {

{cat} / {latest cat} {pending? cat ? "p" : "-"}

diff --git a/scenarios/S-solid-35.md b/scenarios/S-solid-35.md index 9cadb111..58c5f6f8 100644 --- a/scenarios/S-solid-35.md +++ b/scenarios/S-solid-35.md @@ -8,7 +8,7 @@ Rules: AC-1, AC-3, H-11. export default component App() { source truth := 1 source guess = truth - action attempt() { try { optimistic guess := 5; await fetch("unrelated") } catch {} } + action attempt() { try { guess guess := 5; await fetch("unrelated") } catch {} }

{guess} / {latest guess} {pending? guess ? "p" : "-"}

} @@ -35,7 +35,7 @@ export default component App() { source truth := 1 source guess = truth derived = await fetch("x:" + str(guess)) - action attempt() { try { optimistic guess := 5; await fetch("unrelated") } catch {} } + action attempt() { try { guess guess := 5; await fetch("unrelated") } catch {} } errored fallback(e) {

error: {e.message}

} {

{guess} {derived} / {latest guess} {pending? guess ? "p" : "-"}

@@ -65,7 +65,7 @@ instant 3, and untracked `pending? guess` is `true` until the commit. ```rx export default component App() { source list = await fetch("list") - action add() { try { optimistic list := list + "+mine"; await fetch("save"); refresh list } catch {} } + action add() { try { guess list := list + "+mine"; await fetch("save"); refresh list } catch {} } errored fallback(e) {

error: {e.message}

} {

{list}

diff --git a/scenarios/S-solid-44.md b/scenarios/S-solid-44.md index 9f4e1e1b..70a8bc6c 100644 --- a/scenarios/S-solid-44.md +++ b/scenarios/S-solid-44.md @@ -8,7 +8,7 @@ Rules: H-9, H-11, AC-3, AC-4, ST-5. export default component App() { source sel := 3 source comments = await fetch("c:" + sel) - action post() { try { optimistic comments := 9; await fetch("post"); refresh comments } catch {} } + action post() { try { guess comments := 9; await fetch("post"); refresh comments } catch {} } errored fallback(e) {

error: {e.message}

} {
{sel} {comments} {latest comments} {str(pending? comments)}
diff --git a/scenarios/S-store-6.md b/scenarios/S-store-6.md index 90889d98..eaf315d7 100644 --- a/scenarios/S-store-6.md +++ b/scenarios/S-store-6.md @@ -9,7 +9,7 @@ D-34 (`createOptimisticStore`). export default component App() { source todos key id = await fetch("todos") action toggle(i) { - try { optimistic todos[i].done := true; await fetch("save"); refresh todos } catch {} + try { guess todos[i].done := true; await fetch("save"); refresh todos } catch {} } errored fallback(e) {

error: {e.message}

} { @@ -21,7 +21,7 @@ export default component App() { } ``` -`optimistic todos[i].done := true` guesses one key of one row; the rest of +`guess todos[i].done := true` guesses one key of one row; the rest of `todos` is the truth beneath it. The action saves, then re-asks the list (`refresh todos`) for the truth that confirms the guess. @@ -65,7 +65,7 @@ corrected by the truth it was made over (D-34). export default component App() { source id := 1 source state = await fetch("s:" + str(id)) - action patch() { try { optimistic state.a := 5; await fetch("w") } catch {} } + action patch() { try { guess state.a := 5; await fetch("w") } catch {} } errored fallback(e) {

error: {e.message}

} { {loading fallback {

loading

} {

{state.a} {state.b} {pending? state.a ? "a!" : "-"} {pending? state.b ? "b!" : "-"}

}} @@ -87,10 +87,10 @@ body's end the guess reverts to the truth. ## Rejected variants ```rx --- reject: OPTIMISTIC_NO_QUESTION +-- reject: GUESS_NO_QUESTION export default component App() { source todos: { done: boolean }[] := [{ done: false }] - action toggle(i) { try { optimistic todos[i].done := true; await fetch("save") } catch {} } + action toggle(i) { try { guess todos[i].done := true; await fetch("save") } catch {} }

{len(todos)}

} diff --git a/scenarios/S-store-7.md b/scenarios/S-store-7.md index 2cc997c1..6ed60617 100644 --- a/scenarios/S-store-7.md +++ b/scenarios/S-store-7.md @@ -9,7 +9,7 @@ export default component App() { source filter := "open" source rows key id = await fetch("l:" + filter) shown = latest rows - action ren(i) { try { optimistic rows[i].title := "x"; await fetch("save") } catch (e) {} } + action ren(i) { try { guess rows[i].title := "x"; await fetch("save") } catch (e) {} } errored fallback(e) {

error: {e.message}

} { {len(shown)}{loading on(filter) fallback {

loading

} {
    {for t in rows {
  • {t.title}
  • }}
}} } diff --git a/scenarios/S-store-8.md b/scenarios/S-store-8.md index 7c9fc7e1..b8aeac25 100644 --- a/scenarios/S-store-8.md +++ b/scenarios/S-store-8.md @@ -9,10 +9,10 @@ export default component App() { source todos key id = await fetch("todos") shown = latest todos action toggle(i) { - try { optimistic todos[i].done := true; await fetch("save") } catch {} + try { guess todos[i].done := true; await fetch("save") } catch {} } action rename(i) { - try { optimistic todos[i].title := "Z"; await fetch("ren") } catch {} + try { guess todos[i].title := "Z"; await fetch("ren") } catch {} } errored fallback(e) {

error: {e.message}

} { @@ -49,10 +49,10 @@ export default component App() { source todos key id = await fetch("todos") shown = latest todos action first(i) { - try { optimistic todos[i].title := "X"; await fetch("one") } catch {} + try { guess todos[i].title := "X"; await fetch("one") } catch {} } action second(i) { - try { optimistic todos[i].title := "Y"; await fetch("two") } catch {} + try { guess todos[i].title := "Y"; await fetch("two") } catch {} } errored fallback(e) {

error: {e.message}

} { diff --git a/spec/05-holds.md b/spec/05-holds.md index fe29966e..98424529 100644 --- a/spec/05-holds.md +++ b/spec/05-holds.md @@ -213,7 +213,7 @@ which showed `A 0` where Solid shows `A 1` — S-hold-3.) not `σ`, not `σ′`, not a probe. The instant begins when the run ends (W-3); from then `v` is in `σ′` (probes see it) and in `σ` at commit. -- **H-11 (Optimistic).** [F-11, D-33, D-34] `optimistic x := e`, legal only +- **H-11 (Optimistic).** [F-11, D-33, D-34] `guess x := e`, legal only inside an action body, declared or inferred (AC-3, AC-1b), and only on a source with a question (ST-5), writes a *guess* for `x`: from the instant's settle it is the graph's value (equations derive from it — S-solid-33: `related` re-asks from the @@ -229,7 +229,7 @@ which showed `A 0` where Solid shows `A 1` — S-hold-3.) `true`, and the new question's answer is not the guess's truth — it neither confirms nor corrects the guess; it commits with the hold (S-solid-44). An input the guess's own action writes is the guess's question - (S-solid-34). A functional guess (`optimistic x := v => …`, W-6) applies + (S-solid-34). A functional guess (`guess x := v => …`, W-6) applies to `x` as the screen shows it: an input its action holds, the launching handler's own write included (AC-1), is not shown yet, so the updater sees the old answer (S-action-3; #33). `latest x` answers the guess diff --git a/spec/06-stores.md b/spec/06-stores.md index 6b0b750a..32a83a90 100644 --- a/spec/06-stores.md +++ b/spec/06-stores.md @@ -2,7 +2,7 @@ **Status: OPEN (2026-10-21).** Structured values, sources with a standing question, drafts, keys, streams, and the imperative forms that tie writes to -async work: `action`, `optimistic`, `refresh`, `affects`. Rules cite Solid's +async work: `action`, `guess`, `refresh`, `affects`. Rules cite Solid's F-9, F-11 and the derived rules they restate. In `ref/`: ST-1…ST-6, AC-1…AC-5 (S-store-1…5, S-solid-8/9/15/20/26/32…35/37). Optimistic path writes: S-store-6. AC-6 `until`: S-action-2. AC-7 try/catch: S-action-1. A functional guess over a held input, and two actions guessing one slot (H-11): S-action-3, S-action-4. AC-1b…AC-1d (inferred actions, held writes, stale own reads): S-infer-1…5. AC-1e (stale write-back): S-infer-6. @@ -84,7 +84,7 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } a `createOptimistic(() => truth())` layer. - **ST-5b.** Its question (V-9) is `e`'s inputs. `refresh x` re-asks it quietly; `affects x` declares that in-flight work will change it (H-9). -- **ST-5c.** It is the only legal target of `optimistic` (AC-3): a guess +- **ST-5c.** It is the only legal target of `guess` (AC-3): a guess needs a truth that will confirm or correct it. ## ST-6 — Streams @@ -142,7 +142,7 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } adoption). - **AC-1b (Inferred action).** [ruled 2026-10-28] An arrow function in imperative scope, including an element's event handler (02 SC-4), is an - action when its own statements include `optimistic x := e` (AC-3), + action when its own statements include `guess x := e` (AC-3), `affects x` (AC-5), or both a write to a source (W-1) and a statement-level `await` (07 decision 16). It is then `(ps) => action { B }` with every consequence of AC-1: one change across its awaited work, its @@ -156,7 +156,7 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } An `effect`'s apply block is never inferred: its return is the effect's cleanup (EF-5), which an action's body cannot give, and the next run's cleanup could not cancel an action in flight. A server function's body is - never inferred. A guess in either stays `OPTIMISTIC_OUTSIDE_HOLD`. + never inferred. A guess in either stays `GUESS_OUTSIDE_HOLD`. Lowers to Solid's `action` exactly as `action { … }` does, nothing added (11). Solid: a TypeScript function's promises are invisible to the transition, so its author writes `action(function* () { … })`; here every @@ -168,7 +168,7 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } so a later write in the body hides it and otherwise it lands late, and nothing in the text says so. The fix names what the write is for: ephemeral state that shows while the work runs ("saving", a spinner, a - pending row) is a guess, `optimistic x := v` on a source with a question + pending row) is a guess, `guess x := v` on a source with a question (ST-5c; often a constant one, `source status = ""`); a reset that lands now and stays (clear an error, clear an input, close a menu) is `onFlush(() => { x := v })` (02 EF-8). A write meant to be held — a @@ -205,7 +205,7 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } index computed from `x` (`let i = indexOf(x, …); … x[i].f := v`) is not the written value and does not count. The fix is the functional write (`x := v => …`, W-6), which applies to `x` as it stands at the write, or - a guess (`optimistic`, AC-3) that the answer replaces. A guess or a write + a guess (`guess`, AC-3) that the answer replaces. A guess or a write before the `await` is the change's own and is not checked; nor is an awaited answer (`let r = await …`), which is the server's, not a snapshot of `x`. The check is the language's (Solid cannot see the read, @@ -217,17 +217,17 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } - **AC-2 (Not special).** An action is only a way to tie writes to async work; the hold is the thing (H-2). `refresh`, `affects` and probes behave the same inside and outside one. -- **AC-3 (optimistic).** `optimistic x := e`, legal only inside an action +- **AC-3 (guess).** `guess x := e`, legal only inside an action body, declared or inferred (AC-1b); in an `effect`'s apply block, which - is never an action, it is `OPTIMISTIC_OUTSIDE_HOLD`, and only on a source with a - question (ST-5; `OPTIMISTIC_NO_QUESTION`): writes a guess that is the + is never an action, it is `GUESS_OUTSIDE_HOLD`, and only on a source with a + question (ST-5; `GUESS_NO_QUESTION`): writes a guess that is the graph's and the screen's value for `x` from the instant's settle, display-ahead and verdict-inert, until the hold it was made over commits or `x`'s own answer differs — the full rule is H-11. `x`'s truth is whatever its question gives: a constant (`source draft = "Ann"`), another source (`source draft = name`, S-solid-32), or an `await`. Lowers to `createOptimistic`/`createOptimisticStore` by usage. Path targets - (`optimistic x.a := v`, `optimistic todos[i].done := true`) guess per key, + (`guess x.a := v`, `guess todos[i].done := true`) guess per key, as Solid's per-node override: the rest of `x` is the truth beneath the guess, a new answer corrects only where it differs at a guessed key, and equal there it confirms silently (S-store-6). A question re-asked by an @@ -299,11 +299,11 @@ selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false } | `ACTION_CALLED_IN_OWNED_SCOPE` | U | AC-1: an action is `io` (IO-2) | | `INVALID_REFRESH_TARGET` | S | AC-4: not a source with a question / await eq | | `INVALID_AFFECTS_TARGET` | S | AC-5 | -| `OPTIMISTIC_OUTSIDE_HOLD` (new) | S | AC-3: an apply block (never an action, AC-1b) | +| `GUESS_OUTSIDE_HOLD` (new) | S | AC-3: an apply block (never an action, AC-1b) | | `HELD_WRITE` (new) | S | AC-1c: an early write in an inferred action | | `STALE_OWN_READ` (new) | S | AC-1d: a read of the change's own write | | `STALE_WRITE_BACK` (new) | S | AC-1e: a pre-`await` read written back after it | -| `OPTIMISTIC_NO_QUESTION` (new) | S | AC-3 / ST-5c | +| `GUESS_NO_QUESTION` (new) | S | AC-3 / ST-5c | ## Scenarios diff --git a/spec/07-surface.md b/spec/07-surface.md index 3784d3bc..f2df3daf 100644 --- a/spec/07-surface.md +++ b/spec/07-surface.md @@ -198,7 +198,7 @@ effect (count) { } action rename(n: string) { - optimistic user.name := n // AC-3: only here + guess user.name := n // AC-3: only here let saved = await api.rename(n) // AC-1: a hold-safe suspension user := saved } @@ -206,7 +206,7 @@ action rename(n: string) { action { refresh user; affects user } // inline, one run (AC-1) ``` @@ -230,7 +230,7 @@ action { refresh user; affects user } // inline, one run (AC-1) - An arrow that guesses, declares `affects`, or both writes and awaits is an action (06 AC-1b); an `effect`'s apply block never is. In such an arrow a plain write before an `await` is `HELD_WRITE` (AC-1c): make it - `optimistic` if it is ephemeral state that shows while the work runs, + `guess` if it is ephemeral state that shows while the work runs, `onFlush(() => { … })` if it is a reset that lands now, or declare the action to hold it on purpose. In every run, reading a source the run's own change wrote is `STALE_OWN_READ` (AC-1d): keep the value in a local. @@ -252,7 +252,7 @@ action { refresh user; affects user } // inline, one run (AC-1) ```rx action placeOrder() { let items = cart // a snapshot of what is being ordered - optimistic cart := [] // the guess shows now (AC-3) + guess cart := [] // the guess shows now (AC-3) affects cart // `pending? cart` is true until the body ends (AC-5) — the guess alone is verdict-inert try { let receipt = await checkout(items) @@ -268,7 +268,7 @@ action { refresh user; affects user } // inline, one run (AC-1) `await`, inside the `try`, and report failure from the `catch` (AC-7, R-f-6). An optimistic write over a *structured* question source is a root write (functional or whole); path targets are not in `ref/` yet. -- `refresh x`, `affects x`, `optimistic x := e` are statements (AC-3..5). +- `refresh x`, `affects x`, `guess x := e` are statements (AC-3..5). ## 6. Components (RG-4) @@ -445,7 +445,7 @@ Control ::= "if" Expr "{" RBlock "}" ("else" "{" RBlock "}")? | "loading" ("on" "(" Expr ")")? "fallback" "{" RBlock "}" "{" RBlock "}" | "errored" "fallback" ("(" Name (":" Classes)? ")")? "{" RBlock "}" "{" RBlock "}" Block ::= "{" Stmt* "}" -Stmt ::= Target ":=" Expr | "optimistic" Target ":=" Expr -- Target `use` Name Path: WRITE_TO_NON_SOURCE (O-4) +Stmt ::= Target ":=" Expr | "guess" Target ":=" Expr -- Target `use` Name Path: WRITE_TO_NON_SOURCE (O-4) | "refresh" Name | "affects" Name | "action" Block | ("let" | "const") Name Type? "=" Expr | "if" "(" Expr ")" (Block | Stmt) ("else" (Block | Stmt))? @@ -525,3 +525,13 @@ Expr ::= TSExpr with §3's removals and additions apply block is not inferred (its return is the cleanup, EF-5). The declared `action` stays for names, reuse, and holding a write on purpose. +18. **`guess x := e`, not `optimistic x := e`** (ruled 2026-11-11): the + statement is named for what it writes, a guess (AC-3, H-11), which the + answer replaces. The word was `optimistic` through 0.1, after Solid's + `createOptimistic`, which it still lowers to. A guess on state nothing + will answer reads wrong on its face (`guess saveError := …`: the eval's + pilot runs wrote that nine times as `optimistic`, `GUESS_NO_QUESTION`). + `optimistic` stays reserved, and a line that uses it is the parse error + `OPTIMISTIC`, which names `guess`. `source` was weighed against `state` + and `signal` the same day and kept (decision 2): `signal` reads as atomic + beside stores, and an equation is state too. diff --git a/spec/08-constraints.md b/spec/08-constraints.md index 56ba85df..f59d1e0a 100644 --- a/spec/08-constraints.md +++ b/spec/08-constraints.md @@ -24,7 +24,7 @@ runtime check. | constraint | rules | enforcer | | --- | --- | --- | -| no write, statement or `io` call in reactive scope; `optimistic` only in an action; a draft writes only `d` | W-1, W-2, IO-2, AC-3, ST-3a | grammar: no position exists (bin U) | +| no write, statement or `io` call in reactive scope; `guess` only in an action; a draft writes only `d` | W-1, W-2, IO-2, AC-3, ST-3a | grammar: no position exists (bin U) | | `io` rises and never falls; `fn` is pure, `action` is `io`; `pure` only at the JS boundary | R-io-1..4 | row inference in the checker | | `pending` enters only through `await`; `lane` only through probes; rows flow through equations and across modules | R-1, R-10, R-13, R-14 | row inference | | no instantaneous cycle | C-1 | the static input graph (CN-3) | @@ -79,7 +79,7 @@ runtime check. first load. A probe waits like a read (R-9). Only the first load is checked: an answer that lands after the first paint is a transition (H-2), legal anywhere. Whether a later hold shows on screen is the - affordances' business (`pending?`, `latest`, an `optimistic` guess; a + affordances' business (`pending?`, `latest`, a `guess` guess; a `loading` region is an escape, not an affordance), and a hold that shows none is Solid's run-time `SILENT_HOLD` report; the static form stays advice (99, the advisory rows). With SSR the held mount stays correct diff --git a/spec/09-modules.md b/spec/09-modules.md index 134b2541..8333995d 100644 --- a/spec/09-modules.md +++ b/spec/09-modules.md @@ -41,7 +41,7 @@ see the list at the end. ## MO-2 — The value boundary - **MO-2a.** An imported source is read-only in the importing module: - `x := …` and `optimistic x := …` through an import are + `x := …` and `guess x := …` through an import are `WRITE_TO_NON_SOURCE` (rule MO-2). The exporting module exports actions as its write interface (`export action select(id) { selected := id }`). - **MO-2b.** `refresh x` and `affects x` of an imported question are @@ -188,7 +188,7 @@ export component Save(props: { v: unrendered action go catches(nothing) }) text (S-module-3 is S-module-1 with only the text). Beyond decision 5's words, a line carries: - a source's kind: `export source x … = …` asks a question (ST-5), - `:= …` does not (`refresh x` and `optimistic x := …` depend on it); + `:= …` does not (`refresh x` and `guess x := …` depend on it); - a fn's parameters (`ref`, with their written types), `rejects(…)` (absent: the fn returns nothing that rejects — unlike a JS declaration's, MO-8a, where absent is `Error`), and `instantiates` (RG-6); diff --git a/spec/11-compiler.md b/spec/11-compiler.md index 223e84e3..81a43f08 100644 --- a/spec/11-compiler.md +++ b/spec/11-compiler.md @@ -198,7 +198,7 @@ spellings again. Compile-time only; nothing of it reaches run time. `latest` / `pending?` it carries the probe (R-9, R-12); a read of one field of props or a store records the field. - **CP-6c (writes and calls).** A write is `x := e` (W-1, with whether it - writes a path), `optimistic` (AC-3), `refresh` (AC-4) or `affects` + writes a path), `guess` (AC-3), `refresh` (AC-4) or `affects` (AC-5); `use p := e` is recorded for the diagnostic it gets (O-4). A call names a fn, action, server fn or JS import called by name. - **CP-6d (one walk, two implementations).** The facts are recorded by the diff --git a/spec/99-diagnostics.md b/spec/99-diagnostics.md index eff454bd..46e2272a 100644 --- a/spec/99-diagnostics.md +++ b/spec/99-diagnostics.md @@ -62,7 +62,7 @@ Bins: | `UNSTABLE_MEMO_OUTPUT` | R | data-dependent (partly S once ST decides structured equality) | | `UNSTABLE_LIST_IDENTITY` | S | ST: key is on the type; unkeyed `for` over records is a diagnostic | | `IMMUTABLE_UPDATE_IN_STORE` | U | ST: a structured equation is a delta over `pre`, not a replaced container | -| `OPTIMISTIC_REVERTED` | R | data-dependent | +| `GUESS_REVERTED` | R | data-dependent | | `GRAPH_GROWTH` | R | profiling | | `REACTIVITY_HALTED` | R | an error escaping every boundary | | `INVARIANT_VIOLATION` | R | runtime internal | @@ -99,11 +99,11 @@ named. | `WRITE_TO_NON_SOURCE` | S | W-1 | S-core-1, S-check-1 | | `DRAFT_WRITE_OUTSIDE_DRAFT` | S | ST-3a | S-store-2 | | `ASYNC_STORE_SETTER` | S | ST-3b | S-check-1 | -| `OPTIMISTIC_OUTSIDE_HOLD` | S | AC-3 (an `effect`'s apply block: never an action, AC-1b) | S-check-1, S-infer-1 | +| `GUESS_OUTSIDE_HOLD` | S | AC-3 (an `effect`'s apply block: never an action, AC-1b) | S-check-1, S-infer-1 | | `HELD_WRITE` | S | AC-1c | S-infer-2 | | `STALE_OWN_READ` | S | AC-1d | S-infer-5 | | `STALE_WRITE_BACK` | S | AC-1e | S-infer-6 | -| `OPTIMISTIC_NO_QUESTION` | S | AC-3 / ST-5c | S-check-1, S-solid-32 | +| `GUESS_NO_QUESTION` | S | AC-3 / ST-5c | S-check-1, S-solid-32 | | `INVALID_REFRESH_TARGET`, `INVALID_AFFECTS_TARGET` | S | AC-4, AC-5 | S-check-1 | | `REGION_ESCAPE` | S | O-3, O-3a, O-3b | S-check-1 (seven shapes), S-module-1 | | `UNSTABLE_LIST_IDENTITY` | S | ST-4b (narrow form: `map` into fresh records, unkeyed) | S-store-3 | @@ -163,6 +163,7 @@ section with a fix in `guide/diagnostics.md`, and the compiler's tests check its | `JS_FUNCTION`| `function f() { … }` | | `JS_RETURN` | `return
…
` in a component | | `JS_API` | `createSignal(…)`, `useState(…)`, … on a failing line | +| `OPTIMISTIC` | `optimistic x := …`, the old spelling of `guess x := …` (AC-3) | | `ASSIGNMENT` | `x = …`, `x += …`, `x++` where a write `x := …` was meant (W-1) | | `REACTIVE_WRITE_IN_OWNED_SCOPE` | `() => x := …`: the arrow needs a braced body (W-2, SC-3) | diff --git a/tooling/vscode-rx/markdown.js b/tooling/vscode-rx/markdown.js index 06c3da23..fda0ac9c 100644 --- a/tooling/vscode-rx/markdown.js +++ b/tooling/vscode-rx/markdown.js @@ -7,7 +7,7 @@ const typescript = require("highlight.js/lib/languages/typescript"); /** The words .rx adds to TypeScript's: the ones syntaxes/rx.tmLanguage.json highlights (spec/07). */ const RX_KEYWORDS = [ "source", "component", "action", "pure", "server", "client", "fn", "error", "context", "use", "provide", "pre", - "latest", "peek", "pending", "optimistic", "affects", "refresh", "errored", "loading", "fallback", "portal", + "latest", "peek", "pending", "guess", "affects", "refresh", "errored", "loading", "fallback", "portal", "deferred", "key", "rejects", "fails", "effect", ]; diff --git a/tooling/vscode-rx/syntaxes/rx.tmLanguage.json b/tooling/vscode-rx/syntaxes/rx.tmLanguage.json index 2381a671..1711ec8b 100644 --- a/tooling/vscode-rx/syntaxes/rx.tmLanguage.json +++ b/tooling/vscode-rx/syntaxes/rx.tmLanguage.json @@ -247,7 +247,7 @@ "name": "keyword.operator.reactive.rx" }, { - "match": "\\b(optimistic|refresh|affects)\\b", + "match": "\\b(guess|refresh|affects)\\b", "name": "keyword.other.action.rx" }, { From a2989597f9b5966f09fe02dd0e667124c2384d15 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 03:59:07 +0000 Subject: [PATCH 2/3] Don't reserve `optimistic`: it names Solid's primitive, not a keyword Drops the OPTIMISTIC parse hint, its guide section, spec row and S-parse-hints variant; 07 decision 18 says why. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWP7LmQhib6M9zGnmBH --- compiler/src/parse.rs | 9 +-------- guide/diagnostics.md | 27 --------------------------- ref/src/parse.ts | 5 +---- scenarios/S-parse-hints.md | 8 -------- spec/07-surface.md | 4 ++-- spec/99-diagnostics.md | 1 - 6 files changed, 4 insertions(+), 50 deletions(-) diff --git a/compiler/src/parse.rs b/compiler/src/parse.rs index 370feb38..b0a4e017 100644 --- a/compiler/src/parse.rs +++ b/compiler/src/parse.rs @@ -42,7 +42,7 @@ re!(LINE_COMMENT, r"//.*$"); static KEYWORDS: LazyLock> = LazyLock::new(|| { [ - "source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "guess", "optimistic", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", + "source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "guess", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", "null", "provide", "context", "use", "import", "export", "from", "pure", "return", "type", "interface", "try", "catch", "finally", "throw", "portal", ] .into_iter() @@ -262,7 +262,6 @@ re!(JS_APIS, r"\b(useState|useReducer|useMemo|useCallback|useEffect|useRef|creat re!(JS_FUNCTION, r"^(export\s+)?(default\s+)?(async\s+)?function\b"); re!(JS_LOCAL, r"^(const|let|var)\b"); re!(JS_RETURN, r"^return\b"); -re!(OLD_OPTIMISTIC, r"(^|[{;]\s*)optimistic\s+[A-Za-z_$]"); re!(NAME_BEFORE, r"([A-Za-z_$][A-Za-z0-9_$.]*)\s*$"); re!(ARROW_BEFORE, r"=>\s*[A-Za-z_$][A-Za-z0-9_$.]*\s*$"); re!(OP_ASSIGN, r"^(\+\+|--|(\*\*|\|\||&&|\?\?|[-+*/%])=)"); @@ -279,9 +278,6 @@ pub fn hint(src: &str, i: usize, msg: &str) -> Option<(&'static str, String)> { let line = src[start..end].trim(); let before = &src[src[..i].rfind('\n').map(|j| j + 1).unwrap_or(0)..i]; let name = NAME_BEFORE.captures(before).map(|c| c[1].to_string()).unwrap_or_else(|| "x".into()); - if OLD_OPTIMISTIC.is_match(line) { - return Some(("OPTIMISTIC", "`optimistic` is spelled `guess`: `guess x := e` shows a guess until the action's answer replaces it (AC-3); it lowers to `createOptimistic`".into())); - } if msg.starts_with("W-2") && rest.starts_with(":=") && ARROW_BEFORE.is_match(before) { return Some(("REACTIVE_WRITE_IN_OWNED_SCOPE", format!("an arrow that writes takes a braced body, `() => {{ {} := … }}`: an expression-bodied arrow is a pure function (SC-3)", name))); } @@ -929,9 +925,6 @@ fn parse_path(c: &mut Cursor) -> R> { fn parse_stmt(c: &mut Cursor) -> R { c.ws(true); let start = c.i; - if c.keyword("optimistic") { - return c.fail("AC-3: `optimistic` is not a statement"); - } if c.keyword("guess") { c.ws(true); let name = c.ident_or("target")?; diff --git a/guide/diagnostics.md b/guide/diagnostics.md index 3b8177c5..b634c891 100644 --- a/guide/diagnostics.md +++ b/guide/diagnostics.md @@ -97,33 +97,6 @@ export default component App() { } ``` -### OPTIMISTIC - -A guess is written `guess x := e` (AC-3). The keyword was `optimistic` before 0.2; it lowers to the same -`createOptimistic`, and shows until the action's answer replaces it. - -```rx before -export default component App() { - source liked = false - action like() { - optimistic liked := true - refresh liked - } - -} -``` - -```rx after -export default component App() { - source liked = false - action like() { - guess liked := true - refresh liked - } - -} -``` - ### ASSIGNMENT A source is written with `:=` as a statement, in a handler or an action (W-1). There is no `=`, `+=` or `++` on state. diff --git a/ref/src/parse.ts b/ref/src/parse.ts index 319bea21..946d9182 100644 --- a/ref/src/parse.ts +++ b/ref/src/parse.ts @@ -15,8 +15,6 @@ export function hint(src: string, i: number, msg: string): [string, string] | nu const line = src.slice(lineStart(p), (src.indexOf("\n", p) + 1 || src.length + 1) - 1).trim(); const before = src.slice(lineStart(i), i); const name = /([A-Za-z_$][A-Za-z0-9_$.]*)\s*$/.exec(before)?.[1] ?? "x"; - if (/(^|[{;]\s*)optimistic\s+[A-Za-z_$]/.test(line)) - return ["OPTIMISTIC", "`optimistic` is spelled `guess`: `guess x := e` shows a guess until the action's answer replaces it (AC-3); it lowers to `createOptimistic`"]; if (msg.startsWith("W-2") && rest.startsWith(":=") && /=>\s*[A-Za-z_$][A-Za-z0-9_$.]*\s*$/.test(before)) return ["REACTIVE_WRITE_IN_OWNED_SCOPE", `an arrow that writes takes a braced body, \`() => { ${name} := … }\`: an expression-bodied arrow is a pure function (SC-3)`]; if (/^(\+\+|--|(\*\*|\|\||&&|\?\?|[-+*\/%])=|=(?![=>]))/.test(rest)) @@ -112,7 +110,7 @@ function parseClasses(c: Cursor): string[] | undefined { return out.some(n => n === "Error" || n === "unknown" || n === "any") ? undefined : out; } -const KEYWORDS = new Set(["source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "guess", "optimistic", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", "null", "provide", "context", "use", "import", "export", "from", "pure", "return", "type", "interface", "try", "catch", "finally", "throw", "portal"]); +const KEYWORDS = new Set(["source", "effect", "component", "ref", "await", "latest", "loading", "errored", "fallback", "on", "deferred", "action", "guess", "refresh", "affects", "if", "else", "pre", "peek", "fn", "let", "for", "in", "true", "false", "undefined", "null", "provide", "context", "use", "import", "export", "from", "pure", "return", "type", "interface", "try", "catch", "finally", "throw", "portal"]); export function parseProgram(src: string): Program { const c = new Cursor(src); @@ -290,7 +288,6 @@ export function parseStmts(src: string): Stmt[] { function parseStmt(c: Cursor): Stmt { c.ws(); - if (c.keyword("optimistic")) c.fail("AC-3: `optimistic` is not a statement"); if (c.keyword("guess")) { c.ws(); const name = c.ident() ?? c.fail("target"); const path: Target["path"] = []; for (;;) { if (c.eat(".")) path.push(c.ident() ?? c.fail("member")); else if (c.eat("[")) { c.ws(); path.push(parseExpr(c)); c.ws(); c.expect("]"); } else break; } diff --git a/scenarios/S-parse-hints.md b/scenarios/S-parse-hints.md index 6ef2f673..5b49203f 100644 --- a/scenarios/S-parse-hints.md +++ b/scenarios/S-parse-hints.md @@ -54,14 +54,6 @@ export default component App() { } ``` -``` --- reject: OPTIMISTIC -export default component App() { - source liked = false - -} -``` - ``` -- reject: JS_API export default component App() { diff --git a/spec/07-surface.md b/spec/07-surface.md index f2df3daf..28d150b7 100644 --- a/spec/07-surface.md +++ b/spec/07-surface.md @@ -531,7 +531,7 @@ Expr ::= TSExpr with §3's removals and additions `createOptimistic`, which it still lowers to. A guess on state nothing will answer reads wrong on its face (`guess saveError := …`: the eval's pilot runs wrote that nine times as `optimistic`, `GUESS_NO_QUESTION`). - `optimistic` stays reserved, and a line that uses it is the parse error - `OPTIMISTIC`, which names `guess`. `source` was weighed against `state` + `optimistic` is not reserved: Solid's primitive keeps its name, and the + keyword need not. `source` was weighed against `state` and `signal` the same day and kept (decision 2): `signal` reads as atomic beside stores, and an equation is state too. diff --git a/spec/99-diagnostics.md b/spec/99-diagnostics.md index 46e2272a..54543cfd 100644 --- a/spec/99-diagnostics.md +++ b/spec/99-diagnostics.md @@ -163,7 +163,6 @@ section with a fix in `guide/diagnostics.md`, and the compiler's tests check its | `JS_FUNCTION`| `function f() { … }` | | `JS_RETURN` | `return
…
` in a component | | `JS_API` | `createSignal(…)`, `useState(…)`, … on a failing line | -| `OPTIMISTIC` | `optimistic x := …`, the old spelling of `guess x := …` (AC-3) | | `ASSIGNMENT` | `x = …`, `x += …`, `x++` where a write `x := …` was meant (W-1) | | `REACTIVE_WRITE_IN_OWNED_SCOPE` | `() => x := …`: the arrow needs a braced body (W-2, SC-3) | From bffcff84144dbfd14b852f7ac615f1cec216a444 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 05:41:06 +0000 Subject: [PATCH 3/3] Clear errors with onFlush, not a guess An error clear is a reset that lands now (EF-8), not a guess at the server's answer: `guess saveError := ""` worked only because the source was given a constant question to revert to. The guide's action example, the examples and the eval reference apps now write `onFlush(() => { saveError := "" })`, and those error sources are declared with `:=`. S-ai2-settings gains the flush instant. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWP7LmQhib6M9zGnmBH --- eval/README.md | 2 +- eval/reference/chain1/rx/comments.rx | 4 ++-- eval/reference/chain1/rx/issues.rx | 12 ++++++------ eval/reference/chain1/rx/labels.rx | 6 +++--- eval/reference/chain1/rx/settings.rx | 4 ++-- eval/reference/chain2/rx/comments.rx | 4 ++-- eval/reference/chain2/rx/issues.rx | 12 ++++++------ eval/reference/chain2/rx/labels.rx | 6 +++--- eval/reference/chain2/rx/settings.rx | 4 ++-- eval/reference/chain3/rx/comments.rx | 4 ++-- eval/reference/chain3/rx/issues.rx | 12 ++++++------ eval/reference/chain3/rx/labels.rx | 6 +++--- eval/reference/chain3/rx/settings.rx | 4 ++-- eval/reference/chain4/rx/comments.rx | 4 ++-- eval/reference/chain4/rx/issues.rx | 18 +++++++++--------- eval/reference/chain4/rx/labels.rx | 6 +++--- eval/reference/chain4/rx/settings.rx | 4 ++-- eval/reference/close/rx/main.rx | 8 ++++---- eval/reference/comments/rx/main.rx | 8 ++++---- eval/reference/count/rx/comments.rx | 4 ++-- eval/reference/count/rx/issues.rx | 12 ++++++------ eval/reference/count/rx/settings.rx | 4 ++-- eval/reference/desk/rx/comments.rx | 4 ++-- eval/reference/desk/rx/issues.rx | 12 ++++++------ eval/reference/desk/rx/settings.rx | 4 ++-- eval/reference/labels/rx/comments.rx | 4 ++-- eval/reference/labels/rx/issues.rx | 12 ++++++------ eval/reference/labels/rx/labels.rx | 6 +++--- eval/reference/labels/rx/settings.rx | 4 ++-- eval/reference/related/rx/comments.rx | 4 ++-- eval/reference/related/rx/issues.rx | 12 ++++++------ eval/reference/related/rx/settings.rx | 4 ++-- eval/reference/rename/rx/main.rx | 8 ++++---- eval/reference/settings/rx/settings.rx | 4 ++-- eval/reference/tracker/rx/issues.rx | 4 ++-- examples/ai2/settings/settings.rx | 8 ++++---- guide/async.md | 4 ++-- scenarios/S-ai2-settings.md | 12 +++++++----- 38 files changed, 128 insertions(+), 126 deletions(-) diff --git a/eval/README.md b/eval/README.md index ef04e4a6..6b576b49 100644 --- a/eval/README.md +++ b/eval/README.md @@ -86,7 +86,7 @@ predicted every frame right; the briefs here are stricter. - `tracker`: `assignToMe` cleared its error with a plain write (`assignError := ""`) inside an action that awaits, and a plain write in an async action is held until the action ends (AC-1), so the old error stayed up until the server answered. The fix is the `settings` app's own idiom: `assignError` a question source, - `guess assignError := ""`. + `guess assignError := ""` (since 2026-11-11 the references clear errors with `onFlush`, EF-8). ## Layout diff --git a/eval/reference/chain1/rx/comments.rx b/eval/reference/chain1/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/chain1/rx/comments.rx +++ b/eval/reference/chain1/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/chain1/rx/issues.rx b/eval/reference/chain1/rx/issues.rx index 2579778c..13838f04 100644 --- a/eval/reference/chain1/rx/issues.rx +++ b/eval/reference/chain1/rx/issues.rx @@ -16,12 +16,12 @@ export component IssuesPage() { source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -33,7 +33,7 @@ export component IssuesPage() { } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -51,7 +51,7 @@ export component IssuesPage() { } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/chain1/rx/labels.rx b/eval/reference/chain1/rx/labels.rx index e37c0edc..1fb4ff95 100644 --- a/eval/reference/chain1/rx/labels.rx +++ b/eval/reference/chain1/rx/labels.rx @@ -12,14 +12,14 @@ export component Labels(props: { id: number }) { ...labels.filter(l => !removing.some(r => r.id == props.id && r.label == l)), ...adding.filter(a => a.id == props.id).map(a => a.label), ] - source labelError: string = "" + source labelError: string := "" source labelInput: string := "" // a label error belongs to the issue it was made on: selecting another clears it effect (props.id) { labelError := "" } action add(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) @@ -30,7 +30,7 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) diff --git a/eval/reference/chain1/rx/settings.rx b/eval/reference/chain1/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/chain1/rx/settings.rx +++ b/eval/reference/chain1/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/chain2/rx/comments.rx b/eval/reference/chain2/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/chain2/rx/comments.rx +++ b/eval/reference/chain2/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/chain2/rx/issues.rx b/eval/reference/chain2/rx/issues.rx index 300add69..a400f55d 100644 --- a/eval/reference/chain2/rx/issues.rx +++ b/eval/reference/chain2/rx/issues.rx @@ -24,13 +24,13 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -42,7 +42,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -60,7 +60,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/chain2/rx/labels.rx b/eval/reference/chain2/rx/labels.rx index e37c0edc..1fb4ff95 100644 --- a/eval/reference/chain2/rx/labels.rx +++ b/eval/reference/chain2/rx/labels.rx @@ -12,14 +12,14 @@ export component Labels(props: { id: number }) { ...labels.filter(l => !removing.some(r => r.id == props.id && r.label == l)), ...adding.filter(a => a.id == props.id).map(a => a.label), ] - source labelError: string = "" + source labelError: string := "" source labelInput: string := "" // a label error belongs to the issue it was made on: selecting another clears it effect (props.id) { labelError := "" } action add(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) @@ -30,7 +30,7 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) diff --git a/eval/reference/chain2/rx/settings.rx b/eval/reference/chain2/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/chain2/rx/settings.rx +++ b/eval/reference/chain2/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/chain3/rx/comments.rx b/eval/reference/chain3/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/chain3/rx/comments.rx +++ b/eval/reference/chain3/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/chain3/rx/issues.rx b/eval/reference/chain3/rx/issues.rx index 90d04f88..a19469ef 100644 --- a/eval/reference/chain3/rx/issues.rx +++ b/eval/reference/chain3/rx/issues.rx @@ -25,13 +25,13 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -43,7 +43,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -61,7 +61,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/chain3/rx/labels.rx b/eval/reference/chain3/rx/labels.rx index e37c0edc..1fb4ff95 100644 --- a/eval/reference/chain3/rx/labels.rx +++ b/eval/reference/chain3/rx/labels.rx @@ -12,14 +12,14 @@ export component Labels(props: { id: number }) { ...labels.filter(l => !removing.some(r => r.id == props.id && r.label == l)), ...adding.filter(a => a.id == props.id).map(a => a.label), ] - source labelError: string = "" + source labelError: string := "" source labelInput: string := "" // a label error belongs to the issue it was made on: selecting another clears it effect (props.id) { labelError := "" } action add(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) @@ -30,7 +30,7 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) diff --git a/eval/reference/chain3/rx/settings.rx b/eval/reference/chain3/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/chain3/rx/settings.rx +++ b/eval/reference/chain3/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/chain4/rx/comments.rx b/eval/reference/chain4/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/chain4/rx/comments.rx +++ b/eval/reference/chain4/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/chain4/rx/issues.rx b/eval/reference/chain4/rx/issues.rx index 488ee123..97122ac4 100644 --- a/eval/reference/chain4/rx/issues.rx +++ b/eval/reference/chain4/rx/issues.rx @@ -25,18 +25,18 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" // the newest row action's failure; row actions and the detail's leave each other's errors alone - source rowError: string = "" + source rowError: string := "" action assignTo(id: number, who: string, fromRow: boolean) { if (fromRow) { - guess rowError := "" + onFlush(() => { rowError := "" }) } else { - guess assignError := "" + onFlush(() => { assignError := "" }) } try { let i = issues.findIndex(t => t.id == id) @@ -53,7 +53,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -72,9 +72,9 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void; onOpenHel action closeOne(id: number, fromRow: boolean) { if (fromRow) { - guess rowError := "" + onFlush(() => { rowError := "" }) } else { - guess closeError := "" + onFlush(() => { closeError := "" }) } try { guess issues := issues.filter(t => t.id != id) diff --git a/eval/reference/chain4/rx/labels.rx b/eval/reference/chain4/rx/labels.rx index e37c0edc..1fb4ff95 100644 --- a/eval/reference/chain4/rx/labels.rx +++ b/eval/reference/chain4/rx/labels.rx @@ -12,14 +12,14 @@ export component Labels(props: { id: number }) { ...labels.filter(l => !removing.some(r => r.id == props.id && r.label == l)), ...adding.filter(a => a.id == props.id).map(a => a.label), ] - source labelError: string = "" + source labelError: string := "" source labelInput: string := "" // a label error belongs to the issue it was made on: selecting another clears it effect (props.id) { labelError := "" } action add(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) @@ -30,7 +30,7 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) diff --git a/eval/reference/chain4/rx/settings.rx b/eval/reference/chain4/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/chain4/rx/settings.rx +++ b/eval/reference/chain4/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/close/rx/main.rx b/eval/reference/close/rx/main.rx index 1f04fbb0..738dc2b3 100644 --- a/eval/reference/close/rx/main.rx +++ b/eval/reference/close/rx/main.rx @@ -11,10 +11,10 @@ export default component App() { source filter: string := "open" source selected: number | null := null source issues key id = await fetchIssues(filter) - source assignError: string = "" - source closeError: string = "" + source assignError: string := "" + source closeError: string := "" action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := keep(issues, t => t.id != id) await closeIssue(id) @@ -25,7 +25,7 @@ export default component App() { } action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = indexOf(issues, t => t.id == id) guess issues[i].assignee := who diff --git a/eval/reference/comments/rx/main.rx b/eval/reference/comments/rx/main.rx index 85d7afb3..d9f2f0d5 100644 --- a/eval/reference/comments/rx/main.rx +++ b/eval/reference/comments/rx/main.rx @@ -11,13 +11,13 @@ export default component App() { source filter: string := "open" source selected: number | null := null source issues key id = await fetchIssues(filter) - source assignError: string = "" - source postError: string = "" + source assignError: string := "" + source postError: string := "" source commentInput: string := "" source comments = selected == null ? [] : await fetchComments(selected) action postComment(id: number, author: string, text: string) { - guess postError := "" + onFlush(() => { postError := "" }) try { guess comments := [...comments, { author: author, text: `${text} (sending…)` }] await addComment(id, author, text) @@ -29,7 +29,7 @@ export default component App() { action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = indexOf(issues, t => t.id == id) guess issues[i].assignee := who diff --git a/eval/reference/count/rx/comments.rx b/eval/reference/count/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/count/rx/comments.rx +++ b/eval/reference/count/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/count/rx/issues.rx b/eval/reference/count/rx/issues.rx index 5825e67e..0bac8b0e 100644 --- a/eval/reference/count/rx/issues.rx +++ b/eval/reference/count/rx/issues.rx @@ -23,13 +23,13 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -41,7 +41,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -59,7 +59,7 @@ export component IssuesPage(props: { onOpenCount: (n: number) => void }) { } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/count/rx/settings.rx b/eval/reference/count/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/count/rx/settings.rx +++ b/eval/reference/count/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/desk/rx/comments.rx b/eval/reference/desk/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/desk/rx/comments.rx +++ b/eval/reference/desk/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/desk/rx/issues.rx b/eval/reference/desk/rx/issues.rx index 36fc5323..7cd02652 100644 --- a/eval/reference/desk/rx/issues.rx +++ b/eval/reference/desk/rx/issues.rx @@ -15,12 +15,12 @@ export component IssuesPage() { source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -32,7 +32,7 @@ export component IssuesPage() { } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -50,7 +50,7 @@ export component IssuesPage() { } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/desk/rx/settings.rx b/eval/reference/desk/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/desk/rx/settings.rx +++ b/eval/reference/desk/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/labels/rx/comments.rx b/eval/reference/labels/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/labels/rx/comments.rx +++ b/eval/reference/labels/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/labels/rx/issues.rx b/eval/reference/labels/rx/issues.rx index 2579778c..13838f04 100644 --- a/eval/reference/labels/rx/issues.rx +++ b/eval/reference/labels/rx/issues.rx @@ -16,12 +16,12 @@ export component IssuesPage() { source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -33,7 +33,7 @@ export component IssuesPage() { } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -51,7 +51,7 @@ export component IssuesPage() { } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/labels/rx/labels.rx b/eval/reference/labels/rx/labels.rx index e37c0edc..1fb4ff95 100644 --- a/eval/reference/labels/rx/labels.rx +++ b/eval/reference/labels/rx/labels.rx @@ -12,14 +12,14 @@ export component Labels(props: { id: number }) { ...labels.filter(l => !removing.some(r => r.id == props.id && r.label == l)), ...adding.filter(a => a.id == props.id).map(a => a.label), ] - source labelError: string = "" + source labelError: string := "" source labelInput: string := "" // a label error belongs to the issue it was made on: selecting another clears it effect (props.id) { labelError := "" } action add(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess adding := [...adding, { id: id, label: label }] await addLabel(id, label) @@ -30,7 +30,7 @@ export component Labels(props: { id: number }) { } action remove(id: number, label: string) { - guess labelError := "" + onFlush(() => { labelError := "" }) try { guess removing := [...removing, { id: id, label: label }] await removeLabel(id, label) diff --git a/eval/reference/labels/rx/settings.rx b/eval/reference/labels/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/labels/rx/settings.rx +++ b/eval/reference/labels/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/related/rx/comments.rx b/eval/reference/related/rx/comments.rx index 5476dd55..8517b69a 100644 --- a/eval/reference/related/rx/comments.rx +++ b/eval/reference/related/rx/comments.rx @@ -8,14 +8,14 @@ export component Comments(props: { id: number; user: string }) { source comments = await fetchComments(props.id) source sending: { id: number; author: string; text: string }[] = [] shownSending = sending.filter(s => s.id == props.id) - source postError: string | null = null + source postError: string | null := null source commentInput: string := "" // a post error belongs to the issue it was made on: selecting another clears it effect (props.id) { postError := null } action postComment(id: number, author: string, text: string) { - guess postError := null + onFlush(() => { postError := null }) try { guess sending := [...sending, { id: id, author: author, text: text }] await addComment(id, author, text) diff --git a/eval/reference/related/rx/issues.rx b/eval/reference/related/rx/issues.rx index 878833c3..db6ad3f6 100644 --- a/eval/reference/related/rx/issues.rx +++ b/eval/reference/related/rx/issues.rx @@ -16,12 +16,12 @@ export component IssuesPage(props: { onOpenHelp: (query: string, id: number) => source issues key id = await fetchIssues(filter) source detail = selected == null ? null : found(await fetchIssue(selected), selected) source titleInput: string := "" - source assignError: string = "" - source renameError: string = "" - source closeError: string = "" + source assignError: string := "" + source renameError: string := "" + source closeError: string := "" action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = issues.findIndex(t => t.id == id) guess issues[i].assignee := who @@ -33,7 +33,7 @@ export component IssuesPage(props: { onOpenHelp: (query: string, id: number) => } action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = issues.findIndex(t => t.id == id) if (i >= 0) { @@ -51,7 +51,7 @@ export component IssuesPage(props: { onOpenHelp: (query: string, id: number) => } action closeOne(id: number) { - guess closeError := "" + onFlush(() => { closeError := "" }) try { guess issues := issues.filter(t => t.id != id) await closeIssue(id) diff --git a/eval/reference/related/rx/settings.rx b/eval/reference/related/rx/settings.rx index 6c45198c..5093aaca 100644 --- a/eval/reference/related/rx/settings.rx +++ b/eval/reference/related/rx/settings.rx @@ -13,11 +13,11 @@ fn validName(name: string): string => export component SettingsPage() { source settings: Settings = await loadSettings() - source saveError: string = "" + source saveError: string := "" action save(next: Settings) { guess settings := next - guess saveError := "" + onFlush(() => { saveError := "" }) affects settings try { let saved = await saveSettings(next) diff --git a/eval/reference/rename/rx/main.rx b/eval/reference/rename/rx/main.rx index 8bf3450f..55ce3ff3 100644 --- a/eval/reference/rename/rx/main.rx +++ b/eval/reference/rename/rx/main.rx @@ -11,13 +11,13 @@ export default component App() { source filter: string := "open" source selected: number | null := null source issues key id = await fetchIssues(filter) - source assignError: string = "" - source renameError: string = "" + source assignError: string := "" + source renameError: string := "" source titleInput: string := "" source detail = selected == null ? null : found(await fetchIssue(selected), selected) action renameTo(id: number, title: string) { - guess renameError := "" + onFlush(() => { renameError := "" }) try { let i = indexOf(issues, t => t.id == id) if (i >= 0) { @@ -35,7 +35,7 @@ export default component App() { } action assignTo(id: number, who: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) try { let i = indexOf(issues, t => t.id == id) guess issues[i].assignee := who diff --git a/eval/reference/settings/rx/settings.rx b/eval/reference/settings/rx/settings.rx index f57f9cab..7d93eab6 100644 --- a/eval/reference/settings/rx/settings.rx +++ b/eval/reference/settings/rx/settings.rx @@ -22,7 +22,7 @@ export source settings: Settings = await loadSettings() // What the last save failed with, or "". A question with a constant truth, so // a new save can clear it as a guess the moment it starts (ST-5a). -export source saveError: string = "" +export source saveError: string := "" export action retryLoad() { refresh settings @@ -30,7 +30,7 @@ export action retryLoad() { export action save(next: Settings) { guess settings := next // the page shows what is being saved, now (H-11) - guess saveError := "" // and stops showing the previous failure + onFlush(() => { saveError := "" }) // and stops showing the previous failure affects settings // `pending? settings` is true until the save ends (AC-5) try { let saved = await saveSettings(next) diff --git a/eval/reference/tracker/rx/issues.rx b/eval/reference/tracker/rx/issues.rx index 61cb3b84..43e64616 100644 --- a/eval/reference/tracker/rx/issues.rx +++ b/eval/reference/tracker/rx/issues.rx @@ -24,7 +24,7 @@ export source selected: number | null := null // The server's message from the last declined assignment ("" when none). A // question with a constant truth, so a new assignment can clear it as a guess // the moment it starts (ST-5a); a plain write would be held with the action. -export source assignError: string = "" +export source assignError: string := "" // Only asked while something is selected (E-11: the null arm opens no flight). export detail = selected == null ? null : found(await fetchIssue(selected), selected) @@ -42,7 +42,7 @@ export action select(id: number) { } export action assignToMe(id: number, user: string) { - guess assignError := "" + onFlush(() => { assignError := "" }) guess issues := list => withAssignee(list, id, user) try { await assign(id, user) diff --git a/examples/ai2/settings/settings.rx b/examples/ai2/settings/settings.rx index f57f9cab..2a60047a 100644 --- a/examples/ai2/settings/settings.rx +++ b/examples/ai2/settings/settings.rx @@ -20,9 +20,9 @@ export fn validate(s: Settings): Settings => // The truth. A source with a question (ST-5): `refresh` re-asks it, a save guesses it. export source settings: Settings = await loadSettings() -// What the last save failed with, or "". A question with a constant truth, so -// a new save can clear it as a guess the moment it starts (ST-5a). -export source saveError: string = "" +// What the last save failed with, or "". A new save clears it in an onFlush run +// of its own, so it goes the moment the save starts (EF-8). +export source saveError: string := "" export action retryLoad() { refresh settings @@ -30,7 +30,7 @@ export action retryLoad() { export action save(next: Settings) { guess settings := next // the page shows what is being saved, now (H-11) - guess saveError := "" // and stops showing the previous failure + onFlush(() => { saveError := "" }) // and stops showing the previous failure affects settings // `pending? settings` is true until the save ends (AC-5) try { let saved = await saveSettings(next) diff --git a/guide/async.md b/guide/async.md index 08f981e0..635abcd3 100644 --- a/guide/async.md +++ b/guide/async.md @@ -61,11 +61,11 @@ forecast = city == "" ? null : city.length < 3 ? throw TooShort(city) : await fe ```rx export source profile: Profile = await loadProfile() // a source with a question: it can take a guess -export source saveError: string = "" +export source saveError: string := "" export action save(next: Profile) { guess profile := next // shows now; reverts by itself if the save fails - guess saveError := "" // the old error goes now + onFlush(() => { saveError := "" }) // the old error goes now, in its own instant (EF-8) affects profile // `pending? profile` is true until the save ends try { let answer = await saveProfile(next) diff --git a/scenarios/S-ai2-settings.md b/scenarios/S-ai2-settings.md index 3cf087e3..764d27d6 100644 --- a/scenarios/S-ai2-settings.md +++ b/scenarios/S-ai2-settings.md @@ -22,8 +22,9 @@ file: ../examples/ai2/settings/main.rx | 7 | handler `save(draft)` | `Saved: Bea · dark · compact (saving…) Profile [compact] Display name 3/20 Appearance [compact] Theme: dark light dark Toggle density Preview: dark / compact Save` | `pending? settings` true (affects) | | 8 | reject "saveSettings:{"name":"Bea","theme":"dark","density":"compact"}" "quota exceeded" | `Saved: Ann · light · compact Save failed: quota exceeded Profile [compact] Display name 3/20 Appearance [compact] Theme: dark light dark Toggle density Preview: dark / compact Save` | `pending? settings` false | | 9 | handler `draftDensity := "comfortable"` | `Saved: Ann · light · compact Save failed: quota exceeded Profile [comfortable] Display name 3/20 Appearance [comfortable] Theme: dark light dark Toggle density Preview: dark / comfortable Save` | — | -| 10 | handler `save(draft)` | `Saved: Bea · dark · comfortable (saving…) Profile [comfortable] Display name 3/20 Appearance [comfortable] Theme: dark light dark Toggle density Preview: dark / comfortable Save` | `pending? settings` true | -| 11 | resolve "saveSettings:{"name":"Bea","theme":"dark","density":"comfortable"}" {"name":"Bea","theme":"dark","density":"comfortable"} | `Saved: Bea · dark · comfortable Profile [comfortable] Display name 3/20 Appearance [comfortable] Theme: dark light dark Toggle density Preview: dark / comfortable Save` | `pending? settings` false | +| 10 | handler `save(draft)` | `Saved: Bea · dark · comfortable (saving…) Save failed: quota exceeded Profile [comfortable] Display name 3/20 Appearance [comfortable] Theme: dark light dark Toggle density Preview: dark / comfortable Save` | `pending? settings` true | +| 11 | (flush) | `Saved: Bea · dark · comfortable (saving…) Profile [comfortable] Display name 3/20 Appearance [comfortable] Theme: dark light dark Toggle density Preview: dark / comfortable Save` | `pending? settings` true | +| 12 | resolve "saveSettings:{"name":"Bea","theme":"dark","density":"comfortable"}" {"name":"Bea","theme":"dark","density":"comfortable"} | `Saved: Bea · dark · comfortable Profile [comfortable] Display name 3/20 Appearance [comfortable] Theme: dark light dark Toggle density Preview: dark / comfortable Save` | `pending? settings` false | Why, one line per instant: @@ -34,8 +35,9 @@ Why, one line per instant: - **4.** An empty name makes `validate(draft)` fail with `NameRequired`. The `catch` turns that into its message, so `if nameError` opens the message branch (R-f-4, RG-1). The counter reads `0/20`. - **5.** 27 characters fails with `NameTooLong(27)`. `nameError` goes from one truthy string to another, so the branch is kept and the narrowed binding updates (RG-1b, RG-1c). - **6.** Two writes in one handler are one instant (W-3). The name is valid again, so `nameError` is "" and the branch closes. The ThemePicker label and the Preview both show the theme through the context. The saved line does not move. -- **7.** `save` guesses `settings := draft` and `saveError := ""`, then `affects settings` and awaits. The guess's readers form a lane and show at once (H-11, H-7). `pending? settings` is true because of `affects` (AC-5, H-9). The form is not recreated (the `if` goes truthy→truthy) and its draft does not change. +- **7.** `save` guesses `settings := draft`, clears `saveError` in an `onFlush` run of its own (EF-8; nothing to clear yet), then `affects settings` and awaits. The guess's readers form a lane and show at once (H-11, H-7). `pending? settings` is true because of `affects` (AC-5, H-9). The form is not recreated (the `if` goes truthy→truthy) and its draft does not change. - **8.** The rejection throws at the `await`. The `catch` writes `saveError` inside the still-open change (AC-7). At the end of the body the guess on `settings` is corrected to the unchanged truth (Ann), and that commits in one frame with the error message (H-11, AC-1). The draft is local to the form, so it keeps Bea/dark. - **9.** A plain local write changes the `density` provision, so both Panels and the Preview update. Nothing else moves. -- **10.** A new key (the density differs) means a new flight. The `settings` guess shows now. The `saveError` guess `""` closes the "Save failed" branch now (H-11; a lane condition's branch change attaches immediately, H-7). `pending? settings` is true. -- **11.** The answer is written as the truth (`settings := saved`), equal to the guess, so it confirms silently. The plain `saveError := ""` is the truth for that source too, so it does not revert to "quota exceeded". The change commits, and `pending? settings` is false. +- **10.** A new key (the density differs) means a new flight. The `settings` guess shows now (H-11). `pending? settings` is true. The old error is still up: its clear is registered with `onFlush`, not written by this run. +- **11.** The `onFlush` run is a change of its own and commits at once, while the save is still out (EF-8, S-flush-1): the "Save failed" branch closes. +- **12.** The answer is written as the truth (`settings := saved`), equal to the guess, so it confirms silently. The plain `saveError := ""` is the truth for that source too, so it does not revert to "quota exceeded". The change commits, and `pending? settings` is false.