Skip to content
Draft
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
282 changes: 232 additions & 50 deletions compiler/src/check.rs

Large diffs are not rendered by default.

38 changes: 33 additions & 5 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,13 @@ impl<'p> Gen<'p> {
self.set(env, &n.alias, b);
continue;
}
// MO-11a: a JS component is a JSX tag
if let Some(JsDecl::Comp { .. }) = x {
let mut c = bind(Kind::Comp, cm(&n.alias));
c.shape = Some(FShape::Comp);
self.set(env, &n.alias, c);
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 @@ -656,6 +663,14 @@ impl<'p> Gen<'p> {
let mut b = bind(Kind::Acc, mg(name));
if let ExprKind::Call { f, .. } = &e.k {
if let ExprKind::Id(fname) = &f.k {
// MO-11f: a factory's component is a JSX tag
if self.lookup(env, fname).is_some_and(|x| x.kind == Kind::Jfn && x.shape == Some(FShape::Comp)) {
let mut c = bind(Kind::Comp, cm(name));
c.inst = Some(cm(name));
c.shape = Some(FShape::Comp);
self.set(env, name, c);
continue;
}
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();
Expand Down Expand Up @@ -692,8 +707,9 @@ impl<'p> Gen<'p> {
} else {
return fail(format!("{name} from \"{from}\" is not in the scenario prelude"));
};
out.push(konst(mg(alias), v));
out.push(sexpr(rtc("reg", vec![str(alias.clone()), obj(vec![("value", id(mg(alias)))])])));
let js = if self.lookup(env, alias).is_some_and(|b| b.kind == Kind::Comp) { cm(alias) } else { mg(alias) };
out.push(konst(js.clone(), v));
out.push(sexpr(rtc("reg", vec![str(alias.clone()), obj(vec![("value", id(js))])])));
}
at(&mut out, n0, d.line());
}
Expand Down Expand Up @@ -1032,7 +1048,11 @@ impl<'p> Gen<'p> {
}
}
}
if let Some(fx) = self.foreign(e, env, &b)? {
if let Some(mut fx) = self.foreign(e, env, &b)? {
if b.kind == Kind::Comp {
fx.push(sexpr(rtc("reg", vec![str(name.clone()), obj(vec![("value", id(b.js.clone()))])])));
return Ok(fx);
}
return fin(fx, self);
}
if let Some(k) = key {
Expand Down Expand Up @@ -1198,8 +1218,13 @@ impl<'p> Gen<'p> {
}
out.push(rtc("memo", vec![E::Null, thunk(self.expr(a, env, &reactive())?)]));
}
// MO-11f: the component the factory hands back
if fb.shape == Some(FShape::Comp) {
return Ok(Some(vec![konst(b.js.clone(), call(id(fb.js.clone()), out))]));
}
let r = b.inst.clone().unwrap();
let whole = match fb.shape.as_ref().unwrap() {
FShape::Comp => unreachable!(),
FShape::Part(p) => part(id(r.clone()), *p),
FShape::Items(items) => rtc(
"memo",
Expand Down Expand Up @@ -1465,7 +1490,7 @@ impl<'p> Gen<'p> {
};
match b.kind {
Kind::Acc | Kind::Src => Ok(if ctx.imp { rtc("snapshot", vec![id(b.js)]) } else { call(id(b.js), vec![]) }),
Kind::Comp => fail(format!("component {name} used as a value")),
// a component value is the compiled component (MO-11c)
_ => Ok(id(b.js)),
}
}
Expand Down Expand Up @@ -1690,7 +1715,10 @@ impl<'p> Gen<'p> {
xs.push(self.attr(&a.name, &a.e, env)?);
}
if children.is_empty() {
xs.push(("children".into(), AttrValue::Expr(E::Arr(vec![]))));
// MO-11: a JS component gets JSX's own `<X />`, with no children prop
if b.shape != Some(FShape::Comp) {
xs.push(("children".into(), AttrValue::Expr(E::Arr(vec![]))));
}
el(JsxName::Ident(b.js), xs, vec![], true)
} else {
el(JsxName::Ident(b.js), xs, self.kids(children, env)?, false)
Expand Down
168 changes: 165 additions & 3 deletions compiler/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,13 +412,16 @@ pub enum Shape {
Part(Part),
Items(Vec<Part>),
Fields(Vec<(String, Part)>),
/// MO-11f: the whole result is a component
Comp,
}
impl Shape {
pub fn has_ref(&self) -> bool {
match self {
Shape::Part(p) => *p == Part::Ref,
Shape::Items(xs) => xs.contains(&Part::Ref),
Shape::Fields(fs) => fs.iter().any(|f| f.1 == Part::Ref),
Shape::Comp => false,
}
}
}
Expand Down Expand Up @@ -473,6 +476,9 @@ fn shape_of(t: &str) -> Shape {
.collect(),
);
}
if is_component_type(t) {
return Shape::Comp;
}
Shape::Part(part_of(t))
}

Expand All @@ -484,14 +490,166 @@ pub enum JsDecl {
Fn { name: String, pure: bool, params: Vec<String>, rejects: Option<Vec<String>>, line: usize, side: Option<Side>, ptypes: Vec<Option<String>>, ret: Option<String>, refs: Vec<bool>, shape: Shape, instantiates: bool },
Error { name: String, line: usize },
Type { name: String, line: usize, text: String },
/// MO-11a: a Solid component
Comp { name: String, line: usize, props: Vec<JsProp>, fails: Vec<String>, content: Option<crate::content::Shape> },
}
/// MO-11a: a declared JS component's prop — its TypeScript type, what the component discharges for it (`None` in the list:
/// every class), and whether it takes a component value (MO-11c: its type is `Component`).
#[derive(Debug, Clone)]
pub struct JsProp {
pub name: String,
pub optional: bool,
pub ty: Option<String>,
pub catches: Option<Option<Vec<String>>>,
pub component: bool,
}
impl JsDecl {
pub fn name(&self) -> &str {
match self {
JsDecl::Fn { name, .. } | JsDecl::Error { name, .. } | JsDecl::Type { name, .. } => name,
JsDecl::Fn { name, .. } | JsDecl::Error { name, .. } | JsDecl::Type { name, .. } | JsDecl::Comp { name, .. } => name,
}
}
}

/// MO-11: `Component` or `Component<P>`, and nothing that only contains one (`Component[]`).
fn is_component_type(t: &str) -> bool {
match t.strip_prefix("Component") {
Some(r) => {
let r = r.trim_start();
if r.is_empty() {
return true;
}
if !r.starts_with('<') {
return false;
}
// the `>` that closes the first `<` ends the type
let b = r.as_bytes();
let mut depth = 0;
for i in 0..b.len() {
if b[i] == b'<' {
depth += 1;
} else if b[i] == b'>' && (i == 0 || b[i - 1] != b'=') {
depth -= 1;
if depth == 0 {
return i == b.len() - 1;
}
}
}
false
}
None => false,
}
}
/// MO-11a: `component A(props: { href: string; children?: View }) fails(…) shape(…)`.
fn parse_js_component(c: &mut Cursor, line: usize) -> R<JsDecl> {
c.ws(true);
let name = c.ident_or("component name")?;
c.ws(true);
c.expect("(")?;
c.ws(true);
let mut props = vec![];
if !c.starts_with(")") {
if c.ident().as_deref() != Some("props") {
return c.fail("a component's props: `(props: { … })`");
}
c.ws(true);
c.expect(":")?;
c.ws(true);
c.expect("{")?;
loop {
c.ws(true);
if c.eat("}") {
break;
}
let fname = c.ident_or("a prop name")?;
c.ws(false);
let optional = c.eat("?");
c.ws(false);
let (mut ty, mut catches) = (None, None);
if c.eat(":") {
c.ws(false);
if c.keyword("catches") {
c.ws(false);
c.expect("(")?;
c.ws(true);
catches = Some(if c.eat("*") { None } else { parse_classes(c)? });
c.ws(true);
c.expect(")")?;
c.ws(false);
}
let t0 = c.i;
let mut depth = 0i32;
while !c.eof() {
let ch = c.peek();
if depth == 0 && (matches!(ch, ';' | ',' | '}' | '\n') || c.starts_with("//")) {
break;
}
// a string literal type
if ch == '"' || ch == '\'' || ch == '`' {
c.bump();
while !c.eof() && c.peek() != ch {
if c.peek() == '\\' {
c.bump();
}
c.bump();
}
c.bump();
continue;
}
if "([{<".contains(ch) {
depth += 1;
} else if ")]}".contains(ch) || (ch == '>' && c.i > 0 && c.src.as_bytes()[c.i - 1] != b'=') {
depth -= 1;
}
c.bump();
}
let t = js_trim(&c.src[t0..c.i]);
ty = (!t.is_empty()).then(|| t.to_string());
}
let component = ty.as_deref().is_some_and(is_component_type);
props.push(JsProp { name: fname, optional, ty, catches, component });
// a newline separates fields too, as in TypeScript
c.ws(false);
if !c.eat(";") && !c.eat(",") && c.peek() != '\n' {
c.ws(true);
c.expect("}")?;
break;
}
}
c.ws(true);
}
c.expect(")")?;
let mut fails = vec![];
let mut content = None;
loop {
c.ws(false);
if c.keyword("fails") {
c.ws(false);
c.expect("(")?;
fails = parse_classes(c)?.unwrap_or_else(|| vec!["Error".to_string()]);
c.ws(true);
c.expect(")")?;
continue;
}
if c.keyword("shape") {
c.ws(false);
c.expect("(")?;
let t0 = c.i;
while !c.eof() && c.peek() != ')' {
c.bump();
}
let t = c.src[t0..c.i].to_string();
c.expect(")")?;
match crate::signature::content_shape_of(Some(&t)) {
Ok(sh) => content = Some(sh),
Err(e) => return c.fail(&e),
}
continue;
}
break;
}
Ok(JsDecl::Comp { name, line, props, fails, content })
}
pub fn parse_declarations(src: &str) -> R<Vec<JsDecl>> {
let mut c = Cursor::new(src);
let mut out = vec![];
Expand All @@ -502,7 +660,7 @@ pub fn parse_declarations(src: &str) -> R<Vec<JsDecl>> {
}
let line = c.line();
if !c.keyword("export") {
return c.fail("a declaration file declares exports: `export pure fn …`, `export fn …`, `export error …`, `export type …`");
return c.fail("a declaration file declares exports: `export pure fn …`, `export fn …`, `export component …`, `export error …`, `export type …`");
}
c.ws(true);
if c.starts_with("type") || c.starts_with("interface") {
Expand Down Expand Up @@ -538,6 +696,10 @@ pub fn parse_declarations(src: &str) -> R<Vec<JsDecl>> {
out.push(JsDecl::Error { name, line });
continue;
}
if c.keyword("component") {
out.push(parse_js_component(&mut c, line)?);
continue;
}
let side = if c.keyword("server") {
Some(Side::Server)
} else if c.keyword("client") {
Expand Down Expand Up @@ -621,7 +783,7 @@ pub fn parse_declarations(src: &str) -> R<Vec<JsDecl>> {
}
c.ws(false);
let written = c.keyword("instantiates");
let instantiates = written || refs.iter().any(|x| *x) || shape.as_ref().is_some_and(|s| s.has_ref()); // MO-10a
let instantiates = written || refs.iter().any(|x| *x) || shape.as_ref().is_some_and(|s| s.has_ref() || *s == Shape::Comp); // MO-10a; MO-11f: a component handed back
let shape = shape.unwrap_or(Shape::Part(Part::Value));
out.push(JsDecl::Fn { name, pure, params, rejects, line, side, ptypes, ret, refs, shape, instantiates });
}
Expand Down
22 changes: 18 additions & 4 deletions compiler/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,23 @@ fn signature_stub(sig: &ModSig) -> String {
}
/// MO-8: a `.d.rx` as TypeScript's `.d.ts` for the same JS module.
fn declaration_file(decls: &[JsDecl]) -> String {
decls
.iter()
.map(|d| match d {
let own: IndexSet<&str> = decls.iter().filter(|d| matches!(d, JsDecl::Type { .. })).map(|d| d.name()).collect();
let mut head: Vec<String> = vec![];
if decls.iter().any(|d| matches!(d, JsDecl::Comp { .. } | JsDecl::Fn { shape: crate::parse::Shape::Comp, .. })) {
// MO-11a: a field's types
if !own.contains("View") {
head.push("type View = import(\"@solidjs/web/jsx-runtime\").JSX.Element;".into());
}
if !own.contains("Component") {
head.push("type Component<P = any> = (props: P) => View;".into());
}
}
head.into_iter()
.chain(decls.iter().map(|d| match d {
JsDecl::Comp { name, props, .. } => {
let fs: Vec<String> = props.iter().map(|p| format!("{}{}: {};", p.name, if p.optional { "?" } else { "" }, p.ty.as_deref().unwrap_or("any"))).collect();
format!("export declare function {name}(props: {{ {} }}): View;", fs.join(" "))
}
JsDecl::Type { text, .. } => format!("export {text}"),
JsDecl::Error { name, .. } => format!("export declare class {name}$ extends Error {{}}\nexport declare const {name}: (message?: string) => {name}$;\nexport type {name} = {name}$;"),
JsDecl::Fn { name, params, ptypes, ret, .. } => {
Expand All @@ -238,7 +252,7 @@ fn declaration_file(decls: &[JsDecl]) -> String {
});
format!("export declare function {name}({}): {};", ps.collect::<Vec<_>>().join(", "), ret.as_deref().unwrap_or("any"))
}
})
}))
.collect::<Vec<_>>()
.join("\n")
+ "\n"
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub struct PropSig {
pub p: Catch,
pub rendered: bool,
pub action: Option<(Catch, String)>,
/// MO-11c: a JS component's prop that takes a component value
pub component: bool,
}
#[derive(Debug, Clone)]
pub struct Own {
Expand Down Expand Up @@ -360,7 +362,8 @@ fn row_words(s: &str) -> RowWords {
}
RowWords { rows, fails, rest: words[i..].join(" ") }
}
fn shape_of(s: Option<&str>) -> Result<Shape, String> {
/// SV-7d's words (MO-9b; a JS component's, MO-11a).
pub fn content_shape_of(s: Option<&str>) -> Result<Shape, String> {
let mut sh = Shape::default();
let Some(s) = s else { return Ok(sh) };
for p in s.split(';').map(crate::parse::js_trim).filter(|x| !x.is_empty()) {
Expand Down Expand Up @@ -461,7 +464,7 @@ pub fn parse_signature(src: &str, file: &str) -> Result<ModSig, SignatureError>
None => (crate::parse::js_trim(&f).to_string(), String::new()),
};
let fname = if fname0 == "...props" { "*".to_string() } else { fname0 };
let mut v = PropSig { f: Catch::Set(IndexSet::new()), p: Catch::Set(IndexSet::new()), rendered: true, action: None };
let mut v = PropSig { f: Catch::Set(IndexSet::new()), p: Catch::Set(IndexSet::new()), rendered: true, action: None, component: false };
let a = P_ACTION.captures(&spec);
let rest = match &a {
Some(a) => {
Expand Down Expand Up @@ -507,7 +510,7 @@ pub fn parse_signature(src: &str, file: &str) -> Result<ModSig, SignatureError>
sig.stored.entry(f).or_default().push(if t == "module" { t } else { abs_key(file, &t) });
}
}
sig.shape = Some(shape_of(C_SHAPE.captures(tail).map(|x| x.get(2).unwrap().as_str())).map_err(|e| fail(&e))?);
sig.shape = Some(content_shape_of(C_SHAPE.captures(tail).map(|x| x.get(2).unwrap().as_str())).map_err(|e| fail(&e))?);
exports.insert(m[1].to_string(), ExportSig::Comp { sig });
continue;
}
Expand Down
Loading
Loading