Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1ed421e
eval: React reference for desk9 (grades 0: 77/77, 38/38, 0/20)
claude Nov 14, 2026
91ec6ae
eval: desk9 XState reference (grades 0)
claude Nov 14, 2026
f072d16
eval: desk9 vanilla reference (grades 0)
claude Nov 14, 2026
8e67b41
eval: desk9 Foldkit reference (grades 0)
claude Nov 14, 2026
883b0cc
eval fuzz: A4 also covers detail, labels, comments and two views
claude Nov 14, 2026
53e2f68
eval: --r15 fuzz rule (server's last answer wins at rest); React and …
claude Nov 14, 2026
274f306
eval: plain Solid repro of a stale heading after select (rx desk9 ref…
claude Nov 14, 2026
472e73e
eval: board task (round 15 held-out), its fuzzer, and the React refer…
claude Nov 14, 2026
6ba2af8
eval: board .rx reference (grades 0) + plain-Solid repro of stale lab…
claude Nov 14, 2026
ea24896
eval: fuzz-board B1 also exempts the duplicate-card check in a late-w…
claude Nov 14, 2026
882172f
eval: board vanilla reference (grades 0)
claude Nov 14, 2026
2df041f
eval: board XState reference (grades 0)
claude Nov 14, 2026
0cba38d
eval: board Foldkit reference (grades 0)
claude Nov 14, 2026
1074753
eval: board Exact (contract) reference (grades 0)
claude Nov 14, 2026
7278c87
eval: board fuzz checks every card's count against the server; round …
claude Nov 14, 2026
a169017
eval: fuzz-board B3, legal holds (as desk9 A1/A5): held detail and an…
claude Nov 14, 2026
f9edf8f
eval: prereg records the Exact move-error limit (Ryan's call)
claude Nov 14, 2026
edde969
eval: guide-shaped .rx board (rx-guide) and repros for solidjs/solid#…
claude Nov 14, 2026
2d3b0eb
eval: repro for the page hang from solid 9e02d02a8 (board guide shape…
claude Nov 14, 2026
fbf898d
eval: idiomatic one-store .rx board (rx-store) and its repros (solidj…
claude Nov 14, 2026
27377f7
catch reads in handlers are snapshots; ARGUMENT_COUNT and props of C(…
claude Nov 14, 2026
b5ddae5
eval: store board fixes (placeholder list, pending? in handlers, reas…
claude Nov 15, 2026
38b2125
eval: board .rx placeholder back in the store, per-column store versi…
claude Nov 15, 2026
9a7b294
eval: rx-cols grade notes
claude Nov 15, 2026
a78c746
eval: rx-store grade notes
claude Nov 15, 2026
c58dba7
ST-4a: a key applies at any depth, as Solid's (lists under a record l…
claude Nov 15, 2026
2bd5cd9
eval: repros for solidjs/solid#4888 and #4889
claude Nov 15, 2026
4a192c5
eval: rx-cols re-asks the card's server column after a move's answer …
claude Nov 15, 2026
4a2b41a
eval: seed 41 traced to solidjs/solid#4897 (declined variant)
claude Nov 15, 2026
8876be0
guide: one keyed store per fetch; moves guess each list they change; …
claude Nov 15, 2026
20589f4
prereg: shared-list holds excluded as by design (Ryan, 2026-11-15)
claude Nov 15, 2026
d2bfd96
eval: rx-cols re-asks every column whose answer has the moved card; 0…
claude Nov 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
row, so a row's readers re-run only when its own leaves change (#51).
- The guide's action example (`guide/async.md` §3) lost an error thrown before the first `await`: the `onFlush` clear
ran after the catch. The catch now writes the error through `onFlush` as well (#52).
- A `catch` expression read with no observer (a component `fn` called from a handler or an action) built a fresh error
boundary, which had no value while a `refresh` was out, so the click threw `NotReadyError` and was lost. It is now a
snapshot: the committed value, or the alternative on a matching failure (R-8, S-catch-snapshot).

**New**

- `ARGUMENT_COUNT` (AC-1f): a call of an action declared in the module with another number of arguments than its
parameters is refused.
- `props` in a component declared without props (`component C()`) is `UNBOUND` (RG-4b); it read `undefined`.
- `guide/async.md`: "One store over the server"; the `GUESS_WHILE_LOADING` advice says what holds when a selection
changes while a guess is out (05 DV-22).

## 0.3.1

Expand Down
5 changes: 3 additions & 2 deletions compiler/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub enum Decl {
TypeDecl { name: String, line: usize, span: Span, text: String, exported: bool },
Effect { inputs: Vec<Expr>, body: Vec<Stmt>, line: usize, span: Span },
Fn { name: String, params: Vec<Param>, decls: Vec<Decl>, tail: Expr, line: usize, span: Span, ret: Option<String>, exported: bool },
Component { name: String, props: String, decls: Vec<Decl>, view: Vec<Markup>, line: usize, span: Span, props_type: Option<String>, exported: bool, is_default: bool },
Component { name: String, props: String, decls: Vec<Decl>, view: Vec<Markup>, line: usize, span: Span, props_type: Option<String>, no_props: bool, exported: bool, is_default: bool },
Action { name: String, params: Vec<String>, ptypes: Vec<Option<String>>, body: Vec<Stmt>, line: usize, span: Span, exported: bool },
Server { name: String, params: Vec<String>, ptypes: Vec<Option<String>>, ret: Option<String>, body: Vec<Stmt>, line: usize, span: Span, exported: bool },
Provide { name: String, e: Expr, line: usize, span: Span },
Expand Down Expand Up @@ -584,14 +584,15 @@ impl Decl {
("ret", os(ret)),
("exported", flag(*exported)),
]),
Decl::Component { name, props, decls, view, line, props_type, exported, is_default, .. } => obj(vec![
Decl::Component { name, props, decls, view, line, props_type, no_props, exported, is_default, .. } => obj(vec![
("k", s("component")),
("name", s(name)),
("props", s(props)),
("decls", Some(decls_json(decls))),
("view", Some(markups_json(view))),
("line", num(*line)),
("propsType", os(props_type)),
("noProps", flag(*no_props)),
("exported", flag(*exported)),
("isDefault", flag(*is_default)),
]),
Expand Down
62 changes: 56 additions & 6 deletions compiler/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ struct Sym<'a> {
foreign: Option<FShape>,
/// MO-10d: bound from a foreign accessor — it may hold, as a question's reader does
reader: bool,
/// an action declared here: how many arguments a call passes (AC-1f)
arity: Option<usize>,
/// RG-4b: the props of `component C()`, which declares none — C's name: reading them is UNBOUND
no_props: Option<String>,
}
impl<'a> Sym<'a> {
fn new(kind: SymKind, name: &str, region: RId, line: i64) -> Self {
Expand Down Expand Up @@ -278,6 +282,8 @@ impl<'a> Sym<'a> {
decl_env: None,
foreign: None,
reader: false,
arity: None,
no_props: None,
}
}
}
Expand Down Expand Up @@ -1199,7 +1205,10 @@ impl<'a> Checker<'a> {
match d {
Decl::Server { params, .. } => sym.params = Some(params.iter().map(|n| SigParam { name: n.clone(), r#ref: false, ty: None }).collect()),
Decl::Source { question, .. } => sym.question = *question,
Decl::Action { .. } => sym.io = true,
Decl::Action { params, .. } => {
sym.io = true; // R-io-2: an action is io
sym.arity = Some(params.len()); // AC-1f: its parameters
}
Decl::Fn { params, decls: fdecls, .. } => {
sym.params = Some(params.iter().map(|p| SigParam { name: p.name.clone(), r#ref: p.r#ref, ty: p.ty.clone() }).collect());
sym.instantiating = params.iter().any(|p| p.r#ref) || !fdecls.is_empty();
Expand Down Expand Up @@ -1865,11 +1874,15 @@ impl<'a> Checker<'a> {
sym.fails_t = Some(FailsT::Expr { e: tail, env: inner, cache: None, busy: false });
sym.rows = Some(RowsT::Expr { e: tail, env: inner, cache: None, busy: false });
}
Decl::Component { name, props, decls, view, line, .. } => {
Decl::Component { name, props, decls, view, line, no_props, .. } => {
let s = self.lookup(env, name).unwrap();
let r = self.syms[s].comp_region.unwrap();
let inner = self.env(Some(env), r);
self.bind(inner, props, Sym::new(SymKind::Props, props, r, *line as i64));
let mut ps = Sym::new(SymKind::Props, props, r, *line as i64);
if *no_props {
ps.no_props = Some(name.clone());
}
self.bind(inner, props, ps);
self.syms[s].comp_env = Some(inner);
self.block(decls, view, inner, Ctx::reactive());
self.later.push(Later::Content { ms: view, env: inner, anc: vec![] });
Expand Down Expand Up @@ -1995,6 +2008,9 @@ impl<'a> Checker<'a> {
// ---- expressions ----
fn resolve(&mut self, name: &str, env: EId, ctx: Ctx) -> Option<SId> {
if let Some(s) = self.lookup(env, name) {
if let Some(c) = self.syms[s].no_props.clone() {
self.err("UNBOUND", "RG-4b", format!("{} is not declared here: component {} declares no props — declare what it takes, `component {}(props: {{ … }})`, and pass it where it is applied", name, c, c));
}
self.sided(s, ctx);
return Some(s);
}
Expand Down Expand Up @@ -2264,9 +2280,9 @@ impl<'a> Checker<'a> {
return;
}
let s = self.lookup(env, fname);
let (io, kind, side, io_via, instantiating) = match s {
Some(s) => (self.syms[s].io, Some(self.syms[s].kind), self.syms[s].side, self.syms[s].io_via.clone(), self.syms[s].instantiating),
None => (false, None, None, None, false),
let (io, kind, side, io_via, instantiating, arity) = match s {
Some(s) => (self.syms[s].io, Some(self.syms[s].kind), self.syms[s].side, self.syms[s].io_via.clone(), self.syms[s].instantiating, self.syms[s].arity),
None => (false, None, None, None, false, None),
};
if !ctx.imperative && (io || (s.is_none() && fname == "log")) {
if ctx.in_fn {
Expand All @@ -2289,6 +2305,16 @@ impl<'a> Checker<'a> {
if kind == Some(SymKind::Comp) {
self.err("COMPONENT_AS_VALUE", "RG-4a", format!("component {} called as a function: apply it in a view", fname));
}
if let (Some(SymKind::Action), Some(n)) = (kind, arity) {
if args.len() != n {
let takes = match n {
0 => "no arguments".to_string(),
1 => "1 argument".to_string(),
n => format!("{} arguments", n),
};
self.err("ARGUMENT_COUNT", "AC-1f", format!("{} takes {} and is called with {}: an action's parameters are its whole input — pass each one, and nothing it does not declare", fname, takes, args.len()));
}
}
self.model.call(s, kind.map(|k| k.as_str()).unwrap_or(""));
}
self.expr(f, env, ctx, None);
Expand Down Expand Up @@ -4511,4 +4537,28 @@ export default component App() {
// outer `on(sel)` region's own read of `detail` beside the inner one
assert_eq!(got, vec![8, 24]);
}

/// 06 AC-1f: a call of a declared action passes one argument per parameter; 03 RG-4b: `component C()` has no
/// `props` (the eval board's check-accepts-arity-and-missing-props.rx).
#[test]
fn argument_count_and_missing_props() {
let src = r#"component Lane() { <h3>{props.col}</h3> }
component Card(props: { col: string }) { <p>{props.col}</p> }
export default component App() {
source n := 0
action go() { n := 1 }
action by(k: number, m: number) { n := k + m }
<div>
<Lane col="todo" />
<Card col="done" />
<button onClick={() => { go(1, 2) }}>a</button>
<button onClick={() => { go() }}>b</button>
<button onClick={() => { by(1) }}>c</button>
<button onClick={() => { by(1, 2) }}>d</button>
</div>
}
"#;
let got: Vec<(i64, String)> = codes(src, false).into_iter().filter(|(_, c)| c == "ARGUMENT_COUNT" || c == "UNBOUND").collect();
assert_eq!(got, vec![(1, "UNBOUND".to_string()), (10, "ARGUMENT_COUNT".to_string()), (12, "ARGUMENT_COUNT".to_string())], "{:?}", codes(src, false));
}
}
8 changes: 5 additions & 3 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,7 @@ impl<'p> Gen<'p> {
}
Decl::Source { name, init, key, .. } => {
let lv = self.loading_value(init, env)?;
let mode = obj(vec![("optimistic", E::Bool(self.u.optimistic.contains(name))), ("structured", E::Bool(self.u.paths.contains(name)))]);
let mode = obj(vec![("optimistic", E::Bool(self.u.optimistic.contains(name))), ("structured", E::Bool(self.u.paths.contains(name))), ("record", E::Bool(matches!(init.k, ExprKind::Rec(_))))]);
let x = self.expr(init, env, &Ctx { tail: true, self_: Some(name.clone()), ..Ctx::default() })?;
let mut args = vec![str(name.clone()), arrow(&["__prev"], x), lv, mode];
if let Some(k) = key {
Expand All @@ -995,7 +995,9 @@ impl<'p> Gen<'p> {
Decl::Eq { name, e, key, deferred, .. } if self.guessed_eq(name, e, *deferred) => {
// AC-3 over an equation: createOptimistic(fn) / createOptimisticStore(fn, seed) — derived, its setter only guesses
let lv = self.loading_value(e, env)?;
let mode = obj(vec![("optimistic", E::Bool(true)), ("structured", E::Bool(self.u.paths.contains(name) || key.is_some())), ("derived", E::Bool(true))]);
// ST-4a: a key applies at any depth (Solid's); a record result is the store's root, its keyed lists inside it
let record = matches!(e.k, ExprKind::Rec(_));
let mode = obj(vec![("optimistic", E::Bool(true)), ("structured", E::Bool(self.u.paths.contains(name) || key.is_some())), ("derived", E::Bool(true)), ("record", E::Bool(record))]);
let x = self.expr(e, env, &Ctx { tail: true, self_: Some(name.clone()), ..Ctx::default() })?;
let mut args = vec![str(name.clone()), arrow(&["__prev"], x), lv, mode];
if let Some(k) = key {
Expand Down Expand Up @@ -1037,7 +1039,7 @@ impl<'p> Gen<'p> {
}
if let Some(k) = key {
let x = self.expr(e, env, &Ctx { tail: true, ..Ctx::default() })?;
return fin(vec![konst(b.js.clone(), rtc("keyed", vec![str(name.clone()), str(k.clone()), thunk(x)]))], self);
return fin(vec![konst(b.js.clone(), rtc("keyed", vec![str(name.clone()), str(k.clone()), thunk(x), E::Bool(matches!(e.k, ExprKind::Rec(_)))]))], self);
}
// R-f-4 over a tail await: the flight is an inner memo, read through a boundary
if let (ExprKind::Catch { e: ce, name: cn, classes, alt }, false) = (&e.k, deferred) {
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@ fn parse_decl_at(c: &mut Cursor) -> R<Decl> {
c.ws(true);
// RG-4b: `component C({ a, b })` — each name a projection of props
let sig = if c.peek() == '{' { Some(parse_pattern(c)?) } else { None };
let no_props = sig.is_none() && c.peek() == ')'; // `component C()`: no props (RG-4b)
let props = if sig.is_some() || c.peek() == ')' { "props".to_string() } else { c.ident_or("props name")? };
let props_type = opt_type(c, false)?;
c.ws(true);
Expand All @@ -1177,7 +1178,7 @@ fn parse_decl_at(c: &mut Cursor) -> R<Decl> {
let pre: Vec<Decl> = names.into_iter().map(|(name, e, line, span)| Decl::Eq { name, e, line, span, deferred: false, key: None, ty: None, exported: false }).collect();
decls.splice(0..0, pre);
}
return Ok(Decl::Component { name, props, decls, view, line, span: Span::NONE, props_type, exported: false, is_default: false });
return Ok(Decl::Component { name, props, decls, view, line, span: Span::NONE, props_type, no_props, exported: false, is_default: false });
}
let save = c.i;
let name = c.ident();
Expand Down
Loading
Loading