Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.3.1

**New**

- Spread attributes in markup, `<button {...rest}>` and `<Inner {...rest} />`, lowered to Solid's spread and `merge`:
later attributes override, and each field keeps updating (07 §4, S-surface-5).

## 0.3.0

`@reactivescript/compiler`, `@reactivescript/runtime`, `@reactivescript/language-server` and the editor extension,
Expand Down
2 changes: 1 addition & 1 deletion compiler/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion compiler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "reactivescript"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
description = "ReactiveScript's compiler (spec/11): parse, check, resolve, and lower through Solid's solidjs-compiler"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-darwin-arm64",
"version": "0.3.0",
"version": "0.3.1",
"cpu": [
"arm64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-darwin-x64",
"version": "0.3.0",
"version": "0.3.1",
"cpu": [
"x64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-linux-arm64-gnu",
"version": "0.3.0",
"version": "0.3.1",
"cpu": [
"arm64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-linux-x64-gnu",
"version": "0.3.0",
"version": "0.3.1",
"cpu": [
"x64"
],
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/wasm32-wasi/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-wasm32-wasi",
"version": "0.3.0",
"version": "0.3.1",
"main": "compiler.wasi.cjs",
"browser": "compiler.wasi-browser.js",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion compiler/npm/win32-x64-msvc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactivescript/compiler-win32-x64-msvc",
"version": "0.3.0",
"version": "0.3.1",
"cpu": [
"x64"
],
Expand Down
4 changes: 2 additions & 2 deletions compiler/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion compiler/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@reactivescript/compiler",
"description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin",
"version": "0.3.0",
"version": "0.3.1",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
39 changes: 27 additions & 12 deletions compiler/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,16 @@ fn snapshot_reads<'a>(n: Node<'a>, caught: bool, locals: &mut HashSet<String>, o
}
}
/// AC-3a: the submit buttons of a form's markup (a `<button>` without `type`, or `type="submit"`), not through components.
/// 07 §4: a spread attribute (`{...e}`) passes every field: "*"
fn field_of(name: &str) -> String {
if name == "..." { "*".into() } else { name.to_string() }
}

/// an argument as a message shows it: `x={…}`, or `{...…}` for a spread
fn arg_show(field: &str) -> String {
if field == "*" { "{...…}".into() } else { format!("{field}={{…}}") }
}

fn submit_buttons<'a>(ms: &'a [Markup], out: &mut Vec<&'a [Attr]>) {
for m in ms {
match &m.kind {
Expand Down Expand Up @@ -2713,14 +2723,15 @@ impl<'a> Checker<'a> {
fn retained(&mut self) {
let targets = |this: &Self, c: RId, field: &str| -> Vec<String> {
if let Some(sig) = &this.regions[c].sig {
let mut v: Vec<String> = sig.stored.get(field).cloned().unwrap_or_default();
// a spread ("*") goes wherever any field goes
let mut v: Vec<String> = if field == "*" { sig.stored.values().flatten().cloned().collect() } else { sig.stored.get(field).cloned().unwrap_or_default() };
if field != "*" {
v.extend(sig.stored.get("*").cloned().unwrap_or_default());
}
return v.into_iter().map(|t| if t == "module" { "d:0".to_string() } else { format!("p:{}", t) }).collect();
}
let Some(m) = this.retains.get(&c) else { return vec![] };
let mut v: Vec<String> = m.get(field).map(|x| x.iter().cloned().collect()).unwrap_or_default();
let mut v: Vec<String> = if field == "*" { m.values().flatten().cloned().collect() } else { m.get(field).map(|x| x.iter().cloned().collect()).unwrap_or_default() };
if field != "*" {
v.extend(m.get("*").map(|x| x.iter().cloned().collect::<Vec<_>>()).unwrap_or_default());
}
Expand Down Expand Up @@ -2750,8 +2761,8 @@ impl<'a> Checker<'a> {
"REGION_ESCAPE",
"O-3",
format!(
"<{} {}={{…}}>: the closure passed captures {} (declared in a region that ends sooner), and {} stores {} (itself, or through a component it passes it to) where it outlives it",
atag, field, ln, atag, field
"<{} {}>: the closure passed captures {} (declared in a region that ends sooner), and {} stores {} (itself, or through a component it passes it to) where it outlives it",
atag, arg_show(&field), ln, atag, if field == "*" { "a field of it" } else { field.as_str() }
),
);
}
Expand Down Expand Up @@ -2829,7 +2840,7 @@ impl<'a> Checker<'a> {
}
if is_comp {
let cr = self.syms[s.unwrap()].comp_region.unwrap();
self.apps.push(AppRec { comp: cr, site: region, line: self.line, tag: tag.clone(), args: attrs.iter().filter(|a| !is_handler(&a.name)).map(|a| (a.name.clone(), &a.e, env)).collect(), passed: attrs.iter().map(|a| (a.name.clone(), &a.e, env)).collect() });
self.apps.push(AppRec { comp: cr, site: region, line: self.line, tag: tag.clone(), args: attrs.iter().filter(|a| !is_handler(&a.name)).map(|a| (field_of(&a.name), &a.e, env)).collect(), passed: attrs.iter().map(|a| (field_of(&a.name), &a.e, env)).collect() });
let kids = self.region(Some(region), "children", false, Some(tag.clone()));
self.regions[kids].kids = Some((cr, region));
let kenv = self.env(Some(env), kids);
Expand Down Expand Up @@ -4113,10 +4124,14 @@ impl<'a> Checker<'a> {
/// Where a component's prop field goes once read — up to its root, or out of an action.
fn pf(&mut self, g: RId, field: &str) -> Vec<PFEntry> {
if let Some(sig) = &self.regions[g].sig {
let Some(s) = sig.props.get(field).or_else(|| sig.props.get("*")) else { return vec![] };
let mut out = vec![PFEntry { chain: vec![Step { f: Some(s.f.clone()), p: Some(s.p.clone()) }], term: None }];
if let Some((af, an)) = &s.action {
out.push(PFEntry { chain: vec![Step { f: Some(af.clone()), p: None }], term: Some((-1, an.clone())) });
// a spread ("*") goes wherever any field goes
let ss: Vec<_> = if field == "*" { sig.props.values().collect() } else { sig.props.get(field).or_else(|| sig.props.get("*")).into_iter().collect() };
let mut out = vec![];
for s in ss {
out.push(PFEntry { chain: vec![Step { f: Some(s.f.clone()), p: Some(s.p.clone()) }], term: None });
if let Some((af, an)) = &s.action {
out.push(PFEntry { chain: vec![Step { f: Some(af.clone()), p: None }], term: Some((-1, an.clone())) });
}
}
return out;
}
Expand All @@ -4131,7 +4146,7 @@ impl<'a> Checker<'a> {
let mut out = vec![];
let occ = self.occ.clone();
for o in &occ {
if o.comp == g && (o.field == field || o.field == "*") {
if o.comp == g && (o.field == field || o.field == "*" || field == "*") {
for v in self.via(&o.via) {
let mut c = o.pre.clone();
c.extend(v.chain);
Expand Down Expand Up @@ -4208,7 +4223,7 @@ impl<'a> Checker<'a> {
esc_p(&ch, HOLD)
})
{
push_r(&mut res, &mut seen, Req { name: HOLD.into(), line: aline, what: format!("<{} {}={{…}}>, read in {},", atag, field, atag) });
push_r(&mut res, &mut seen, Req { name: HOLD.into(), line: aline, what: format!("<{} {}>, read in {},", atag, arg_show(&field), atag) });
break;
}
}
Expand Down Expand Up @@ -4255,7 +4270,7 @@ impl<'a> Checker<'a> {
ch.extend(q.iter().cloned());
esc_f(&ch, cl)
}) {
push_f(&mut res, &mut seen, Item { cls: cl.clone(), line: aline, what: format!("<{} {}={{…}}>", atag, field) });
push_f(&mut res, &mut seen, Item { cls: cl.clone(), line: aline, what: format!("<{} {}>", atag, arg_show(&field)) });
break;
}
}
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ impl<'p> Gen<'p> {
let kind = if tag.as_bytes()[0].is_ascii_lowercase() { ElementKind::Native(tag.clone()) } else { ElementKind::Component };
let attributes = attrs
.iter()
.filter(|a| !is_handler(&a.name))
.filter(|a| !is_handler(&a.name) && a.name != "...")
.map(|a| Attribute {
name: a.name.clone(),
value: match &a.e.k {
Expand All @@ -541,7 +541,7 @@ impl<'p> Gen<'p> {
})
.collect();
let kids = flatten(tree(g, children));
out.push(Item::El(Element { id: g.style_ids, kind, attributes, has_spread: false, children: kids }));
out.push(Item::El(Element { id: g.style_ids, kind, attributes, has_spread: attrs.iter().any(|a| a.name == "..."), children: kids }));
}
MarkupKind::Mif { t, f, .. } => {
out.push(Item::Container(tree(g, t)));
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/oxc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ impl<'a> Build<'a> {
let Jsx::El { name, attrs, children, self_closing, .. } = j else { unreachable!() };
let attrs = self.vec(attrs.iter().map(|(n, v)| {
let start = self.open();
// 07 §4: `{...e}`, Solid's spread
if let ("...", AttrValue::Expr(e)) = (n.as_str(), v) {
return JSXAttributeItem::new_spread_attribute(self.close(start), self.expr(e), b);
}
let name = match n.split_once(':') {
Some((ns, local)) => JSXAttributeName::new_namespaced_name(SPAN, JSXIdentifier::new(SPAN, self.s(ns), b), JSXIdentifier::new(SPAN, self.s(local), b), b),
None => JSXAttributeName::new_identifier(SPAN, self.s(n), b),
Expand Down
16 changes: 16 additions & 0 deletions compiler/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2189,6 +2189,22 @@ fn parse_element(c: &mut Cursor) -> R<Markup> {
break;
}
let a0 = c.i;
// 07 §4: a spread attribute, `{...e}`: every field of e, in order with the named ones
if c.starts_with("{") {
c.eat("{");
c.ws(true);
let d0 = c.i;
if !c.eat("...") {
return c.fail("attr: an attribute in braces is a spread, `{...props}`");
}
let span = Span::new(d0, c.i);
c.ws(true);
let e = parse_expr(c)?;
attrs.push(Attr { name: "...".into(), e, span });
c.ws(true);
c.expect("}")?;
continue;
}
let Some(name) = c.tag_name() else { return c.fail("attr") };
let span = Span::new(a0, c.i);
c.ws(false);
Expand Down
5 changes: 5 additions & 0 deletions compiler/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,11 @@ impl Proj<'_> {
let tag_at = (!sp.is_none() && sp.text(self.src)[1..].starts_with(tag.as_str())).then_some(sp.start as usize + 1);
let mut a = Code::new();
for at in attrs {
// 07 §4: a spread, as JSX's
if at.name == "..." {
a = a.t(" {...").c(self.expr(&at.e, false, None)).t("}");
continue;
}
let name_at = (!at.span.is_none() && at.span.text(self.src) == at.name).then_some(at.span.start as usize);
a = a.t(" ").at(&at.name, name_at);
if !matches!(at.e.k, ExprKind::Lit(Lit::Bool(true))) {
Expand Down
3 changes: 2 additions & 1 deletion guide/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ 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).
Destructured props stay live too: `component Card({ title, open = false })` or `{ title } = props` (RG-4b). Pass
the rest on with a spread, `<button {...rest}>` or `<Inner {...rest} />`: later attributes override it, as in JSX.

A prop that takes a handler is named `on…` and typed as a function: `props: { onPick: (id: number) => void }`, passed
as `<Row onPick={id => { selected := id }} />` and called from the component's own handlers (`props.onPick(id)`).
4 changes: 2 additions & 2 deletions ref/compile/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ class Gen {
case "read": return "{0}";
case "el": {
if (m.tag === "") return `<>${tree(m.children)}</>`;
const attrs = m.attrs.filter(a => !isHandler(a.name)).map(a => a.e.k === "lit" && typeof a.e.v === "string" ? ` ${a.name}="${esc(a.e.v)}"` : a.e.k === "lit" && a.e.v === true ? ` ${a.name}` : ` ${a.name}={0}`).join("");
const attrs = m.attrs.filter(a => !isHandler(a.name) && a.name !== "...").map(a => a.e.k === "lit" && typeof a.e.v === "string" ? ` ${a.name}="${esc(a.e.v)}"` : a.e.k === "lit" && a.e.v === true ? ` ${a.name}` : ` ${a.name}={0}`).join("") + (m.attrs.some(a => a.name === "...") ? " {...0}" : "");
return VOID_TAGS.has(m.tag) ? `<${m.tag}${attrs} />` : `<${m.tag}${attrs}>${tree(m.children)}</${m.tag}>`;
}
case "mif": return `{(<>${tree(m.t)}</>)}{(<>${tree(m.f)}</>)}`;
Expand Down Expand Up @@ -555,7 +555,7 @@ class Gen {
return `(${param}) => {\n${pre}${this.block(decls, view, inner, ind + 1)}\n${pad}}`;
}
private region(decls: Decl[], view: Markup[], env: Env, ind: number): string { return `{(${this.regionFn(decls, view, env, ind)})()}`; }
private attr(name: string, e: Expr, env: Env): string { return `${name}={${this.expr(e, env, { imp: isHandler(name) })}}`; }
private attr(name: string, e: Expr, env: Env): string { return name === "..." ? `{...${this.expr(e, env, { imp: false })}}` : `${name}={${this.expr(e, env, { imp: isHandler(name) })}}`; } // 07 §4: a spread is Solid's
private child(m: Markup, env: Env, ind: number): string {
const re: Ctx = { imp: false };
const pad = " ".repeat((ind + 1) * 2);
Expand Down
15 changes: 10 additions & 5 deletions ref/oracle/lower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, omit
createRoot, createRenderEffect, flush, untrack, latest, isPending, action, refresh, affects, until, onSettled, onFlush, NotReadyError, resetErrorHalt, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary, omit, merge
} 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)"); };
Expand Down Expand Up @@ -620,13 +620,18 @@ export class Oracle {
if (/^[A-Z]/.test(m.tag)) {
const b = scope.lookup(m.tag); if (!b || b.k !== "comp") throw new Error(`unknown component ${m.tag}`);
const comp = b.decl, home = b.home;
const props: Record<string, unknown> = {};
for (const a of m.attrs) Object.defineProperty(props, a.name, { enumerable: true, get: () => this.ev(a.e, scope) });
Object.defineProperty(props, "children", { enumerable: true, get: () => this.fragment(m.children, scope) });
const parts: unknown[] = []; let run: Record<string, unknown> | null = null; // 07 §4: a spread is a function source of Solid's merge, between runs of named props
for (const a of m.attrs) {
if (a.name === "...") { run = null; parts.push(() => this.ev(a.e, scope)); continue; }
if (!run) { run = {}; parts.push(run); }
Object.defineProperty(run, a.name, { enumerable: true, get: () => this.ev(a.e, scope) });
}
const kids = {}; Object.defineProperty(kids, "children", { enumerable: true, get: () => this.fragment(m.children, scope) }); parts.push(kids);
const props = parts.length === 1 ? kids : (merge as (...xs: unknown[]) => Record<string, unknown>)(...parts);
return R.createComponent((p: any) => { const s2 = new Scope(home); s2.vars.set(comp.props, { k: "const", value: p }); return this.region(comp.decls, comp.view, s2); }, props);
}
const el = R.createElement(m.tag);
for (const a of m.attrs) { if (/^on[A-Z:]/.test(a.name)) { const h = untrack(() => { this.imperative++; try { return this.ev(a.e, scope); } finally { this.imperative--; } }); R.setProp(el, a.name, h); } else R.effect(() => this.ev(a.e, scope), v => { R.setProp(el, a.name, v); }); } // 07 §5: a handler is an imperative closure
for (const a of m.attrs) { if (a.name === "...") { R.effect(() => this.ev(a.e, scope), (v: any) => { for (const k of v && typeof v === "object" ? Object.keys(v) : []) R.setProp(el, k, v[k]); }); continue; } if (/^on[A-Z:]/.test(a.name)) { const h = untrack(() => { this.imperative++; try { return this.ev(a.e, scope); } finally { this.imperative--; } }); R.setProp(el, a.name, h); } else R.effect(() => this.ev(a.e, scope), v => { R.setProp(el, a.name, v); }); } // 07 §5: a handler is an imperative closure
for (const item of (untrack(() => this.fragment(m.children, scope, true)) as unknown[]).flat(Infinity)) { const marker = R.createTextNode(""); R.insertNode(el, marker); R.insert(el, item as never, marker); }
return el;
}
Expand Down
Loading
Loading