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, 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.
- ST-1 (Structured vs atomic is the type). A source or equation whose
value is an object or array is structured; there is no separate store
primitive. Atomic lowers to
createSignal/createMemo, structured tocreateStore/createProjection. - ST-2 (Dependencies are per object identity, per key). A read of
o.kdepends on(o, k); a read of a reactive name depends on its root. Reachingothroughxtracks every step. A shared object reached through two names notifies readers through either (S-region-3 instant 6). - ST-2a (Writes).
x := vreplaces the root; E-6 per-path equality applies (an equal-by-paths value is a no-op and keeps the old identity).x.a.b := vmutates in place and notifies(x.a, b); readers ofxalone are not notified. Several path writes in one imperative run are one change (W-3). - ST-2b (Equation results). A structured equation result is compared per path with its previous value (E-6): equal → cut, keep old identity; otherwise the root changed. With a key (ST-4) the result is reconciled instead.
- ST-2c (Pending is per key). [Solid F-9, D-30, S-37] A held path write makes
pending? x.atrue and leavespending? x.bfalse; a root write or a re-ask of a source with a question (ST-5) makes the whole family pending (S-solid-37 and its variant).
An equation may write only its own draft, in its own body:
selected = draft(pre selected -> {}) { d[id] := true; d[prevId] := false }
- ST-3a.
draft(e) Blockevaluatese(normallypre x -> seed), binds a copy of it todin the block, runs the block (the only place:=appears in reactive scope, and only on paths rooted atd), and the equation's value isd. Writes are the delta (O(changes)); the copy is made along written paths only, so untouched sub-objects keep identity (S-store-2). E-6 then compares the draft with the previous value per path: an unchanged key's readers and effects are quiet. - ST-3b. The block is reactive scope for reads: every read is an input.
It is not imperative scope: no
iocalls, no writes to anything butd(W-1 applies), noawaitexcept as an equation input. - ST-3c. A draft never escapes its body: a closure that captures
dand runs later is rejected (O-3). Late answers areyield(ST-6), not callbacks into a draft.
- ST-4a.
x: T[] key k = e(abstract syntaxx key k = e; also on a source,source x key k = e) declares that elements ofxhave identityk. A new result is reconciled byk: rows with the same key keep their object identity and receive per-key path updates (their readers run only for changed keys); new keys create, missing keys remove; a reorder moves rows (S-store-3).pending?andlateston an element follow the key. - ST-4b.
for u in xover a keyed collection uses the key for row identity (RG-2a). A collection derived from a keyed one by selection (filter(rows, …)) keeps the rows' identity and needs no key of its own; an unkeyedforover records that are rebuilt on every change (mapinto fresh objects) is a diagnostic (UNSTABLE_LIST_IDENTITY, bin S). - ST-4c. Solid:
createProjection(fn, seed, { key }),<For keyed>.
- ST-5a.
source x = e(not:=) declares a source whose value is the equationeuntil written: it re-derives whene's inputs change, and a writex := voverrides it until the next re-derivation.emayawait.emay also be a constant (source editing = null,source cart = []): a question with no inputs, whose truth is that constant — the shape for provisional UI state that a guess (AC-3) can clear and that reverts to the truth when the hold ends (examples/users.rx, examples/cart.rx). Solid:createSignal(fn),createStore(fn, seed); written both plainly and optimistically, acreateSignal(fn)truth under acreateOptimistic(() => truth())layer. - ST-5b. Its question (V-9) is
e's inputs.refresh xre-asks it quietly;affects xdeclares that in-flight work will change it (H-9). - ST-5c. It and an equation (V-2) are the legal targets of
guess(AC-3): a guess needs a truth that will confirm or correct it, and an equation's truth is its expression. A guess is not a write of the truth, so an equation still takes no:=(W-1). A guessed equation lowers to Solid'screateOptimistic(fn)/createOptimisticStore(fn, seed), whose setter only guesses; a guessed source with a question to acreateSignal(fn)truth under acreateOptimisticlayer, because it is also written plainly. Not a deferred equation (GUESS_ON_DEFERRED): its value lags the one a guess would be confirmed by (V-16). Notpropsor a parameter (O-4): guess over the child's own equation of a prop (shown = props.title), which shows the guess where the child reads it (S-guess-equation).
- ST-6a.
yield vin an equation body is another answer to the same question: a quiet update (F-9), each landing its own change —pending?isfalseafter the first answer. A change to an input restarts the body (R-4) — later answers of the old body are inert — and runs itsfinally.yieldhas no meaning in imperative scope. Inref/a stream is a flight that lands more than once (S-store-5); the body form waits on 07's concrete syntax. - ST-6b.
await* eof an async iterable is its latest answer; the loop is implied (07: streams areawait*). A plainawaittakes one answer — of an async iterable, its first; later landings are not its answers (the three runners inref/follow this since 2026-10-24; S-store-5 reads its stream withawait*). A keyed structured stream reconciles each answer (ST-4). - ST-6c. Solid: an async-generator memo/projection;
live(fn).
- AC-1 (action).
action name(params) { … }declares aniofunction whose body is one change across its awaited work: every write between entry and return belongs to that change and is held until the body returns (F-2's explicit form) — that is, until everyawaitin it has landed. The questions its writes cause (a fetch re-asked because a source changed) are not the action's: they hold the change only as any change's pending readers do (H-2…H-6), so an action with noawait,select(id) { selected := id }, is an ordinary instant and aloading on(selected)shows its fallback at once (clarified 2026-10-24: all three agents of the second AI test asked). The handler that launches it is the same instant, so the handler's own plain writes — before or after the call — are the action's and are held with it (D-7's same-tick adoption); to land something now, make it a guess, a separate event, or anonFlushrun (02 EF-8).action { … }inline in a handler is the same for one run, and so is an arrow whose body guesses, declaresaffects, or writes and awaits (AC-1b). There is no escape: a write after anawaitis still the action's (the language has the scope, so none of 2.0's generator plumbing — S-9's "fresh write escapes" is unwritable; S-solid-9 is the inverted port). A rejection inside the body ends it like a return: the writes it held commit, and only its optimistic state reverts — a write is intent, a guess is the one provisional state (ruled 2026-10-22 against the oracle, DV-2; S-solid-8 and S-solid-32 variants). Compensation is the catch's job. An action body suspends only at a statement-levelawait(await eorlet v = await e, either with acatch (x: C) => altafter it: a matching rejection is the statement's value, R-f-4); anawaitanywhere else in imperative scope isAWAIT_IN_EXPRESSION(bind it first; S-await-1). The instant begins when the run suspends or returns (H-10). An action called from a handler is launched, not awaited: the handler continues, and the handler's own writes are the same instant's change (D-7's same-tick 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
guess x := e(AC-3),affects x(AC-5), or both a write to a source (W-1) and a statement-levelawait(07 decision 16). It is then(ps) => action { B }with every consequence of AC-1: one change across its awaited work, its writes held with it, its guesses living over that hold, and every failure it can produce caught (R-f-6). "Own statements" excludes the bodies of closures inside it, each inferred on its own, and an inlineaction { … }, which is its own run; it includes the statements of a local closure the body calls by name (let f = () => { … }; f()), whose writes are this run's. Calling an inferred arrow launches the action, as calling a declared one does. A body that awaits and writes nothing is a plain run. Aneffect'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 staysGUESS_OUTSIDE_HOLD. Lowers to Solid'sactionexactly asaction { … }does, nothing added (11). Solid: a TypeScript function's promises are invisible to the transition, so its author writesaction(function* () { … }); here every suspension is a statement the compiler sees, so the keyword is needed only to hold a write on purpose (AC-1c) or to name an action (S-infer-1, 3, 4). - AC-1c (Held write). [ruled 2026-10-28] In an inferred action (AC-1b),
a plain write from which some path of the body reaches an
awaitis a compile error (HELD_WRITE): it would be held until the work ends (AC-1), 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,guess x := von 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) isonFlush(() => { x := v })(02 EF-8). A write meant to be held — a transition:action go() { id := 2; await prefetch() }(S-solid-20) — is written in a declared action, which this check does not cover:actionis the author saying "hold it". Guesses andaffectsare not plain writes (S-infer-2). The check is the language's, not Solid's (Solid cannot see a body): it adds nothing at run time. - AC-1d (Stale own read). [ruled 2026-10-28] In any imperative run — a
handler, an apply block, an action body, declared or inferred — reading a
source that the same change has already written, on some path before the
read, is a compile error (
STALE_OWN_READ). The read gets the committed value, not the write (02 W-4, 05 H-10, SC-2). Solid agrees: in 2.0.0-rc.19 anaction()that writes a signal, yields, and reads it again gets the old value until the action commits. "The same change" is the whole body for an action (AC-1) and the stretch up to the nextawaitfor a plain run, where theawaitends the change (07 §5). Keys count: afterx.a := v, readingx.aorxis stale and readingx.bis not. A guess is a write the run does not see either (H-10). A functional write (x := v => …, W-6) reads nothing now; a closure's body runs later, except a local closure the run calls, whose reads are the run's;pending?andlatestare probes, not snapshots. Every other read is the committed snapshot it always was, with no error. The fix is the local the value came from (let n = seq + 1; seq := n; … n) or a functional write (S-infer-5). - AC-1e (Stale write-back). [taken by default 2026-10-28] In any
imperative run, a plain value write
x := eafter anawait, whereeis computed from a read ofxtaken before thatawait— directly, or through locals bound from it — is a compile error (STALE_WRITE_BACK). The read isxas the run started; whatever changedxwhile the run waited (another action, a reload, a click) is erased by the write: a lost update. Keys count as in AC-1d: a read ofx.aorxand a write ofx.aorxoverlap; a read ofx.band a write ofx.ado not; an index computed fromx(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 toxas it stands at the write, or a guess (guess, AC-3) that the answer replaces. A guess or a write before theawaitis the change's own and is not checked; nor is an awaited answer (let r = await …), which is the server's, not a snapshot ofx. The check is the language's (Solid cannot see the read, the suspension and the write in one body): it adds nothing at run time. Found by the AI eval's round 4 (eval/results/change-2026-10-28): 2 of its 3 .rx defects, and a latent one in a run that passed every test; on all 76 eval .rx apps and the repository's examples it fires on those 3 runs only (S-infer-6). - AC-2 (Not special). An action is only a way to tie writes to async
work; the hold is the thing (H-2).
refresh,affectsand probes behave the same inside and outside one. - AC-3 (guess).
guess x := e, legal only inside an action body, declared or inferred (AC-1b); in aneffect's apply block, which is never an action, it isGUESS_OUTSIDE_HOLD, and only on a source with a question (ST-5) or on an equation (ST-5c; elseGUESS_NO_QUESTION): writes a guess that is the graph's and the screen's value forxfrom the instant's settle, display-ahead and verdict-inert, until the hold it was made over commits orx'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 anawait. Lowers tocreateOptimistic/createOptimisticStoreby usage. Path targets (guess x.a := v,guess todos[i].done := true) guess per key, as Solid's per-node override: the rest ofxis 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 action that guessed (refresh x) is authoritative work in flight: the change waits for that answer before the body-end correction (D-34) — for a guess onxitself, and for a guess on another source whose truth may followx(S-app-todos: an add guessed in its own list stays until the re-asked server list brings the row; a declined toggle's revert and its recorded failure land with that list, in one frame). Solid does the same: the action's transition holds until the refresh it started lands. Lowers tocreateOptimisticStore(fn, seed); a plain write to a source that is also guessed by path is not lowered yet. A guess is verdict-inert: an action that wantspending? xtrue while its guess stands declaresaffects x(AC-5; the idiom is in 07 §5). - AC-3a (A guess while its value loads a new question). [taken by default
2026-11-12; follows Solid's ruling on solidjs/solid#4795] While an equation
or source
xthat awaits is loading a new question (its inputs changed), a guess on it composes on the previous answer, and the new answer replaces it (D-34; a guess the screen never showed is void, and one it showed stays only until its hold commits). Before a first answer the guess is judged by that answer the same way (S-56). Either way the user's change is not on screen while its action is out. A handler (a plain arrow or an inferred action, or a named action it calls) that guessesx, wherex's question reads a name the handler or that action reads (what the click is about: a selection, a row), must be covered as in R-8: the element sits inside aloadingregion whose content readsx, or itsdisabledreadsx(for a<form>'sonSubmit, every submit button'sdisableddoes, since a form with its submit buttons disabled does not submit). Otherwise it isGUESS_WHILE_LOADING(static). The repairs are Solid's: the control insidex'sloading, ordisabled={pending? x}, or the pending change kept in a source of its own until it lands. Solid's dev build warns the same case at the guess (OPTIMISTIC_WRITE_WHILE_LOADING); the check finds it before the race runs. Principle: a write must not read stale state (the guess's function reads the previous question's answer). Found by the AI eval: over round 12's .rx apps it flags 8 handlers in 5 apps, and driving each in a browser (select, then rename or post while the selection loads) loses the rename or the issue's existing comment in every one, and in no unflagged app (eval/results/desk12-2026-11-11/repro/probe-guess-while-loading.*). It also fired on the eval's own references (the rename and post buttons sat beside the detail'sloadingregion) and onexamples/ai/feed.rx(send while a new room loads); those now disable the control while it loads. - AC-4 (refresh).
refresh x, in imperative scope anywhere, re-asksx's question (ST-5) or re-runs anawaitequation for the same inputs: quiet —pending? xstaysfalse,xkeeps showing its last answer while the re-ask is up, and the fresh value reveals silently (S-solid-26 instants 4–5, S-solid-37). On a question source it drops a write's override (S-store-4). Issued inside an open change, the landing is staged into that change and reveals at its commit, still quiet (Solid D-31; S-solid-26 instant 7). Any other target isINVALID_REFRESH_TARGET. An awaitedrefresh xresolves when the re-ask lands (not inref/). - AC-5 (affects).
affects x, in imperative scope anywhere, declares that work now in flight will changex:pending? xistrueuntil the change it was declared in commits (S-solid-26 instants 6–8). In an arrow it makes the arrow an action (AC-1b), since only an action's change lasts across the work (S-infer-4); in an apply block it lasts one instant. - AC-7 (try/catch). [ruled 2026-10-23] Actions are imperative code,
so they fail the way imperative code does:
try { … } catch (e) { … } finally { … }andthrow eare statements of imperative scope. A rejectedawaitthrows at theawait; acatchinside the body runs inside the action — the same change, still open — so its writes land with the body's end in one frame (S-action-1: the reverted guess andstatus := "failed: …"commit together).finallyruns on the way out either way. An uncaught throw or rejection is the action's rejection (AC-1, DV-2) — and since R-f-6 (ruled 2026-10-23) a compile error for any failure the body can produce: an action catches what it can fail with, and only a panic reaches DV-2.catch (e: A | B)takes those classes and rethrows the rest; a snapshot of a failing value throws at the read (R-f-7). In a plain handler the same statements work with no hold involved and no check. Solid: a generator action'stryaroundyield— the rejection is thrown back at the yield point. - AC-6 (until). [executable 2026-10-25]
await until(() => p)in imperative scope waits until the predicatepsettles truthy and gives that value. It is a function of the language, not syntax — Solid'suntil— and is called from imperative scope (in reactive scope,IO_IN_REACTIVE); its argument is a predicate,() => e(UNTIL_PREDICATE), whose body is reactive scope: it reads, tracked, and does not write.preads the authoritative view: guesses (AC-3), and whatever is derived from them, are invisible, so a guess never satisfies the wait for its own confirmation; staged truth reads normally. Pending is "not yet". Whenpfails,untilrejects with that failure, soawait until(…)fails with what its predicate can fail with (R-f-6 — a predicate that cannot fail needs notry). The write or landing that flipspjoins the awaiting action's change and reveals with it (Solid D-31): the action resumes in that instant (S-action-2). Not yet: Solid's{ timeout, signal }options.
| code | bin | rule |
|---|---|---|
ASYNC_STORE_SETTER |
U | ST-3b: a draft block has no await statement |
IMMUTABLE_UPDATE_IN_STORE |
U | ST-3: a structured equation is a delta over pre |
UNSTABLE_LIST_IDENTITY |
S | ST-4b |
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 |
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 |
GUESS_NO_QUESTION (new) |
S | AC-3 / ST-5c |
GUESS_ON_DEFERRED (new) |
S | AC-3 / ST-5c: a deferred equation lags its truth |
GUESS_WHILE_LOADING (new) |
S | AC-3a: a guess a click can make while its value loads a new question (D-34) |
S-store-1: path writes notify per key; a root write compares per path (ST-2). ✓S-store-2: draft delta keeps identity (ST-3). ✓S-store-3: keyed reconcile; rows keep identity across a new result (ST-4). ✓S-store-4:source x = ederived until written; refresh is quiet (ST-5, AC-4). ✓S-store-5: stream of answers, restart on input change (ST-6). ✓- Solid ports: S-8 (+ rejection variant), S-9 (inverted: no escape), S-15, S-20 (+ the race the fallback won), S-26 rows 4–7 (+ verdict-inert guess), S-32 (+ two rejected variants), S-33, S-34, S-35 (+ derived flight), S-37 (+ per-key variant). ✓