feat: when/else conditional branching

- AST: When(cond, body) -> When(cond, body, Option<else_body>)
- Parser: optional 'else -> expr' after when body
- Codegen: reactive DOM swap with anchor comments
- Signal graph + type checker updated for 3-arg When
- Component prop signal wrapping: .value compatible accessors
- Added examples/when-else-demo.ds
This commit is contained in:
enzotar 2026-02-26 16:03:29 -08:00
parent 55dc24eecc
commit bb65e10f5c
6 changed files with 89 additions and 10 deletions

View file

@ -321,9 +321,12 @@ fn collect_deps(expr: &Expr, deps: &mut Vec<String>) {
collect_deps(item, deps);
}
}
Expr::When(cond, body) => {
Expr::When(cond, body, else_body) => {
collect_deps(cond, deps);
collect_deps(body, deps);
if let Some(eb) = else_body {
collect_deps(eb, deps);
}
}
Expr::Match(scrutinee, arms) => {
collect_deps(scrutinee, deps);
@ -439,13 +442,16 @@ fn collect_bindings(expr: &Expr, bindings: &mut Vec<DomBinding>) {
}
}
}
Expr::When(cond, body) => {
Expr::When(cond, body, else_body) => {
let deps = extract_dependencies(cond);
bindings.push(DomBinding {
kind: BindingKind::Conditional { condition_signals: deps.clone() },
dependencies: deps,
});
collect_bindings(body, bindings);
if let Some(eb) = else_body {
collect_bindings(eb, bindings);
}
}
_ => {}
}

View file

@ -743,7 +743,8 @@ impl JsEmitter {
node_var
}
Expr::When(cond, body) => {
Expr::When(cond, body, else_body) => {
let else_expr = else_body.clone();
let anchor_var = self.next_node_id();
let container_var = self.next_node_id();
let cond_js = self.emit_expr(cond);
@ -751,12 +752,25 @@ impl JsEmitter {
self.emit_line(&format!("const {} = document.createComment('when');", anchor_var));
self.emit_line(&format!("let {} = null;", container_var));
// Build the conditional child
let has_else = else_expr.is_some();
let else_container = if has_else {
let ec = self.next_node_id();
self.emit_line(&format!("let {} = null;", ec));
Some(ec)
} else {
None
};
self.emit_line("DS.effect(() => {");
self.indent += 1;
self.emit_line(&format!("const show = {};", cond_js));
// Show when: condition true and not already showing
self.emit_line(&format!("if (show && !{}) {{", container_var));
self.indent += 1;
if let Some(ref ec) = else_container {
self.emit_line(&format!("if ({}) {{ {}.remove(); {} = null; }}", ec, ec, ec));
}
let child_var = self.emit_view_expr(body, graph);
self.emit_line(&format!("{} = {};", container_var, child_var));
self.emit_line(&format!(
@ -764,11 +778,39 @@ impl JsEmitter {
anchor_var, container_var, anchor_var
));
self.indent -= 1;
// Hide when: condition false and currently showing
self.emit_line(&format!("}} else if (!show && {}) {{", container_var));
self.indent += 1;
self.emit_line(&format!("{}.remove();", container_var));
self.emit_line(&format!("{} = null;", container_var));
if let Some(eb) = &else_expr {
if let Some(ec) = &else_container {
let else_child = self.emit_view_expr(eb, graph);
self.emit_line(&format!("{} = {};", ec, else_child));
self.emit_line(&format!(
"{}.parentNode.insertBefore({}, {}.nextSibling);",
anchor_var, ec, anchor_var
));
}
}
self.indent -= 1;
// Initial else: show=false and nothing rendered yet
if let Some(eb) = &else_expr {
if let Some(ec) = &else_container {
self.emit_line(&format!("}} else if (!show && !{} && !{}) {{", container_var, ec));
self.indent += 1;
let else_child2 = self.emit_view_expr(eb, graph);
self.emit_line(&format!("{} = {};", ec, else_child2));
self.emit_line(&format!(
"{}.parentNode.insertBefore({}, {}.nextSibling);",
anchor_var, ec, anchor_var
));
self.indent -= 1;
}
}
self.emit_line("}");
self.indent -= 1;
self.emit_line("});");

View file

@ -261,8 +261,8 @@ pub enum Expr {
Element(Element),
/// Container: `column [ ... ]`, `row [ ... ]`
Container(Container),
/// When conditional: `when count > 10 -> ...`
When(Box<Expr>, Box<Expr>),
/// When conditional: `when count > 10 -> ...` with optional `else -> ...`
When(Box<Expr>, Box<Expr>, Option<Box<Expr>>),
/// Each loop: `each item in list => template`
Each(String, Box<Expr>, Box<Expr>),

View file

@ -891,7 +891,17 @@ impl Parser {
self.expect(&TokenKind::Arrow)?;
self.skip_newlines();
let body = self.parse_expr()?;
Ok(Expr::When(Box::new(cond), Box::new(body)))
// Optional else branch
self.skip_newlines();
let else_body = if self.check(&TokenKind::Else) {
self.advance();
self.expect(&TokenKind::Arrow)?;
self.skip_newlines();
Some(Box::new(self.parse_expr()?))
} else {
None
};
Ok(Expr::When(Box::new(cond), Box::new(body), else_body))
}
// Each loop: `each item in list => template`

View file

@ -391,7 +391,7 @@ impl TypeChecker {
});
}
}
Expr::When(condition, body) => {
Expr::When(condition, body, else_body) => {
let cond_type = self.infer_expr(condition);
let inner = cond_type.unwrap_reactive().clone();
if inner != Type::Bool && !matches!(inner, Type::Var(_)) && inner != Type::Error {
@ -634,8 +634,12 @@ impl TypeChecker {
self.infer_expr(then_br)
}
Expr::When(_, body) => {
self.infer_expr(body)
Expr::When(_, body, else_body) => {
let body_ty = self.infer_expr(body);
if let Some(eb) = else_body {
let _ = self.infer_expr(eb);
}
body_ty
}
Expr::Block(exprs) => {

View file

@ -0,0 +1,17 @@
-- DreamStack When/Else Demo
let loggedIn = false
let count = 0
view main = column [
text "🔀 When/Else Demo" { variant: "title" }
button "Toggle Login" { click: loggedIn = !loggedIn }
when loggedIn ->
text "Welcome back! ✅"
else ->
text "Please log in 🔒"
text "Count: {count}"
button "+1" { click: count += 1 }
]