From 10b271728112832ce5f508842736dcbb3213e3a4 Mon Sep 17 00:00:00 2001 From: enzotar Date: Thu, 26 Feb 2026 17:29:47 -0800 Subject: [PATCH] feat: multi-statement event handlers with semicolons - Added Semicolon token to lexer - Enables: click: items.push(x); input = "" - Push-and-clear, increment-and-reset, clear-all patterns - Browser-verified: both actions fire in one click - All existing examples pass regression --- compiler/ds-parser/src/lexer.rs | 2 ++ compiler/ds-parser/src/parser.rs | 19 ++++++++++++++++++- examples/multi-action.ds | 28 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 examples/multi-action.ds diff --git a/compiler/ds-parser/src/lexer.rs b/compiler/ds-parser/src/lexer.rs index a21443d..79ae771 100644 --- a/compiler/ds-parser/src/lexer.rs +++ b/compiler/ds-parser/src/lexer.rs @@ -82,6 +82,7 @@ pub enum TokenKind { PlusEq, // += MinusEq, // -= Arrow, // -> + Semicolon, // ; Pipe, // | Dot, // . @@ -223,6 +224,7 @@ impl Lexer { '>' => { self.advance(); Token { kind: TokenKind::Gt, lexeme: ">".into(), line, col } } '!' => { self.advance(); Token { kind: TokenKind::Not, lexeme: "!".into(), line, col } } '|' => { self.advance(); Token { kind: TokenKind::Pipe, lexeme: "|".into(), line, col } } + ';' => { self.advance(); Token { kind: TokenKind::Semicolon, lexeme: ";".into(), line, col } } '.' => { self.advance(); Token { kind: TokenKind::Dot, lexeme: ".".into(), line, col } } '(' => { self.advance(); Token { kind: TokenKind::LParen, lexeme: "(".into(), line, col } } ')' => { self.advance(); Token { kind: TokenKind::RParen, lexeme: ")".into(), line, col } } diff --git a/compiler/ds-parser/src/parser.rs b/compiler/ds-parser/src/parser.rs index 254d655..29f1363 100644 --- a/compiler/ds-parser/src/parser.rs +++ b/compiler/ds-parser/src/parser.rs @@ -1355,7 +1355,24 @@ impl Parser { let key = self.expect_ident()?; self.expect(&TokenKind::Colon)?; self.skip_newlines(); - let val = self.parse_expr()?; + let first = self.parse_expr()?; + // Multi-action: `click: action1; action2; action3` + // Collects semicolon-separated expressions into a Block + let val = if self.check(&TokenKind::Semicolon) { + let mut exprs = vec![first]; + while self.check(&TokenKind::Semicolon) { + self.advance(); // consume ';' + self.skip_newlines(); + // Stop if we hit } (end of props) or , (next prop) + if self.check(&TokenKind::RBrace) || self.check(&TokenKind::Comma) { + break; + } + exprs.push(self.parse_expr()?); + } + Expr::Block(exprs) + } else { + first + }; props.push((key, val)); self.skip_newlines(); if self.check(&TokenKind::Comma) { diff --git a/examples/multi-action.ds b/examples/multi-action.ds new file mode 100644 index 0000000..f6950a1 --- /dev/null +++ b/examples/multi-action.ds @@ -0,0 +1,28 @@ +-- Multi-Action Click Handler Test +-- Tests semicolon-separated actions in click handlers + +let items = ["Buy milk", "Walk dog"] +let input = "" +let count = 0 + +view main = column [ + text "Multi-Action Demo" { variant: "title" } + text "Add items AND clear input in one click" { variant: "subtitle" } + + row [ + input { bind: input, placeholder: "New item..." } + button "Add & Clear" { click: items.push(input); input = "", variant: "primary" } + ] + + each item in items -> + row [ + text "• {item}" + button "×" { click: items.remove(_idx), variant: "ghost" } + ] + + text "Count: {count}" + row [ + button "+5 & Reset Name" { click: count += 5; input = "reset!", variant: "secondary" } + button "Reset All" { click: count = 0; items = [], variant: "ghost" } + ] +]