From bb65e10f5c22471c4b8e697ca2bcc8f450b0d14c Mon Sep 17 00:00:00 2001 From: enzotar Date: Thu, 26 Feb 2026 16:03:29 -0800 Subject: [PATCH] feat: when/else conditional branching - AST: When(cond, body) -> When(cond, body, Option) - 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 --- compiler/ds-analyzer/src/signal_graph.rs | 10 ++++-- compiler/ds-codegen/src/js_emitter.rs | 46 ++++++++++++++++++++++-- compiler/ds-parser/src/ast.rs | 4 +-- compiler/ds-parser/src/parser.rs | 12 ++++++- compiler/ds-types/src/checker.rs | 10 ++++-- examples/when-else-demo.ds | 17 +++++++++ 6 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 examples/when-else-demo.ds diff --git a/compiler/ds-analyzer/src/signal_graph.rs b/compiler/ds-analyzer/src/signal_graph.rs index 57fd357..4240a99 100644 --- a/compiler/ds-analyzer/src/signal_graph.rs +++ b/compiler/ds-analyzer/src/signal_graph.rs @@ -321,9 +321,12 @@ fn collect_deps(expr: &Expr, deps: &mut Vec) { 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) { } } } - 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); + } } _ => {} } diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index cbe820f..89fdfb7 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -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("});"); diff --git a/compiler/ds-parser/src/ast.rs b/compiler/ds-parser/src/ast.rs index a5229d7..ec78e17 100644 --- a/compiler/ds-parser/src/ast.rs +++ b/compiler/ds-parser/src/ast.rs @@ -261,8 +261,8 @@ pub enum Expr { Element(Element), /// Container: `column [ ... ]`, `row [ ... ]` Container(Container), - /// When conditional: `when count > 10 -> ...` - When(Box, Box), + /// When conditional: `when count > 10 -> ...` with optional `else -> ...` + When(Box, Box, Option>), /// Each loop: `each item in list => template` Each(String, Box, Box), diff --git a/compiler/ds-parser/src/parser.rs b/compiler/ds-parser/src/parser.rs index 99ef0bf..cddd620 100644 --- a/compiler/ds-parser/src/parser.rs +++ b/compiler/ds-parser/src/parser.rs @@ -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` diff --git a/compiler/ds-types/src/checker.rs b/compiler/ds-types/src/checker.rs index e1245dc..aa2f82b 100644 --- a/compiler/ds-types/src/checker.rs +++ b/compiler/ds-types/src/checker.rs @@ -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) => { diff --git a/examples/when-else-demo.ds b/examples/when-else-demo.ds new file mode 100644 index 0000000..deb4122 --- /dev/null +++ b/examples/when-else-demo.ds @@ -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 } +]