diff --git a/compiler/src/ast.rs b/compiler/src/ast.rs index 46bcbe77..66bdff89 100644 --- a/compiler/src/ast.rs +++ b/compiler/src/ast.rs @@ -90,6 +90,8 @@ pub enum ExprKind { /// RG-7b Use(String), Spread(Box), + /// RG-4b: an object rest, `{ a, ...rest } = e` (the parser's own; Solid's `omit`) + Omit { e: Box, keys: Vec }, /// R-f-3 Throw(Box), /// R-f-4: `e catch (x: A | B) => alt`; classes absent: every failure @@ -422,6 +424,7 @@ impl Expr { ExprKind::Draft { .. } => "draft", ExprKind::Use(_) => "use", ExprKind::Spread(_) => "spread", + ExprKind::Omit { .. } => "omit", ExprKind::Throw(_) => "throw", ExprKind::Catch { .. } => "catch", ExprKind::StyleMap { .. } => "stylemap", @@ -439,6 +442,7 @@ impl Expr { ExprKind::Un { op, e } => obj(vec![("k", k), ("op", s(op)), ("e", b(e))]), ExprKind::Pre { name, init } => obj(vec![("k", k), ("name", s(name)), ("init", ob(init))]), ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Throw(e) => obj(vec![("k", k), ("e", b(e))]), + ExprKind::Omit { e, keys } => obj(vec![("k", k), ("e", b(e)), ("keys", Some(strs(keys)))]), ExprKind::Await { e, init, stream } => obj(vec![("k", k), ("e", b(e)), ("init", ob(init)), ("stream", Some(Value::Bool(*stream)))]), ExprKind::If { c, t, f } => obj(vec![("k", k), ("c", b(c)), ("t", b(t)), ("f", b(f))]), ExprKind::Tpl(parts) => obj(vec![ diff --git a/compiler/src/check.rs b/compiler/src/check.rs index 8dcec25c..780b01aa 100644 --- a/compiler/src/check.rs +++ b/compiler/src/check.rs @@ -353,7 +353,7 @@ pub(crate) fn children<'a>(n: Node<'a>, paths: bool) -> Vec> { Node::E(e) => match &e.k { ExprKind::Lit(_) | ExprKind::Id(_) | ExprKind::Use(_) | ExprKind::StyleMap { .. } => {} ExprKind::Bin { l, r, .. } => out.extend([Node::E(l), Node::E(r)]), - ExprKind::Un { e, .. } | ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Throw(e) => out.push(Node::E(e)), + ExprKind::Un { e, .. } | ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Omit { e, .. } | ExprKind::Throw(e) => out.push(Node::E(e)), ExprKind::Pre { init, .. } => out.extend(init.iter().map(|x| Node::E(x))), ExprKind::Await { e, init, .. } => { out.push(Node::E(e)); @@ -438,7 +438,7 @@ fn free_names(e: &Expr, bound: &HashSet, out: &mut IndexSet) { go(i, out); } } - ExprKind::Latest(x) | ExprKind::Pending(x) | ExprKind::Un { e: x, .. } | ExprKind::Spread(x) => go(x, out), + ExprKind::Latest(x) | ExprKind::Pending(x) | ExprKind::Un { e: x, .. } | ExprKind::Spread(x) | ExprKind::Omit { e: x, .. } => go(x, out), ExprKind::Bin { l, r, .. } => { go(l, out); go(r, out); @@ -2051,7 +2051,7 @@ impl<'a> Checker<'a> { self.expr(x, env, ctx, None); self.model.probe = save; } - ExprKind::Un { e: x, .. } | ExprKind::Spread(x) => self.expr(x, env, ctx, None), + ExprKind::Un { e: x, .. } | ExprKind::Spread(x) | ExprKind::Omit { e: x, .. } => self.expr(x, env, ctx, None), ExprKind::Await { e: inner, init, .. } => { if ctx.imperative && !self.stmt_awaits.contains(&(e as *const Expr)) { let sh = show(e); diff --git a/compiler/src/emit.rs b/compiler/src/emit.rs index 342a660e..0ed92891 100644 --- a/compiler/src/emit.rs +++ b/compiler/src/emit.rs @@ -1178,6 +1178,8 @@ impl<'p> Gen<'p> { rtc("preOr", vec![call(id(p), vec![]), thunk(i)]) } ExprKind::Peek(x) => rtc("untrack", vec![thunk(self.expr(x, env, &sub)?)]), + // RG-4b: Solid's live view + ExprKind::Omit { e: x, keys } => rtc("omit", std::iter::once(self.expr(x, env, &sub)).chain(keys.iter().map(|k| Ok(str(k.clone())))).collect::, _>>()?), ExprKind::Await { e: x, stream, .. } => { if let Some(v) = self.hoisted.get(&(e as *const Expr)) { return Ok(call(id(v.clone()), vec![])); diff --git a/compiler/src/infer.rs b/compiler/src/infer.rs index 02413af7..a4056e28 100644 --- a/compiler/src/infer.rs +++ b/compiler/src/infer.rs @@ -271,7 +271,7 @@ impl Walker { self.read(l, st, sh, local, depth); self.read(r, st, sh, local, depth); } - ExprKind::Un { e, .. } | ExprKind::Spread(e) | ExprKind::Throw(e) => self.read(e, st, sh, local, depth), + ExprKind::Un { e, .. } | ExprKind::Spread(e) | ExprKind::Omit { e, .. } | ExprKind::Throw(e) => self.read(e, st, sh, local, depth), ExprKind::Await { e, init, .. } => { self.read(e, st, sh, local, depth); if let Some(i) = init { @@ -362,7 +362,7 @@ impl Walker { self.deps(l, st, sh, out); self.deps(r, st, sh, out); } - ExprKind::Un { e, .. } | ExprKind::Spread(e) | ExprKind::Throw(e) => self.deps(e, st, sh, out), + ExprKind::Un { e, .. } | ExprKind::Spread(e) | ExprKind::Omit { e, .. } | ExprKind::Throw(e) => self.deps(e, st, sh, out), ExprKind::If { c, t, f } => { self.deps(c, st, sh, out); self.deps(t, st, sh, out); diff --git a/compiler/src/parse.rs b/compiler/src/parse.rs index b0a4e017..e31d4ae1 100644 --- a/compiler/src/parse.rs +++ b/compiler/src/parse.rs @@ -72,10 +72,14 @@ pub struct Cursor<'a> { /// on with an empty one, so that what is being typed still projects pub recover: bool, pub errors: Vec, + /// a destructuring `let` (07 §5) is several statements: the rest of them, after the first + pub pending: Vec, + /// `$$n`: the bound value of a destructuring whose right side is not a name (RG-4b) + pub temps: usize, } impl<'a> Cursor<'a> { pub fn new(src: &'a str) -> Self { - Cursor { src, i: 0, recover: false, errors: vec![] } + Cursor { src, i: 0, recover: false, errors: vec![], pending: vec![], temps: 0 } } /// What `fail` would return, recorded instead (`recover`). fn note(&mut self, msg: &str) { @@ -304,7 +308,7 @@ pub fn parse_program(src: &str) -> R { let mut decls = vec![]; c.ws(true); while !c.eof() && looks_like_decl(&mut c) { - decls.push(parse_decl(&mut c)?); + decls.extend(parse_decls1(&mut c)?); c.ws(true); c.eat(";"); c.ws(true); @@ -357,8 +361,8 @@ pub fn parse_program_recovering(src: &str) -> (Program, Vec) { } } else { let (at, n) = (c.i, c.errors.len()); - match parse_decl(&mut c) { - Ok(d) => decls.push(d), + match parse_decls1(&mut c) { + Ok(d) => decls.extend(d), Err(e) => { // what the failed attempt noted on the way is moot: the declaration is out c.errors.truncate(n); @@ -532,6 +536,221 @@ fn params_typed(c: &mut Cursor) -> R<(Vec, Vec>)> { Ok((params, ptypes)) } +/// RG-4b, 07 §5: a destructuring pattern, `{ a, b: c, d = 1, e: { f }, ...rest }` or `[a, , b, ...rest]`. +enum Pat { + Name { name: String, line: usize, span: Span }, + Obj(Vec<(String, Pat, Option)>, Option>), + Arr(Vec)>>, Option>), +} +/// `...name`, the last element of a pattern closed by `close`. +fn parse_rest(c: &mut Cursor, close: &str) -> R { + c.i += 3; + c.ws(true); + let (line, start) = (c.line(), c.i); + let name = match c.ident() { + Some(n) if !is_kw(&n) => n, + _ => return c.fail("a name for the rest"), + }; + let span = Span::new(start, c.i); + c.ws(true); + if !c.starts_with(close) { + return c.fail(&format!("a rest element is last, with no default: `...{name}{close}`")); + } + Ok(Pat::Name { name, line, span }) +} +fn pat_default(c: &mut Cursor) -> R> { + if c.peek() == '=' && c.peek_at(1) != '=' && c.peek_at(1) != '>' { + c.bump(); + c.ws(true); + return Ok(Some(parse_expr(c)?)); + } + Ok(None) +} +fn parse_pattern(c: &mut Cursor) -> R { + c.ws(true); + if c.eat("{") { + let mut props = vec![]; + loop { + c.ws(true); + if c.starts_with("}") { + break; + } + if c.starts_with("...") { + let rest = parse_rest(c, "}")?; + c.expect("}")?; + return Ok(Pat::Obj(props, Some(Box::new(rest)))); + } + let (kline, kstart) = (c.line(), c.i); + let key = c.ident_or("a field name")?; + let kspan = Span::new(kstart, c.i); + c.ws(true); + let pat = if c.eat(":") { parse_pattern(c)? } else { Pat::Name { name: key.clone(), line: kline, span: kspan } }; + c.ws(true); + let dflt = pat_default(c)?; + props.push((key, pat, dflt)); + c.ws(true); + if !c.eat(",") { + break; + } + } + c.ws(true); + c.expect("}")?; + return Ok(Pat::Obj(props, None)); + } + if c.eat("[") { + let mut items = vec![]; + loop { + c.ws(true); + if c.starts_with("]") { + break; + } + if c.starts_with(",") { + c.bump(); + items.push(None); + continue; + } + if c.starts_with("...") { + let rest = parse_rest(c, "]")?; + c.expect("]")?; + return Ok(Pat::Arr(items, Some(Box::new(rest)))); + } + let pat = parse_pattern(c)?; + c.ws(true); + let dflt = pat_default(c)?; + items.push(Some((pat, dflt))); + c.ws(true); + if !c.eat(",") { + break; + } + } + c.ws(true); + c.expect("]")?; + return Ok(Pat::Arr(items, None)); + } + let (line, start) = (c.line(), c.i); + let name = c.ident(); + match name { + Some(n) if !is_kw(&n) => Ok(Pat::Name { name: n, line, span: Span::new(start, c.i) }), + _ => c.fail("a name to bind"), + } +} +/// Each name a pattern binds, with its projection of `base` (a default applies when the field is `undefined`, as in JS). +/// The projections are the parser's own: they have no span (CP-8). An object rest is `omit` of the named keys: Solid's +/// live view in reactive scope, a copy of it when `snap` (a `let`). +fn expand_pattern(pat: &Pat, base: Expr, out: &mut Vec<(String, Expr, usize, Span)>, snap: bool) { + let syn = |k: ExprKind| Expr::new(k, Span::NONE); + let dfl = |v: Expr, d: &Option| match d { + Some(d) => syn(ExprKind::If { + c: Box::new(syn(ExprKind::Bin { op: "==".into(), l: Box::new(v.clone()), r: Box::new(syn(ExprKind::Lit(Lit::Undefined))) })), + t: Box::new(d.clone()), + f: Box::new(v), + }), + None => v, + }; + match pat { + Pat::Name { name, line, span } => out.push((name.clone(), base, *line, *span)), + Pat::Obj(props, rest) => { + for (key, p, d) in props { + let v = syn(ExprKind::Member { o: Box::new(base.clone()), p: key.clone(), opt: false }); + expand_pattern(p, dfl(v, d), out, snap); + } + if let Some(r) = rest { + let o = syn(ExprKind::Omit { e: Box::new(base.clone()), keys: props.iter().map(|(k, _, _)| k.clone()).collect() }); + expand_pattern(r, if snap { syn(ExprKind::Rec(vec![Field::Spread(o)])) } else { o }, out, snap); + } + } + Pat::Arr(items, rest) => { + for (i, it) in items.iter().enumerate() { + if let Some((p, d)) = it { + let v = syn(ExprKind::Index { o: Box::new(base.clone()), i: Box::new(syn(ExprKind::Lit(Lit::Num(i as f64)))), opt: false }); + expand_pattern(p, dfl(v, d), out, snap); + } + } + if let Some(r) = rest { + let f = syn(ExprKind::Member { o: Box::new(base.clone()), p: "slice".into(), opt: false }); + expand_pattern(r, syn(ExprKind::Call { f: Box::new(f), args: vec![syn(ExprKind::Lit(Lit::Num(items.len() as f64)))] }), out, snap); + } + } + } +} +/// A right side that is read again per name without asking twice: a name or a path of names. +fn is_path(e: &Expr) -> bool { + match &e.k { + ExprKind::Id(_) => true, + ExprKind::Member { o, opt: false, .. } => is_path(o), + _ => false, + } +} +/// At a block's `{ … } = e` or `[ … ] = e`: a destructuring declaration, not a read in the view. +fn destructuring_ahead(c: &Cursor) -> bool { + let open = c.peek(); + if open != '{' && open != '[' { + return false; + } + let b = c.src.as_bytes(); + let (mut depth, mut j) = (0i64, c.i); + while j < b.len() { + let ch = b[j]; + if ch == b'{' || ch == b'[' { + depth += 1; + } else if ch == b'}' || ch == b']' { + depth -= 1; + if depth == 0 { + break; + } + } else if ch == b'"' || ch == b'\'' { + let q = ch; + j += 1; + while j < b.len() && b[j] != q && b[j] != b'\n' { + if b[j] == b'\\' { + j += 1; + } + j += 1; + } + } else if ch == b'\n' || ch == b'<' || ch == b'`' { + return false; + } + j += 1; + } + let rest = &c.src[(j + 1).min(c.src.len())..]; + let t = rest.trim_start_matches([' ', '\t']); + t.starts_with('=') && !t.starts_with("==") && !t.starts_with("=>") +} +/// RG-4b: `{ a, b } = e` is one equation per name, each a projection of `e` that keeps updating. +fn parse_destructuring_decl(c: &mut Cursor) -> R> { + let (line, start) = (c.line(), c.i); + let pat = parse_pattern(c)?; + c.ws(true); + c.expect("=")?; + c.ws(true); + // `[n, setN] = createSignal(0)`: JS_API's hint + if JS_APIS.find(c.src[c.i..].split('\n').next().unwrap_or("")).is_some_and(|m| m.start() == 0) { + return c.fail("a hook's pair is not .rx"); + } + let mut base = parse_expr(c)?; + let mut out = vec![]; + if !is_path(&base) { + c.temps += 1; + let t = format!("$${}", c.temps); + out.push(Decl::Eq { name: t.clone(), e: base, line, span: Span::new(start, c.i), deferred: false, key: None, ty: None, exported: false }); + base = Expr::new(ExprKind::Id(t), Span::NONE); + } + let mut names = vec![]; + expand_pattern(&pat, base, &mut names, false); + for (name, e, line, span) in names { + out.push(Decl::Eq { name, e, line, span, deferred: false, key: None, ty: None, exported: false }); + } + Ok(out) +} +/// One declaration, or the several a destructuring declares. +fn parse_decls1(c: &mut Cursor) -> R> { + c.ws(true); + if destructuring_ahead(c) { + return parse_destructuring_decl(c); + } + Ok(vec![parse_decl(c)?]) +} + fn parse_decl(c: &mut Cursor) -> R { c.ws(true); let start = c.i; @@ -821,7 +1040,7 @@ fn parse_decl_at(c: &mut Cursor) -> R { if !looks_like_decl(c) { break; } - decls.push(parse_decl(c)?); + decls.extend(parse_decls1(c)?); c.ws(true); c.eat(";"); } @@ -837,13 +1056,21 @@ fn parse_decl_at(c: &mut Cursor) -> R { c.ws(true); c.expect("(")?; c.ws(true); - let props = if c.peek() == ')' { "props".to_string() } else { c.ident_or("props name")? }; + // RG-4b: `component C({ a, b })` — each name a projection of props + let sig = if c.peek() == '{' { Some(parse_pattern(c)?) } else { None }; + 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); c.expect(")")?; c.ws(true); c.expect("{")?; - let (decls, view) = parse_block_body(c)?; + let (mut decls, view) = parse_block_body(c)?; + if let Some(sig) = &sig { + let mut names = vec![]; + expand_pattern(sig, Expr::new(ExprKind::Id(props.clone()), Span::NONE), &mut names, false); + let pre: Vec = 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 }); } let save = c.i; @@ -884,6 +1111,7 @@ pub fn parse_block(c: &mut Cursor) -> R> { let mut out = vec![]; while !c.starts_with("}") { out.push(parse_stmt(c)?); + out.append(&mut c.pending); c.ws(true); c.eat(";"); c.ws(true); @@ -898,6 +1126,7 @@ pub fn parse_stmts(src: &str) -> R> { c.ws(true); while !c.eof() { out.push(parse_stmt(&mut c)?); + out.append(&mut c.pending); c.ws(true); c.eat(";"); c.ws(true); @@ -949,6 +1178,32 @@ fn parse_stmt(c: &mut Cursor) -> R { } if c.keyword("let") || c.keyword("const") { c.ws(true); + if c.peek() == '{' || c.peek() == '[' { + // 07 §5: a destructuring local — each name a snapshot, as in JS + let pat = parse_pattern(c)?; + c.ws(true); + c.expect("=")?; + c.ws(true); + let mut base = parse_expr(c)?; + let mut out = vec![]; + if !is_path(&base) { + c.temps += 1; + let t = format!("$${}", c.temps); + out.push(st(start, StmtKind::Let { name: t.clone(), e: base, ty: None }, c)); + base = Expr::new(ExprKind::Id(t), Span::NONE); + } + let mut names = vec![]; + expand_pattern(&pat, base, &mut names, true); + for (name, e, _, span) in names { + out.push(Stmt { k: StmtKind::Let { name, e, ty: None }, span }); + } + if out.is_empty() { + return c.fail("a destructuring that binds nothing"); + } + let first = out.remove(0); + c.pending.extend(out); + return Ok(first); + } let name = c.ident_or("let name")?; let ty = opt_type(c, false)?; c.ws(true); @@ -1009,12 +1264,12 @@ fn parse_stmt(c: &mut Cursor) -> R { c.ws(true); c.expect(")")?; c.ws(true); - let t = if c.peek() == '{' { parse_block(c)? } else { vec![parse_stmt(c)?] }; + let t = if c.peek() == '{' { parse_block(c)? } else { let mut v = vec![parse_stmt(c)?]; v.append(&mut c.pending); v }; c.ws(true); let mut f = vec![]; if c.keyword("else") { c.ws(true); - f = if c.peek() == '{' { parse_block(c)? } else { vec![parse_stmt(c)?] }; + f = if c.peek() == '{' { parse_block(c)? } else { let mut v = vec![parse_stmt(c)?]; v.append(&mut c.pending); v }; } return Ok(st(start, StmtKind::Sif { c: cond, t, f }, c)); } @@ -1745,7 +2000,7 @@ fn parse_block_body(c: &mut Cursor) -> R<(Vec, Vec)> { if !looks_like_decl(c) { break; } - decls.push(parse_decl(c)?); + decls.extend(parse_decls1(c)?); c.ws(true); c.eat(";"); } @@ -1754,7 +2009,8 @@ fn parse_block_body(c: &mut Cursor) -> R<(Vec, Vec)> { fn looks_like_decl(c: &mut Cursor) -> bool { let save = c.i; c.ws(true); - let r = c.starts_with("source ") + let r = destructuring_ahead(c) + || c.starts_with("source ") || c.starts_with("effect") || c.starts_with("fn ") || c.starts_with("component ") diff --git a/compiler/src/prelude.ts b/compiler/src/prelude.ts index f7867a87..f31a76ae 100644 --- a/compiler/src/prelude.ts +++ b/compiler/src/prelude.ts @@ -9,6 +9,8 @@ export declare function eq(v: T, ok: Async extends true ? "MISSING_AWAIT: export declare function first(v: T, init: I): T | I; export declare function pre(init?: T): T; export declare function pending(v: unknown): boolean; +/** RG-4b: an object rest, \`{ a, ...rest } = e\` (Solid's \`omit\`). */ +export declare function omit(v: T, ...keys: K[]): Omit; export declare function caught(v: T, alt: (e: any) => R): T | R; export declare function raise(e: unknown): never; /** W-1/W-6: a write checks its value (or its function of the slot) against the target — whose type alone decides it. */ diff --git a/compiler/src/project.rs b/compiler/src/project.rs index 889603c2..537eb0c2 100644 --- a/compiler/src/project.rs +++ b/compiler/src/project.rs @@ -803,6 +803,10 @@ impl Proj<'_> { Code::of("({ ").c(Code::join(fs, ", ")).t(" })") } ExprKind::Spread(x) => Code::of("...").c(go!(x)), + ExprKind::Omit { e: x, keys } => { + let ks: String = keys.iter().map(|k| format!(", {}", js_str(k))).collect(); + Code::of("__.omit(").c(go!(x)).t(&ks).t(")") + } ExprKind::Fn { params, body, stmts } => { let mut from = sp.start as usize; let ps = Code::join(params.iter().map(|p| self.name(p, sp, &mut from)).collect(), ", "); diff --git a/compiler/src/spans.rs b/compiler/src/spans.rs index 6c6db4f4..9eac036b 100644 --- a/compiler/src/spans.rs +++ b/compiler/src/spans.rs @@ -30,6 +30,9 @@ impl Spans<'_> { true } fn word_in(&mut self, at: Span, name: &str, what: &str) { + if name.starts_with("$$") { + return; // a destructuring's temp (RG-4b): the parser's own name for its right side + } let t = at.text(self.src); let found = t.match_indices(name).any(|(i, _)| { let before = t[..i].chars().next_back(); @@ -124,8 +127,9 @@ impl Spans<'_> { fn expr(&mut self, e: &Expr, parent: Span) { let at = e.span; if !self.inside(at, parent, e.kind()) { - // the one expression the parser makes: a statement-bodied arrow's `body` - if !matches!(e.k, ExprKind::Lit(Lit::Undefined)) { + // the expressions the parser makes: a statement-bodied arrow's `body`, and a destructured name's projection + // (RG-4b: `.field`, `[i]`, the default's `=== undefined ? d : v`, the right side's temp or `props`, a rest's `omit`, `.slice(n)` or `{ ...omit }`) + if !matches!(e.k, ExprKind::Lit(Lit::Undefined) | ExprKind::Lit(Lit::Num(_)) | ExprKind::Member { .. } | ExprKind::Index { .. } | ExprKind::If { .. } | ExprKind::Bin { .. } | ExprKind::Id(_) | ExprKind::Omit { .. } | ExprKind::Call { .. } | ExprKind::Rec(_)) { self.bad(parent, format!("a {} expression has no span", e.kind())); } return; @@ -146,7 +150,7 @@ impl Spans<'_> { sub(l); sub(r); } - ExprKind::Un { e, .. } | ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Throw(e) => sub(e), + ExprKind::Un { e, .. } | ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Omit { e, .. } | ExprKind::Throw(e) => sub(e), ExprKind::Pre { init, .. } => init.iter().for_each(|x| sub(x)), ExprKind::Await { e, init, .. } => { sub(e); @@ -349,7 +353,7 @@ pub fn walk(p: &Program, f: &mut dyn FnMut(Span)) { match &e.k { ExprKind::Lit(_) | ExprKind::Id(_) | ExprKind::Use(_) | ExprKind::StyleMap { .. } => {} ExprKind::Bin { l, r, .. } => sub.extend([&**l, &**r]), - ExprKind::Un { e, .. } | ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Throw(e) => sub.push(e), + ExprKind::Un { e, .. } | ExprKind::Peek(e) | ExprKind::Latest(e) | ExprKind::Pending(e) | ExprKind::Spread(e) | ExprKind::Omit { e, .. } | ExprKind::Throw(e) => sub.push(e), ExprKind::Pre { init, .. } => sub.extend(init.iter().map(|x| &**x)), ExprKind::Await { e, init, .. } => sub.extend(std::iter::once(&**e).chain(init.iter().map(|x| &**x))), ExprKind::If { c, t, f } => sub.extend([&**c, &**t, &**f]), diff --git a/guide/language.md b/guide/language.md index 0b0771b1..991dce62 100644 --- a/guide/language.md +++ b/guide/language.md @@ -27,6 +27,7 @@ source user = await fetchUser(id) // state with a question: re-asked wh double = count * 2 // an equation: always equal to its right side, never written label = `${user.name} (${count})` rows key id = items // a keyed collection (rows keep identity by `id`) +{ name, age = 0 } = user // destructuring: one equation per name, each keeps updating fn area(w: number, h: number): number => w * h // a pure function action save(n: string) { … } // writes and awaits (async.md) component Row(props: { item: Item }) { … } @@ -35,7 +36,9 @@ type Item = { id: number; name: string } ``` There is no `let` at the top level or in a component: a value that changes is a `source`, and one computed from -others is an equation. Equations are lazy and exact: an arm of `c ? a : b` that isn't taken isn't evaluated. +others is an equation. Bare `x = e` is always an equation and `let x = e` (in imperative code) always a snapshot, as +in JS; that holds for destructuring too. Mutation is only `:=` and `guess`. A rest, `{ id, ...rest } = props`, is the other fields +(Solid's `omit`) and keeps updating too. Equations are lazy and exact: an arm of `c ? a : b` that isn't taken isn't evaluated. ## Expressions @@ -95,7 +98,7 @@ effect (count) { log($0) } // runs after `count` changes; `$0` is its value (a - A write is `x := v`, `x.field := v`, `xs[i].done := v`, or functional `x := old => …` (applied to the slot as it stands at the write). -- `let`/`const`, `if`, `try`/`catch`/`finally`, `return`, `throw` work as in TypeScript (no `for`/`while` loops: +- `let`/`const` (destructuring too: `let { a, b } = pair` takes snapshots), `if`, `try`/`catch`/`finally`, `return`, `throw` work as in TypeScript (no `for`/`while` loops: use array methods). - Browser and JS globals (`console`, `document`, `window`) are not in scope. `log(…)` prints; anything else from JS comes in through an import from a `.js` module. @@ -117,6 +120,7 @@ component Card(props: { title: string; children: View }) { ``` `props.x` stays live: don't copy it into a source unless you mean a one-time seed (`source draft := peek(props.x)`). +Destructured props stay live too: `component Card({ title, open = false })` or `{ title } = props` (RG-4b). A prop that takes a handler is named `on…` and typed as a function: `props: { onPick: (id: number) => void }`, passed as ` { selected := id }} />` and called from the component's own handlers (`props.onPick(id)`). diff --git a/ref/compile/emit.ts b/ref/compile/emit.ts index 3caf7c3f..ccebfbf7 100644 --- a/ref/compile/emit.ts +++ b/ref/compile/emit.ts @@ -452,6 +452,7 @@ class Gen { case "un": return `(${e.op}${go(e.e)})`; case "draft": throw new CompileError("a draft is an equation's whole expression"); case "spread": throw new CompileError("spread outside an array literal"); + case "omit": return `__rt.omit(${[go(e.e), ...e.keys.map(k => J(k))].join(", ")})`; // RG-4b: Solid's live view } } private ref(name: string, env: Env, ctx: Ctx): string { diff --git a/ref/compile/rt-server.ts b/ref/compile/rt-server.ts index 48f629e7..b3c7bec5 100644 --- a/ref/compile/rt-server.ts +++ b/ref/compile/rt-server.ts @@ -8,7 +8,7 @@ import { Portal as WebPortal } from "@solidjs/web"; import { getObserver } from "solid-js"; import { provideRequestEvent } from "@solidjs/web/storage"; -import { Show, For, Loading, Errored, createComponent, createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, untrack, latest, isPending, deep, NotReadyError, getOwner, runWithOwner, getNextChildId, runInServerComponentScope } from "solid-js"; +import { Show, For, Loading, Errored, createComponent, createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, untrack, latest, isPending, deep, NotReadyError, getOwner, runWithOwner, getNextChildId, runInServerComponentScope, omit } from "solid-js"; import { escape, renderToStream, markSafeError, generateHydrationScript } from "@solidjs/web"; import { $TARGET } from "@solidjs/signals"; @@ -218,7 +218,7 @@ export function createApp(opts: { api?: Record unknown> isA, PANIC, catchMemo: catchAcc, tryCatch, catchRead: (f: () => unknown, match: (err: unknown) => boolean, alt: (err: unknown) => unknown) => catchAcc(f, match, alt)(), throw: (e: unknown): never => { throw e; }, eq: (a: unknown, b: unknown) => a === b, - untrack, latest: (f: () => unknown) => latest(() => latestWhole(f())), isPending: (f: () => unknown) => isPending(() => readWhole(f())), + untrack, omit, latest: (f: () => unknown) => latest(() => latestWhole(f())), isPending: (f: () => unknown) => isPending(() => readWhole(f())), fmt, // ---- views: compiled JSX (compile/jsx.ts, generate "ssr", hydratable) does the rest ---- diff --git a/ref/compile/rt-web.ts b/ref/compile/rt-web.ts index 0b49df82..fb3b009a 100644 --- a/ref/compile/rt-web.ts +++ b/ref/compile/rt-web.ts @@ -7,7 +7,7 @@ import { getObserver } from "solid-js"; import { createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, createErrorBoundary, createContext, Show, For, Loading, Errored, createComponent, - flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, getOwner, runWithOwner, sharedConfig, + flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, getOwner, runWithOwner, sharedConfig, omit, } from "solid-js"; import { getContext, setContext, $TARGET } from "@solidjs/signals"; import { hydrate, render } from "@solidjs/web"; @@ -290,7 +290,7 @@ export function createApp(opts: { root: any; api?: Record unknown, match: (err: unknown) => boolean, alt: (err: unknown) => unknown) => catchAcc(f, match, alt)(), throw: (e: unknown): never => { throw e; }, eq: (a: unknown, b: unknown) => raw(a) === raw(b), - untrack, latest: (f: () => unknown) => latest(() => latestWhole(f())), isPending: (f: () => unknown) => isPending(() => readWhole(f())), + untrack, omit, latest: (f: () => unknown) => latest(() => latestWhole(f())), isPending: (f: () => unknown) => isPending(() => readWhole(f())), fmt, // ---- views: compiled JSX (compile/jsx.ts, generate "dom") does the rest ---- diff --git a/ref/compile/rt.ts b/ref/compile/rt.ts index e97a1136..6e7a4299 100644 --- a/ref/compile/rt.ts +++ b/ref/compile/rt.ts @@ -8,7 +8,7 @@ import { getObserver } from "@solidjs/signals"; import { Show, For, Loading, Errored } from "solid-js"; import { createSignal, createMemo, createEffect, createRenderEffect, createRoot, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, - flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary, + flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary, omit, } from "@solidjs/signals"; // 05 H-9 / Solid D-23: `isPending(fn)` judges the reads fn makes, never its return value, so `pending? x` over a structured @@ -297,7 +297,7 @@ export function createApp(opts: { api?: Record unknown> errorClass, isA, PANIC, catchMemo: catchAcc, tryCatch, catchRead: (f: () => unknown, match: (err: unknown) => boolean, alt: (err: unknown) => unknown) => catchAcc(f, match, alt)(), throw: (e: unknown): never => { throw e; }, eq: (a: unknown, b: unknown) => raw(a) === raw(b), - untrack, latest: (f: () => unknown) => latest(() => latestWhole(f())), isPending: (f: () => unknown) => isPending(() => readWhole(f())), + untrack, omit, latest: (f: () => unknown) => latest(() => latestWhole(f())), isPending: (f: () => unknown) => isPending(() => readWhole(f())), fmt, // ---- views: compiled JSX (compile/jsx.ts) calls the renderer; these are the two helpers it calls on __rt ---- diff --git a/ref/oracle/lower.ts b/ref/oracle/lower.ts index b434412d..e42d47eb 100644 --- a/ref/oracle/lower.ts +++ b/ref/oracle/lower.ts @@ -10,7 +10,7 @@ import { Show, For, Loading, Errored } from "solid-js"; import { getObserver } from "@solidjs/signals"; import { createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, - createRoot, createRenderEffect, flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, resetErrorHalt, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary + createRoot, createRenderEffect, flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, resetErrorHalt, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary, omit } from "@solidjs/signals"; const noFlight = (v: unknown) => { if (v instanceof Flight || v instanceof Promise) throw new Unsupported("`await` not in tail position (the oracle lowers a tail `await` to the memo's result)"); }; @@ -453,6 +453,7 @@ export class Oracle { case "draft": throw new Unsupported("draft outside an equation"); case "use": { const b = this.provision(e.name, scope); return b.k === "const" ? b.value : (b as { get: () => unknown }).get(); } case "spread": throw new Error("spread outside an array literal"); + case "omit": return omit(this.ev(e.e, scope, env) as Record, ...e.keys); } } diff --git a/ref/src/ast.ts b/ref/src/ast.ts index eebf0ee4..37ce9a1a 100644 --- a/ref/src/ast.ts +++ b/ref/src/ast.ts @@ -22,6 +22,7 @@ export type Expr = | { k: "draft"; seed: Expr; body: Stmt[] } // ST-3: the only writes in reactive scope, to `d` | { k: "use"; name: string } // RG-7b: the nearest provision above | { k: "spread"; e: Expr } // only inside an array literal + | { k: "omit"; e: Expr; keys: string[] } // RG-4b: an object rest, `{ a, ...rest } = e` (the parser's own; Solid's `omit`) | { k: "throw"; e: Expr } // R-f-3: a failure; an expression in reactive scope too | { k: "catch"; e: Expr; name?: string; classes?: string[]; alt: Expr } | { k: "stylemap"; css: string; line?: number } // 07 CSS-5: `styles = ` — a class map, a constant diff --git a/ref/src/check.ts b/ref/src/check.ts index c0ff8894..1528b85d 100644 --- a/ref/src/check.ts +++ b/ref/src/check.ts @@ -115,7 +115,7 @@ function freeNames(e: Expr, bound: Set, out: Set): void { case "pre": if (!bound.has(e.name)) out.add(`pre:${e.name}`); return; case "peek": go(e.e); return; case "await": go(e.e); if (e.init) go(e.init); return; - case "latest": case "pending": case "un": case "spread": go(e.e); return; + case "latest": case "pending": case "un": case "spread": case "omit": go(e.e); return; case "bin": go(e.l); go(e.r); return; case "if": go(e.c); go(e.t); go(e.f); return; case "tpl": for (const p of e.parts) if (typeof p !== "string") go(p); return; @@ -721,7 +721,7 @@ class Checker { case "use": { if (ctx.server) this.err("SERVER_CAPTURE", "SF-3", `a server function's body reads \`use ${e.name}\`: a provision is the client's — pass it as an argument`); const c = this.ctxOf(e.name, env); if (c) this.model.read(c, ctx.scope === "imperative"); if (c && !c.ctx!.dflt) this.uses.push({ region: env.region, name: c.ctx!.key, line: this.line, display: e.name }); return; } case "peek": this.model.snap++; go(e.e); this.model.snap--; return; // E-5: a snapshot case "latest": case "pending": { const save = this.model.probe; this.model.probe = e.k; go(e.e); this.model.probe = save; return; } // R-9, R-12: probes - case "un": case "spread": go(e.e); return; + case "un": case "spread": case "omit": go(e.e); return; case "await": if (ctx.scope === "imperative" && !this.stmtAwaits.has(e)) this.err("AWAIT_IN_EXPRESSION", "AC-1", `\`${show(e)}\` inside an expression of imperative scope: a run suspends only at a statement — bind it first, \`let v = ${show(e)}\` (a \`catch (e) => alt\` may follow it)`); this.awaitedCalls.add(e.e); go(e.e); if (e.init) go(e.init); return; diff --git a/ref/src/parse.ts b/ref/src/parse.ts index 946d9182..0ab607bf 100644 --- a/ref/src/parse.ts +++ b/ref/src/parse.ts @@ -37,6 +37,8 @@ export function viewStart(src: string): number { parseProgram(src); return viewA class Cursor { i = 0; + pendingStmts: Stmt[] = []; // a destructuring `let` (07 §5) is several statements: the rest of them, after the first + temps = 0; // `$$n`: the bound value of a destructuring whose right side is not a name (RG-4b) src: string; constructor(src: string) { this.src = src; } get line() { return this.src.slice(0, this.i).split("\n").length; } @@ -116,7 +118,7 @@ export function parseProgram(src: string): Program { const c = new Cursor(src); const decls: Decl[] = []; c.ws(); - while (!c.eof() && looksLikeDecl(c)) { decls.push(parseDecl(c)); c.ws(); c.eat(";"); c.ws(); } + while (!c.eof() && looksLikeDecl(c)) { decls.push(...parseDecls1(c)); c.ws(); c.eat(";"); c.ws(); } viewAt = c.i; const view = parseMarkup(c, null); c.ws(); @@ -157,6 +159,82 @@ export function parseDeclarations(src: string): JsDecl[] { } } +/** RG-4b, 07 §5: a destructuring pattern, `{ a, b: c, d = 1, e: { f }, ...rest }` or `[a, , b, ...rest]`. */ +type Pat = { k: "name"; name: string; line: number } | { k: "obj"; props: { key: string; pat: Pat; dflt?: Expr }[]; rest?: Pat } | { k: "arr"; items: ({ pat: Pat; dflt?: Expr } | null)[]; rest?: Pat }; +function parseRest(c: Cursor, close: string): Pat { + c.i += 3; c.ws(); const line = c.line; const name = c.ident(); if (!name || KEYWORDS.has(name)) c.fail("a name for the rest"); c.ws(); + if (!c.startsWith(close)) c.fail(`a rest element is last, with no default: \`...${name}${close}\``); + return { k: "name", name: name!, line }; +} +function parsePattern(c: Cursor): Pat { + c.ws(); + if (c.eat("{")) { + const props: { key: string; pat: Pat; dflt?: Expr }[] = []; + for (;;) { + c.ws(); if (c.startsWith("}")) break; + if (c.startsWith("...")) { const rest = parseRest(c, "}"); c.expect("}"); return { k: "obj", props, rest }; } + const kline = c.line; const key = c.ident() ?? c.fail("a field name"); c.ws(); + const pat: Pat = c.eat(":") ? parsePattern(c) : { k: "name", name: key, line: kline }; c.ws(); + const dflt = c.peek() === "=" && c.peek(1) !== "=" && c.peek(1) !== ">" ? (c.i++, c.ws(), parseExpr(c)) : undefined; + props.push({ key, pat, dflt }); c.ws(); + if (!c.eat(",")) break; + } + c.ws(); c.expect("}"); return { k: "obj", props }; + } + if (c.eat("[")) { + const items: ({ pat: Pat; dflt?: Expr } | null)[] = []; + for (;;) { + c.ws(); if (c.startsWith("]")) break; + if (c.startsWith(",")) { c.i++; items.push(null); continue; } + if (c.startsWith("...")) { const rest = parseRest(c, "]"); c.expect("]"); return { k: "arr", items, rest }; } + const pat = parsePattern(c); c.ws(); + const dflt = c.peek() === "=" && c.peek(1) !== "=" && c.peek(1) !== ">" ? (c.i++, c.ws(), parseExpr(c)) : undefined; + items.push({ pat, dflt }); c.ws(); + if (!c.eat(",")) break; + } + c.ws(); c.expect("]"); return { k: "arr", items }; + } + const line = c.line; const name = c.ident(); if (!name || KEYWORDS.has(name)) c.fail("a name to bind"); + return { k: "name", name: name!, line }; +} +/** Each name a pattern binds, with its projection of `base` (a default applies when the field is `undefined`, as in JS). + * An object rest is `omit` of the named keys: Solid's live view in reactive scope, a copy of it when `snap` (a `let`). */ +function expandPattern(pat: Pat, base: Expr, out: { name: string; e: Expr; line: number }[], snap = false) { + const dfl = (v: Expr, d?: Expr): Expr => d ? { k: "if", c: { k: "bin", op: "==", l: v, r: { k: "lit", v: undefined } }, t: d, f: v } : v; + if (pat.k === "name") out.push({ name: pat.name, e: base, line: pat.line }); + else if (pat.k === "obj") { + for (const p of pat.props) expandPattern(p.pat, dfl({ k: "member", o: base, p: p.key }, p.dflt), out, snap); + if (pat.rest) { const o: Expr = { k: "omit", e: base, keys: pat.props.map(p => p.key) }; expandPattern(pat.rest, snap ? { k: "rec", fields: [{ spread: o }] } : o, out, snap); } + } else { + pat.items.forEach((it, i) => { if (it) expandPattern(it.pat, dfl({ k: "index", o: base, i: { k: "lit", v: i } }, it.dflt), out, snap); }); + if (pat.rest) expandPattern(pat.rest, { k: "call", f: { k: "member", o: base, p: "slice" }, args: [{ k: "lit", v: pat.items.length }] }, out, snap); + } +} +/** A right side that is read again per name without asking twice: a name or a path of names. */ +const isPath = (e: Expr): boolean => e.k === "id" || (e.k === "member" && !e.opt && isPath(e.o)); +/** At a block's `{ … } = e` or `[ … ] = e`: a destructuring declaration, not a read in the view. */ +function destructuringAhead(c: Cursor): boolean { + const open = c.peek(); if (open !== "{" && open !== "[") return false; + let depth = 0, j = c.i; + for (; j < c.src.length; j++) { const ch = c.src[j]; if (ch === "{" || ch === "[") depth++; else if (ch === "}" || ch === "]") { depth--; if (depth === 0) break; } else if (ch === "\"" || ch === "'") { const q = ch; for (j++; j < c.src.length && c.src[j] !== q && c.src[j] !== "\n"; j++) if (c.src[j] === "\\") j++; } else if (ch === "\n" || ch === "<" || ch === "`") return false; } + const rest = c.src.slice(j + 1); + return /^[ \t]*=(?![=>])/.test(rest); +} +/** RG-4b: `{ a, b } = e` is one equation per name, each a projection of `e` that keeps updating. */ +function parseDestructuringDecl(c: Cursor): Decl[] { + const line = c.line; + const pat = parsePattern(c); c.ws(); c.expect("="); c.ws(); + if (JS_APIS.exec(c.src.slice(c.i).split("\n", 1)[0])?.index === 0) c.fail("a hook's pair is not .rx"); // `[n, setN] = createSignal(0)`: JS_API's hint + let base = parseExpr(c); + const out: Decl[] = []; + if (!isPath(base)) { const t = `$$${++c.temps}`; out.push({ k: "eq", name: t, e: base, line }); base = { k: "id", name: t }; } + const names: { name: string; e: Expr; line: number }[] = []; expandPattern(pat, base, names); + for (const n of names) out.push({ k: "eq", name: n.name, e: n.e, line: n.line }); + return out; +} +/** One declaration, or the several a destructuring declares. */ +function parseDecls1(c: Cursor): Decl[] { c.ws(); return destructuringAhead(c) ? parseDestructuringDecl(c) : [parseDecl(c)]; } + function parseDecl(c: Cursor): Decl { c.ws(); const line = c.line; @@ -244,14 +322,16 @@ function parseDecl(c: Cursor): Decl { if (c.eat("=>")) { c.ws(); return { k: "fn", name, params, decls: [], tail: parseExpr(c), line, ret }; } c.expect("{"); const decls: Decl[] = []; - for (;;) { c.ws(); if (!looksLikeDecl(c)) break; decls.push(parseDecl(c)); c.ws(); c.eat(";"); } + for (;;) { c.ws(); if (!looksLikeDecl(c)) break; decls.push(...parseDecls1(c)); c.ws(); c.eat(";"); } c.ws(); const tail = parseExpr(c); c.ws(); c.expect("}"); return { k: "fn", name, params, decls, tail, line, ret }; } if (c.keyword("component")) { c.ws(); const name = c.ident() ?? c.fail("component name"); c.ws(); c.expect("("); c.ws(); - const props = c.peek() === ")" ? "props" : (c.ident() ?? c.fail("props name")); const propsType = optType(c); c.ws(); c.expect(")"); c.ws(); c.expect("{"); // `component C()`: no props (found by S-ai2-tracker) + const sig = c.peek() === "{" ? parsePattern(c) : null; // RG-4b: `component C({ a, b })` — each name a projection of props + const props = sig || c.peek() === ")" ? "props" : (c.ident() ?? c.fail("props name")); const propsType = optType(c); c.ws(); c.expect(")"); c.ws(); c.expect("{"); // `component C()`: no props (found by S-ai2-tracker) const { decls, body } = parseBlockBody(c); + if (sig) { const names: { name: string; e: Expr; line: number }[] = []; expandPattern(sig, { k: "id", name: props }, names); decls.unshift(...names.map(n => ({ k: "eq" as const, name: n.name, e: n.e, line: n.line }))); } return { k: "component", name, props, decls, view: body, line, propsType }; } const save = c.i; @@ -273,7 +353,7 @@ function parseDecl(c: Cursor): Decl { export function parseBlock(c: Cursor): Stmt[] { c.ws(); c.expect("{"); c.ws(); const out: Stmt[] = []; - while (!c.startsWith("}")) { out.push(parseStmt(c)); c.ws(); c.eat(";"); c.ws(); } + while (!c.startsWith("}")) { out.push(parseStmt(c), ...c.pendingStmts.splice(0)); c.ws(); c.eat(";"); c.ws(); } c.expect("}"); return out; } @@ -282,7 +362,7 @@ export function parseStmts(src: string): Stmt[] { const c = new Cursor(src); const out: Stmt[] = []; c.ws(); - while (!c.eof()) { out.push(parseStmt(c)); c.ws(); c.eat(";"); c.ws(); } + while (!c.eof()) { out.push(parseStmt(c), ...c.pendingStmts.splice(0)); c.ws(); c.eat(";"); c.ws(); } return out; } @@ -296,7 +376,17 @@ function parseStmt(c: Cursor): Stmt { if (c.keyword("refresh")) { c.ws(); return { k: "refresh", name: c.ident() ?? c.fail("name") }; } if (c.keyword("affects")) { c.ws(); return { k: "affects", name: c.ident() ?? c.fail("name") }; } if (c.keyword("action")) { c.ws(); return { k: "action", body: parseBlock(c) }; } - if (c.keyword("let") || c.keyword("const")) { c.ws(); const name = c.ident() ?? c.fail("let name"); const type = optType(c); c.ws(); c.expect("="); c.ws(); return { k: "let", name, e: parseExpr(c), type }; } + if (c.keyword("let") || c.keyword("const")) { c.ws(); + if (c.peek() === "{" || c.peek() === "[") { // 07 §5: a destructuring local — each name a snapshot, as in JS + const pat = parsePattern(c); c.ws(); c.expect("="); c.ws(); + let base = parseExpr(c); const out: Stmt[] = []; + if (!isPath(base)) { const t = `$$${++c.temps}`; out.push({ k: "let", name: t, e: base }); base = { k: "id", name: t }; } + const names: { name: string; e: Expr; line: number }[] = []; expandPattern(pat, base, names, true); + for (const n of names) out.push({ k: "let", name: n.name, e: n.e }); + if (!out.length) c.fail("a destructuring that binds nothing"); + c.pendingStmts.push(...out.slice(1)); return out[0]; + } + const name = c.ident() ?? c.fail("let name"); const type = optType(c); c.ws(); c.expect("="); c.ws(); return { k: "let", name, e: parseExpr(c), type }; } if (c.keyword("try")) { // AC-7 c.ws(); const body = parseBlock(c); c.ws(); let catchName: string | undefined, handler: Stmt[] | undefined, fin: Stmt[] | undefined; @@ -310,9 +400,9 @@ function parseStmt(c: Cursor): Stmt { if (c.keyword("return")) { c.ws(false); if (c.peek() === "}" || c.peek() === ";" || c.peek() === "\n" || c.eof()) return { k: "return" }; return { k: "return", e: parseExpr(c) }; } if (c.keyword("if")) { // imperative `if (c) { … } else { … }` c.ws(); c.expect("("); c.ws(); const cond = parseExpr(c); c.ws(); c.expect(")"); c.ws(); - const t = c.peek() === "{" ? parseBlock(c) : [parseStmt(c)]; c.ws(); + const t = c.peek() === "{" ? parseBlock(c) : [parseStmt(c), ...c.pendingStmts.splice(0)]; c.ws(); let f: Stmt[] = []; - if (c.keyword("else")) { c.ws(); f = c.peek() === "{" ? parseBlock(c) : [parseStmt(c)]; } + if (c.keyword("else")) { c.ws(); f = c.peek() === "{" ? parseBlock(c) : [parseStmt(c), ...c.pendingStmts.splice(0)]; } return { k: "sif", c: cond, t, f }; } // write? target := expr @@ -579,13 +669,13 @@ function parseBlockBody(c: Cursor): { decls: Decl[]; body: Markup[] } { for (;;) { c.ws(); if (!looksLikeDecl(c)) break; - decls.push(parseDecl(c)); c.ws(); c.eat(";"); + decls.push(...parseDecls1(c)); c.ws(); c.eat(";"); } return { decls, body: parseMarkup(c, "}") }; } function looksLikeDecl(c: Cursor) { const save = c.i; c.ws(); - const r = c.startsWith("source ") || c.startsWith("effect") || c.startsWith("fn ") || c.startsWith("component ") || c.startsWith("deferred ") || c.startsWith("action ") || c.startsWith("provide ") || c.startsWith("context ") || c.startsWith("import ") || c.startsWith("export ") || c.startsWith("type ") || c.startsWith("interface ") || /^error\s+[A-Z]/.test(c.src.slice(c.i)) || /^server\s+(fn|action)\b/.test(c.src.slice(c.i)) || + const r = destructuringAhead(c) || c.startsWith("source ") || c.startsWith("effect") || c.startsWith("fn ") || c.startsWith("component ") || c.startsWith("deferred ") || c.startsWith("action ") || c.startsWith("provide ") || c.startsWith("context ") || c.startsWith("import ") || c.startsWith("export ") || c.startsWith("type ") || c.startsWith("interface ") || /^error\s+[A-Z]/.test(c.src.slice(c.i)) || /^server\s+(fn|action)\b/.test(c.src.slice(c.i)) || (() => { const id = c.ident(); c.ws(false); if (!id || KEYWORDS.has(id)) return false; if (c.peek() === ":" && c.peek(1) !== "=") return true; const ok = (c.peek() === "=" && c.peek(1) !== "=") || /^key\s+[A-Za-z_$][\w$]*\s*=[^=>]/.test(c.src.slice(c.i)); return ok; })(); c.i = save; return r; } diff --git a/ref/src/runtime.ts b/ref/src/runtime.ts index 42dc8218..9f5a474b 100644 --- a/ref/src/runtime.ts +++ b/ref/src/runtime.ts @@ -883,6 +883,7 @@ export class Runtime { } case "arr": { const items: unknown[] = []; let bad: unknown; for (const i of e.items) { if (i.k === "spread") { const v = this.ev(i.e, region, env); if (isAbsent(v)) bad ??= v; else items.push(...(v as unknown[])); } else { const v = this.ev(i, region, env); if (isAbsent(v)) bad ??= v; items.push(v); } } return bad ?? items; } case "spread": throw new RuleError("SYNTAX", "spread outside an array literal"); + case "omit": { const v = this.ev(e.e, region, env); if (isAbsent(v)) return v; const o: Record = {}; if (isObj(v)) for (const k of Object.keys(v)) if (!e.keys.includes(k)) { this.track(v, k); o[k] = v[k]; } return o; } // RG-4b: an object rest case "rec": { const o: Record = {}; let bad: unknown; for (const f of e.fields) { if ("spread" in f) { const v = this.ev(f.spread, region, env); if (isAbsent(v)) bad ??= v; else if (isObj(v)) { for (const k of Object.keys(v)) { this.track(v, k); o[k] = v[k]; } } continue; } const v = this.ev(f.v, region, env); if (isAbsent(v)) bad ??= v; o[f.key] = v; } return bad ?? o; } case "draft": { // ST-3: a copy of the seed, written only along `d`, is the value const seed = this.ev(e.seed, region, env); if (isAbsent(seed)) return seed; @@ -1251,7 +1252,7 @@ function freeVars(e: Expr, out: Set) { case "rec": e.fields.forEach(f => freeVars("spread" in f ? f.spread : f.v, out)); break; case "tpl": e.parts.forEach(p => { if (typeof p !== "string") freeVars(p, out); }); break; case "fn": { const o = new Set(); freeVars(e.body, o); for (const st of e.stmts ?? []) if ("e" in st) freeVars(st.e, o); for (const n of o) if (!e.params.includes(n)) out.add(n); break; } - case "spread": freeVars(e.e, out); break; + case "spread": case "omit": freeVars(e.e, out); break; case "use": break; case "draft": { freeVars(e.seed, out); for (const st of e.body) { if (st.k === "write" || st.k === "let" || st.k === "expr" || st.k === "optimistic") freeVars(st.e, out); if (st.k === "write") for (const p of st.target.path) if (typeof p !== "string") freeVars(p, out); } out.delete("d"); break; } } diff --git a/ref/types/project.ts b/ref/types/project.ts index 57cce21e..927756b1 100644 --- a/ref/types/project.ts +++ b/ref/types/project.ts @@ -32,6 +32,8 @@ export declare function eq(v: T, ok: Async extends true ? "MISSING_AWAIT: export declare function first(v: T, init: I): T | I; export declare function pre(init?: T): T; export declare function pending(v: unknown): boolean; +/** RG-4b: an object rest, \`{ a, ...rest } = e\` (Solid's \`omit\`). */ +export declare function omit(v: T, ...keys: K[]): Omit; export declare function caught(v: T, alt: (e: any) => R): T | R; export declare function raise(e: unknown): never; /** W-1/W-6: a write checks its value (or its function of the slot) against the target — whose type alone decides it. */ @@ -259,6 +261,7 @@ class Proj { case "arr": return `[${e.items.map(go).join(", ")}]`; case "rec": return `({ ${e.fields.map(f => "spread" in f ? `...${go(f.spread)}` : `${J(f.key)}: ${go(f.v)}`).join(", ")} })`; case "spread": return `...${go(e.e)}`; + case "omit": return `__.omit(${[go(e.e), ...e.keys.map(k => J(k))].join(", ")})`; case "fn": { const ps = e.params.join(", "); if (!e.stmts) return `((${ps}) => ${this.expr(e.body, imp, self)})`; diff --git a/scenarios/S-destructuring.md b/scenarios/S-destructuring.md new file mode 100644 index 00000000..3c32f516 --- /dev/null +++ b/scenarios/S-destructuring.md @@ -0,0 +1,81 @@ +# S-destructuring — a destructured name keeps updating; a destructured local is a snapshot + +Rules: RG-4b, E-1, 07 §5, W-4. + +## Program + +```rx +component Card({ title, size = "m", owner: { name }, ...rest }) { + { a, b: bee } = rest.pair + [first, , third = "z", ...more] = rest.list +

{title} {size} {name} {a} {bee} {first} {third} {more.length}

+} + +export default component App() { + source pair := { a: 1, b: 2 } + source list := ["x", "y"] + { a, ...others } = pair + [head, ...tail] = list.map(s => s.toUpperCase()) + + +

{a} {others.b} {head} {tail.length}

+} +``` + +Each name a reactive pattern binds is an equation, a projection of the +right side that keeps updating (RG-4b): `{ a } = pair` is `a = pair.a`, +and `component Card({ title })` is `title = props.title`. A default +applies when the field is `undefined`, as in JS. A right side that is not a +name (`list.map(…)`) is computed once and read per name. An object rest is +the right side without the named fields, as Solid's `omit` (a live view: +`rest.pair` is `props.pair`); an array rest is the items after the named +ones. In a handler, `let { … } = e` is JavaScript's: each name a snapshot, +and a rest a copy (07 §5). + +## Frames + +| instant | action | frame | +| ------- | --------------- | ---------------------------- | +| 0 | mount | `T m Ann 1 2 x z 01 2 X 1` | +| 1 | handler `let { a: q, ...r } = pair; let [x = 0] = [q + r.b]; pair := { a: x, b: q }` | `T m Ann 3 1 x z 03 1 X 1` | +| 2 | handler `list := ["p", "q", "r", "s"]` | `T m Ann 3 1 p r 13 1 P 3` | + +Instant 1: the handler's locals are the snapshot `q = 1, r = { b: 2 }`, so +the write is `{ a: 3, b: 1 }`; every projection of `pair` updates, through +the rests too. Instant 2: the list's third item now exists, so the default +gives way, and each rest takes the items after its pattern's. + +## Rejected variants + +```rx +-- reject: PARSE +export default component App() { + source pair := { a: 1, b: 2 } + { ...rest, a } = pair +

{a}

+} +``` + +A rest element is last, as in JS. + +```rx +-- reject: WRITE_TO_NON_SOURCE +export default component App() { + source pair := { a: 1, b: 2 } + { a } = pair + +} +``` + +A destructured name is an equation: write the source it projects. + +```rx +-- reject: WRITE_TO_NON_SOURCE +export default component App() { + source pair := { a: 1, b: 2 } + { a, ...rest } = pair + +} +``` + +A rest is an equation too: `pair.b := 5` writes the field it shows. diff --git a/spec/07-surface.md b/spec/07-surface.md index ded33402..185007f4 100644 --- a/spec/07-surface.md +++ b/spec/07-surface.md @@ -433,11 +433,15 @@ Decl' ::= "source" Name Type? ("key" Name)? (":=" | "=") Expr | "context" Name (":" "source"? Type)? ("=" Expr)? -- RG-7a; `source`: SOURCE_CONTEXT (RG-7c withdrawn) | "provide" Name "=" Expr -- Name: a context | "deferred"? Name Type? ("key" Name)? "=" Expr + | Pattern "=" Expr -- RG-4b: one equation per name, a projection of Expr | "effect" "(" (Expr ("," Expr)*)? ")" Block -- no inputs: EF-4 | "fn" Name "(" Params ")" Type? ("=>" Expr | "{" Decl* Expr "}") | "action" Name "(" Params ")" Block - | "component" Name "(" Name Type? ")" "{" RBlock "}" + | "component" Name "(" (Name | ObjPattern) Type? ")" "{" RBlock "}" -- a pattern projects props (RG-4b) | "error" Name ("(" Params ")")? ("=>" Expr)? -- R-f-1 +Pattern ::= ObjPattern | "[" (Bind? ",")* (Bind | "..." Name)? "]" -- an array rest: the items after +ObjPattern ::= "{" (Name (":" Bind | ("=" Expr)?)) ,* ("..." Name)? "}" -- an object rest: Solid's `omit` of the named keys +Bind ::= (Name | Pattern) ("=" Expr)? -- a default applies when the field is `undefined` RBlock ::= Decl* Markup* Markup ::= JSXElement | "{" Expr "}" | Control Control ::= "if" Expr "{" RBlock "}" ("else" "{" RBlock "}")? @@ -447,7 +451,7 @@ Control ::= "if" Expr "{" RBlock "}" ("else" "{" RBlock "}")? Block ::= "{" Stmt* "}" 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 + | ("let" | "const") (Name Type? | Pattern) "=" Expr -- a pattern: one snapshot local per name, as in JS | "if" "(" Expr ")" (Block | Stmt) ("else" (Block | Stmt))? | "try" Block ("catch" ("(" Name (":" Classes)? ")")? Block)? ("finally" Block)? | "throw" Expr | "return" Expr? | Expr @@ -542,3 +546,14 @@ Expr ::= TSExpr with §3's removals and additions truth, and a plain `x := v` on an equation stays `WRITE_TO_NON_SOURCE`. `source x = e` is for state that is also written as truth. A prop is guessed through the child's own equation of it, never `props.x` (O-4). +20. **Destructuring keeps each side's meaning** (ruled 2026-11-11): + `{ a, b } = e` in a component or module is one equation per name, each a + projection of `e` that keeps updating (RG-4b), and `component C({ a, b })` + is the same over `props`; `let { a, b } = e` in imperative code is one + snapshot local per name, as in JS. So bare `x = e` is always an equation + and `let x = e` always a snapshot, and only `:=` and `guess` mutate: a + destructured name is an equation, so `a := v` is `WRITE_TO_NON_SOURCE`. + Defaults apply on `undefined`, as in JS. A rest is last, as in JS: an + object rest `{ a, ...rest } = e` is Solid's `omit(e, "a")`, a live view + of the other fields (`rest.b` stays `e.b`), and an array rest + `[x, ...rest] = e` is `e.slice(1)`; in a `let`, each is a copy.