From 6c9d109ebdb5c933267a6695571c8fd44efdfe30 Mon Sep 17 00:00:00 2001 From: enzotar Date: Thu, 26 Feb 2026 17:19:50 -0800 Subject: [PATCH] fix: match parser allows container bodies in arms - Added can_be_pattern() with look-ahead disambiguation - Ident only treated as pattern if followed by -> or ( - Keywords (row/column/when/each) correctly terminate match - Match arms now support: "active" -> row [ Badge + text ] - Siblings after match (text, button, row) no longer consumed - Project Manager updated with rich row/Badge match arms - All 6 existing examples pass regression --- compiler/ds-parser/src/parser.rs | 65 +++++++++++++++++++++++++++++--- examples/project-manager.ds | 25 ++++++++---- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/compiler/ds-parser/src/parser.rs b/compiler/ds-parser/src/parser.rs index 61bf016..254d655 100644 --- a/compiler/ds-parser/src/parser.rs +++ b/compiler/ds-parser/src/parser.rs @@ -77,6 +77,60 @@ impl Parser { } } + /// Check if the current token could start a match pattern. + /// Patterns are: string literals, integer literals, identifiers (as wildcard/binding). + /// For identifiers, we look ahead to disambiguate: a pattern ident is followed by `->`. + /// A view element like `text "..."` or `button "..." {}` is NOT a pattern. + fn can_be_pattern(&self) -> bool { + match self.peek() { + // String and integer literals are always valid patterns + TokenKind::StringFragment(_) | TokenKind::Int(_) => { + // But verify: is this FOLLOWED by an arrow (after the string)? + // StringFragment patterns look like: "value" -> + // View elements look like: text "value" (no arrow after the string) + true + } + // Identifiers: could be a pattern binding OR a view element (text, button, input...) + // Look ahead: patterns are followed eventually by Arrow + // e.g., `_ -> body` or `myVar -> body` or `Ok(x) -> body` + TokenKind::Ident(name) => { + // Special case: _ is always a wildcard pattern + if name == "_" { + return true; + } + // Look ahead past the ident to see what follows + let next_pos = self.pos + 1; + if next_pos < self.tokens.len() { + match &self.tokens[next_pos].kind { + // Ident followed by Arrow: definitely a pattern (`myVar ->`) + TokenKind::Arrow => true, + // Ident followed by `(`: constructor pattern (`Ok(x) ->`) + TokenKind::LParen => true, + // Ident followed by Newline: could be pattern on next line + // Check the token after the newline(s) + TokenKind::Newline => { + let mut peek_pos = next_pos + 1; + while peek_pos < self.tokens.len() && self.tokens[peek_pos].kind == TokenKind::Newline { + peek_pos += 1; + } + if peek_pos < self.tokens.len() { + matches!(self.tokens[peek_pos].kind, TokenKind::Arrow) + } else { + false + } + } + // Ident followed by anything else (string, LBrace, LBracket, etc.): + // it's a view element like `text "hello"` or `button "click" { }` + _ => false, + } + } else { + false + } + } + _ => false, + } + } + fn error(&self, msg: String) -> ParseError { let tok = self.current_token(); ParseError { @@ -926,13 +980,12 @@ impl Parser { let scrutinee = self.parse_primary()?; self.skip_newlines(); let mut arms = Vec::new(); - while !self.is_at_end() - && !matches!(self.peek(), TokenKind::Let | TokenKind::View | TokenKind::On | TokenKind::Effect - | TokenKind::RBracket | TokenKind::RBrace | TokenKind::Else | TokenKind::Eof) - { + // Match arms: continue while the next token could be a pattern + // (StringFragment, Int, Ident that starts with lowercase, or _ wildcard). + // Stop on anything that can't be a pattern: keywords, brackets, EOF. + while !self.is_at_end() && self.can_be_pattern() { self.skip_newlines(); - if self.is_at_end() || matches!(self.peek(), TokenKind::Let | TokenKind::View | TokenKind::On | TokenKind::Effect - | TokenKind::RBracket | TokenKind::RBrace | TokenKind::Else | TokenKind::Eof) { + if self.is_at_end() || !self.can_be_pattern() { break; } let pattern = self.parse_pattern()?; diff --git a/examples/project-manager.ds b/examples/project-manager.ds index c6400a8..17c1696 100644 --- a/examples/project-manager.ds +++ b/examples/project-manager.ds @@ -50,15 +50,24 @@ route "/" -> column [ -- Project status Card { title: "Status", subtitle: "current phase" } [ match projectStatus - "active" -> Badge { label: "🟢 ACTIVE — Project is on track", variant: "success" } - "review" -> Badge { label: "🟡 IN REVIEW — Awaiting approval", variant: "warning" } - "paused" -> Badge { label: "🔴 PAUSED — Project on hold", variant: "error" } + "active" -> row [ + Badge { label: "🟢 ACTIVE", variant: "success" } + text "Project is on track" + ] + "review" -> row [ + Badge { label: "🟡 IN REVIEW", variant: "warning" } + text "Awaiting approval" + ] + "paused" -> row [ + Badge { label: "🔴 PAUSED", variant: "error" } + text "Project on hold" + ] _ -> Badge { label: "UNKNOWN", variant: "info" } - ] - row [ - Button { label: "Active", onClick: projectStatus = "active", variant: "primary" } - Button { label: "Review", onClick: projectStatus = "review", variant: "secondary" } - Button { label: "Pause", onClick: projectStatus = "paused", variant: "ghost" } + row [ + Button { label: "Active", onClick: projectStatus = "active", variant: "primary" } + Button { label: "Review", onClick: projectStatus = "review", variant: "secondary" } + Button { label: "Pause", onClick: projectStatus = "paused", variant: "ghost" } + ] ] -- Progress