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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

**New**

- Solid libraries (MO-10): declare a primitive once in a `.d.rx` (`export pure fn createToggle(v: boolean): [ref
boolean, (v: boolean) => void]`) and use it like .rx: it is called once, `ref` arguments go in as accessors, readers
are read without a call, and setters are io. A package's `.d.rx` sits beside the file its import resolves to.
`guide/libraries.md`.

## 0.2.0

`@reactivescript/compiler`, `@reactivescript/runtime`, `@reactivescript/language-server` and the editor extension,
Expand Down
77 changes: 74 additions & 3 deletions compiler/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ use crate::content::{misnested, misnested_inside, Shape, P_CLOSERS, P_SCOPE};
use crate::infer::{inferred_action, run_findings, trigger_text, Code};
use crate::model::{ModelBuilder, ModelSymbol};
use crate::modules::ModuleSet;
use crate::parse::{js_trim, JsDecl};
use crate::parse::{js_trim, JsDecl, Part, Shape as FShape};
use crate::signature::{Catch, CompSig, ExportSig, ModSig, Own, PropSig, SigParam, HOLD};
use indexmap::{IndexMap, IndexSet};
use regex::Regex;
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;

Expand Down Expand Up @@ -238,6 +239,10 @@ struct Sym<'a> {
server_rejects: Option<ServerRejects<'a>>,
fn_decl: Option<&'a Decl>,
decl_env: Option<EId>,
/// MO-10: a declared JS primitive (a fn) or an instance of one, or a part of it (an equation): what its result is
foreign: Option<FShape>,
/// MO-10d: bound from a foreign accessor — it may hold, as a question's reader does
reader: bool,
}
impl<'a> Sym<'a> {
fn new(kind: SymKind, name: &str, region: RId, line: i64) -> Self {
Expand Down Expand Up @@ -271,6 +276,8 @@ impl<'a> Sym<'a> {
server_rejects: None,
fn_decl: None,
decl_env: None,
foreign: None,
reader: false,
}
}
}
Expand Down Expand Up @@ -834,6 +841,8 @@ pub struct Opts<'a> {
}

pub struct Checker<'a> {
/// MO-10d: CN-3e asks what may hold, where a foreign reader counts (R-8 asks what may be absent, where it does not)
holds_mode: Cell<bool>,
pub diags: Vec<Diagnostic>,
pub model: ModelBuilder,
opts: Opts<'a>,
Expand Down Expand Up @@ -913,6 +922,7 @@ pub fn check_modules(set: &ModuleSet, spa: bool) -> Checked {
impl<'a> Checker<'a> {
pub fn new(opts: Opts<'a>) -> Self {
Checker {
holds_mode: Cell::new(false),
diags: vec![],
model: ModelBuilder::default(),
opts,
Expand Down Expand Up @@ -1201,6 +1211,7 @@ impl<'a> Checker<'a> {
}
}
}
self.foreign_part(&mut sym, e, env);
if let Some(sf) = unwrap_server(e) {
sym.kind = SymKind::Server;
if let ExprKind::ServerFn { params, .. } = &sf.k {
Expand Down Expand Up @@ -1417,7 +1428,22 @@ impl<'a> Checker<'a> {
self.bind(env, alias, Sym::new(SymKind::ErrClass, alias, region, line));
continue;
}
JsDecl::Fn { pure: xpure, side: xside, rejects, .. } => {
JsDecl::Fn { pure: xpure, side: xside, rejects, params: xparams, refs, shape, instantiates, .. } => {
if *instantiates {
// MO-10a: a primitive is an instantiation (RG-6)
if *pure && !xpure {
self.err("IMPORT_ROW_MISMATCH", "MO-8", format!("{} is declared io in {} (`export fn`, not `export pure fn`); `pure` on the import cannot lower it", name, file));
}
let mut s = Sym::new(SymKind::Fn, alias, region, line);
s.params = Some(xparams.iter().enumerate().map(|(i, n)| SigParam { name: n.clone(), r#ref: refs.get(i).copied().unwrap_or(false), ty: None }).collect());
s.instantiating = true;
s.foreign = Some(shape.clone());
s.rows = Some(RowsT::Fixed(IndexSet::new()));
s.fails_t = Some(FailsT::Fixed(IndexSet::new()));
s.rejects = Some(IndexSet::new());
self.bind(env, alias, s);
continue;
}
if *pure && !xpure {
self.err("IMPORT_ROW_MISMATCH", "MO-8", format!("{} is declared io in {} (`export fn`, not `export pure fn`); `pure` on the import cannot lower it", name, file));
}
Expand All @@ -1440,6 +1466,39 @@ impl<'a> Checker<'a> {
}
}

/// MO-10b: an equation bound from a primitive's result, or from a part of it (`[a, b] = f(…)`, `x.k`): a `ref` part is
/// a reader (it may hold, MO-10d), a function part is io (R-io-3), anything else is a value.
fn foreign_part(&self, sym: &mut Sym<'a>, e: &Expr, env: EId) {
let shape: Option<FShape> = match &e.k {
ExprKind::Call { f, .. } => match &f.k {
ExprKind::Id(n) => self.lookup(env, n).and_then(|t| if self.syms[t].kind == SymKind::Fn { self.syms[t].foreign.clone() } else { None }),
_ => None,
},
ExprKind::Index { o, i, .. } => match (&o.k, &i.k) {
(ExprKind::Id(on), ExprKind::Lit(Lit::Num(v))) => match self.lookup(env, on).and_then(|t| self.syms[t].foreign.clone()) {
Some(FShape::Items(items)) if *v >= 0.0 && v.fract() == 0.0 => items.get(*v as usize).map(|p| FShape::Part(*p)),
_ => None,
},
_ => None,
},
ExprKind::Member { o, p, .. } => match &o.k {
ExprKind::Id(on) => match self.lookup(env, on).and_then(|t| self.syms[t].foreign.clone()) {
Some(FShape::Fields(fields)) => fields.iter().find(|f| &f.0 == p).map(|f| FShape::Part(f.1)),
_ => None,
},
_ => None,
},
_ => None,
};
let Some(shape) = shape else { return };
if shape == FShape::Part(Part::Fn) {
sym.io = true;
}
if shape == FShape::Part(Part::Ref) {
sym.reader = true;
}
sym.foreign = Some(shape);
}
/// MO-3: this module's signature — every export as an importer sees it.
pub fn signature(&mut self, p: &'a Program) -> ModSig {
let mut exports = IndexMap::new();
Expand Down Expand Up @@ -3225,6 +3284,15 @@ impl<'a> Checker<'a> {
if let Some(&b) = memo.get(&s) {
return b;
}
if self.holds_mode.get() {
// MO-10: a reader may hold; a primitive's other parts are bound as they are
if self.syms[s].reader {
return true;
}
if matches!(self.syms[s].foreign, Some(FShape::Part(Part::Fn | Part::Value))) {
return false;
}
}
let Some((e, env)) = self.syms[s].expr else { return false };
if !busy.insert(s) {
return false;
Expand Down Expand Up @@ -3388,7 +3456,10 @@ impl<'a> Checker<'a> {
ExprKind::Id(name) => {
if let Some(t) = self.lookup(env, name) {
if matches!(self.syms[t].kind, SymKind::Eq | SymKind::Source) && busy.insert(t) {
if self.absent(t, &mut HashMap::new(), &mut HashSet::new()) {
self.holds_mode.set(true);
let held = self.absent(t, &mut HashMap::new(), &mut HashSet::new());
self.holds_mode.set(false);
if held {
out.push(t);
}
if let Some((te, tenv)) = self.syms[t].expr {
Expand Down
106 changes: 104 additions & 2 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::awaits::{is_handler, non_tail_awaits, view_readers};
use crate::check::{children, unwrap_server, Node};
use crate::js::*;
use crate::modules::ModuleSet;
use crate::parse::{JsDecl, Part, Shape as FShape};
use crate::signature::ExportSig;
use indexmap::{IndexMap, IndexSet};
use serde_json::Value;
Expand Down Expand Up @@ -41,6 +42,8 @@ enum Kind {
Val,
Fn,
Ifn,
/// MO-10: a declared JS primitive
Jfn,
Act,
Comp,
}
Expand All @@ -58,9 +61,12 @@ struct Bind {
prev: Option<String>,
/// `client` JS (12 SF-5b)
client: bool,
/// MO-10: a primitive's result (Jfn), or an instance's (with `inst`, the name of its raw result)
shape: Option<FShape>,
inst: Option<String>,
}
fn bind(kind: Kind, js: impl Into<String>) -> Bind {
Bind { kind, js: js.into(), w: None, f: None, prev: None, client: false }
Bind { kind, js: js.into(), w: None, f: None, prev: None, client: false, shape: None, inst: None }
}
fn val(js: impl Into<String>) -> Bind {
bind(Kind::Val, js)
Expand Down Expand Up @@ -254,6 +260,11 @@ struct Gen<'p> {
style_ids: u32,
}

/// MO-10: the scenario prelude's `./prims`, a JS module of Solid primitives (`ref/compile/prims-solid.ts`).
fn is_prims(from: &str) -> bool {
from == "prims" || from == "prims.js" || from.ends_with("/prims") || from.ends_with("/prims.js")
}

impl<'p> Gen<'p> {
/// A JS import that becomes a real ES import (MO-8): a package's always; a relative one too when the modules go to a
/// bundler (`by_source`, the product), where `./util.js` is the app's own file. `./api` stays the app's injected API
Expand Down Expand Up @@ -608,8 +619,17 @@ impl<'p> Gen<'p> {
for d in decls {
match d {
Decl::Import { resolved: Some(_), .. } => {}
Decl::Import { names, side, declared_side, .. } => {
Decl::Import { names, side, declared_side, declared, .. } => {
for n in names {
// MO-10a: a declared primitive
let x = declared.as_ref().and_then(|dp| self.set.declarations.get(dp)).and_then(|ds| ds.iter().find(|y| y.name() == n.name));
if let Some(JsDecl::Fn { params, refs, shape, instantiates: true, .. }) = x {
let mut b = bind(Kind::Jfn, mg(&n.alias));
b.f = Some(FnRef { params: params.iter().enumerate().map(|(i, p)| (p.clone(), refs.get(i).copied().unwrap_or(false))).collect(), decl: None });
b.shape = Some(shape.clone());
self.set(env, &n.alias, b);
continue;
}
let mut b = val(mg(&n.alias));
b.client = *side == Some(crate::ast::Side::Client)
|| declared_side.as_ref().is_some_and(|ds| ds.iter().any(|(k, s)| *k == n.name && *s == crate::ast::Side::Client));
Expand All @@ -634,6 +654,14 @@ impl<'p> Gen<'p> {
Decl::Eq { name, e, .. } if unwrap_server(e).is_some() => self.set(env, name, val(mg(name))),
Decl::Eq { name, e, deferred, .. } => {
let mut b = bind(Kind::Acc, mg(name));
if let ExprKind::Call { f, .. } = &e.k {
if let ExprKind::Id(fname) = &f.k {
if let Some(jf) = self.lookup(env, fname).filter(|x| x.kind == Kind::Jfn) {
b.inst = Some(format!("r${name}"));
b.shape = jf.shape.clone();
}
}
}
if self.guessed_eq(name, e, *deferred) {
b.w = Some(format!("w${name}"));
}
Expand All @@ -658,6 +686,9 @@ impl<'p> Gen<'p> {
dot(id("__B"), name)
} else if from.ends_with("/api") {
rtc("api", vec![str(name.clone())])
} else if is_prims(from) {
// MO-10: the scenario prelude's Solid primitives
rtc("prim", vec![str(name.clone())])
} else {
return fail(format!("{name} from \"{from}\" is not in the scenario prelude"));
};
Expand Down Expand Up @@ -1001,6 +1032,9 @@ impl<'p> Gen<'p> {
}
}
}
if let Some(fx) = self.foreign(e, env, &b)? {
return fin(fx, self);
}
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);
Expand Down Expand Up @@ -1133,6 +1167,74 @@ impl<'p> Gen<'p> {
body.push(S::Return(Some(t)));
Ok(S::Func(Func { name: Some(mg(name)), params: ps, directives: vec![], body, is_async: false, generator: false }))
}
/// MO-10: `x = f(…)` over a declared primitive — called once (RG-6), a ref argument as an accessor, a value one
/// untracked; and `y = x[i]` / `x.k` over its result. A `ref` part is bound to the accessor itself, any other part to
/// its value.
fn foreign(&mut self, e: &Expr, env: Env, b: &Bind) -> R<Option<Vec<S>>> {
fn part(r: E, p: Part) -> E {
if p == Part::Ref {
r
} else {
thunk(r)
}
}
let ix = |r: &str, k: E| E::Index(Box::new(id(r)), Box::new(k), false);
match &e.k {
ExprKind::Call { f, args } => {
let ExprKind::Id(fname) = &f.k else { return Ok(None) };
let Some(fb) = self.lookup(env, fname).filter(|x| x.kind == Kind::Jfn) else { return Ok(None) };
let refs: Vec<bool> = fb.f.as_ref().unwrap().params.iter().map(|p| p.1).collect();
let mut out = vec![];
for (i, a) in args.iter().enumerate() {
if !refs.get(i).copied().unwrap_or(false) {
out.push(rtc("untrack", vec![thunk(self.expr(a, env, &reactive())?)]));
continue;
}
if let ExprKind::Id(n) = &a.k {
if let Some(ab) = self.lookup(env, n).filter(|ab| matches!(ab.kind, Kind::Acc | Kind::Src)) {
out.push(id(ab.js));
continue;
}
}
out.push(rtc("memo", vec![E::Null, thunk(self.expr(a, env, &reactive())?)]));
}
let r = b.inst.clone().unwrap();
let whole = match fb.shape.as_ref().unwrap() {
FShape::Part(p) => part(id(r.clone()), *p),
FShape::Items(items) => rtc(
"memo",
vec![E::Null, thunk(E::Arr(items.iter().enumerate().map(|(i, p)| { let x = ix(&r, E::Num(i as f64)); if *p == Part::Ref { call(x, vec![]) } else { x } }).collect()))],
),
FShape::Fields(fields) => rtc(
"memo",
vec![E::Null, thunk(E::Obj(fields.iter().map(|(k, p)| { let x = ix(&r, str(k.clone())); Prop::Kv(Key::Str(k.clone()), if *p == Part::Ref { call(x, vec![]) } else { x }) }).collect()))],
),
};
Ok(Some(vec![konst(r.clone(), call(id(fb.js.clone()), out)), konst(b.js.clone(), whole)]))
}
ExprKind::Index { o, .. } | ExprKind::Member { o, .. } => {
let ExprKind::Id(on) = &o.k else { return Ok(None) };
let Some(ob) = self.lookup(env, on) else { return Ok(None) };
let (Some(inst), Some(sh)) = (ob.inst.clone(), ob.shape.clone()) else { return Ok(None) };
let (key, p) = match (&e.k, &sh) {
(ExprKind::Index { i, .. }, FShape::Items(items)) => match &i.k {
ExprKind::Lit(Lit::Num(v)) if *v >= 0.0 && v.fract() == 0.0 => match items.get(*v as usize) {
Some(p) => (E::Num(*v), *p),
None => return Ok(None),
},
_ => return Ok(None),
},
(ExprKind::Member { p: k, .. }, FShape::Fields(fields)) => match fields.iter().find(|f| &f.0 == k) {
Some(f) => (str(k.clone()), f.1),
None => return Ok(None),
},
_ => return Ok(None),
};
Ok(Some(vec![konst(b.js.clone(), part(ix(&inst, key), p))]))
}
_ => Ok(None),
}
}
fn instantiate(&mut self, e: &Expr, env: Env) -> R<E> {
let ExprKind::Call { f, args } = &e.k else { unreachable!() };
let ExprKind::Id(fname) = &f.k else { unreachable!() };
Expand Down
Loading
Loading