From f960239a8e5b4d1e264f76ec8bddfad5c6684d24 Mon Sep 17 00:00:00 2001 From: Alex van de Sandt Date: Sun, 30 Jun 2024 15:03:20 -0400 Subject: [PATCH] Parse for loops and desguar them to while loops --- Cargo.toml | 4 +- src/ast/stmt.rs | 79 +++++++++++++++++++++++++++++++++++++-- src/interpreter.rs | 10 +++++ src/parser.rs | 92 +++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 170 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0210531..45461e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" publish = false [dependencies] +claims = "0.7.1" clap = { version = "4.5.4", features = ["derive"] } dotenv = "0.15.0" miette = { version = "7.2.0", features = ["fancy"] } @@ -14,6 +15,3 @@ strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.60" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } - -[dev-dependencies] -claims = "0.7.1" diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index a1f655c..8970e6a 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -1,8 +1,11 @@ +use crate::ast::Literal; +use crate::span::Span; use crate::{ ast::Expr, span::Spanned, token::{Token, TokenKind}, }; +use claims::debug_assert_matches; #[derive(Clone, Debug)] pub enum Stmt { @@ -26,7 +29,8 @@ pub enum Stmt { impl Stmt { pub fn expr(inner: Spanned, semi: &Token) -> Spanned { - debug_assert_eq!(semi.kind, TokenKind::Semicolon); + // allow for for loop desugaring + debug_assert_matches!(&semi.kind, TokenKind::Semicolon | TokenKind::RightParen); let span = inner.span().join(&semi.span); Spanned::new(Self::Expr(inner), span) } @@ -58,7 +62,7 @@ impl Stmt { close_brace: &Token, ) -> Spanned { debug_assert_eq!(open_brace.kind, TokenKind::LeftBrace); - debug_assert_eq!(close_brace.kind, TokenKind::RightBrace); + // debug_assert_eq!(close_brace.kind, TokenKind::RightBrace); let span = open_brace.span.join(&close_brace.span); Spanned::new(Self::Block(inner_stmts), span) @@ -93,7 +97,8 @@ impl Stmt { condition: Spanned, body: Spanned, ) -> Spanned { - debug_assert_eq!(while_token.kind, TokenKind::While); + // allow for for-loop desugaring + debug_assert_matches!(&while_token.kind, TokenKind::While | TokenKind::For); let span = while_token.span.join(&body.span()); Spanned::new( @@ -104,6 +109,74 @@ impl Stmt { span, ) } + + /// Construct a for loop by desugaring to a while loop + /// + /// For example, this lox code: + /// ```text + /// for (var i = 0; i < 10; i = i + 1) print i; + /// ``` + /// Should become + /// ```text + /// { + /// var i = 0; + /// while (i < 10) { + /// print i; + /// i = i + 1; + /// } + /// } + /// ``` + pub fn for_stmt( + for_token: &Token, + left_paren: &Token, + initializer: Option>, + condition: Option>, + increment: Option<(Spanned, Token)>, + body: Spanned, + ) -> Spanned { + let open_brace = Token { + kind: TokenKind::LeftBrace, + span: Span::new(body.span().start(), body.span().start() + 1), + _line: 0, + }; + let close_brace = Token { + kind: TokenKind::LeftBrace, + span: Span::new(body.span().end(), body.span().end() + 1), + _line: 0, + }; + + let mut inner_stmts = vec![body]; + if let Some((increment, right_paren)) = increment { + let increment = Self::expr(increment, &right_paren); + inner_stmts.push(increment) + } + + let while_body = Self::block(&open_brace, inner_stmts, &close_brace); + + // default to `true` for the while loop condition + let while_condition = condition + .unwrap_or_else(|| Expr::literal(Spanned::new(Literal::Bool(true), left_paren.span))); + + // construct the while loop + let while_loop = Self::while_stmt(for_token, while_condition, while_body); + + // wrap the initializer and the loop in a block + let open_brace = Token { + kind: TokenKind::LeftBrace, + span: Span::new(while_loop.span().start(), while_loop.span().start() + 1), + _line: 0, + }; + let close_brace = Token { + kind: TokenKind::LeftBrace, + span: Span::new(while_loop.span().end(), while_loop.span().end() + 1), + _line: 0, + }; + + let mut stmts = initializer.map(|i| vec![i]).unwrap_or_default(); + stmts.push(while_loop); + + Self::block(&open_brace, stmts, &close_brace) + } } #[cfg(test)] diff --git a/src/interpreter.rs b/src/interpreter.rs index 8b178b0..9a8f416 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -431,6 +431,16 @@ mod test { assert_eq!(output, "body"); } + #[test] + fn executes_for() { + // This is desguared to a while loop + let (_, output) = execute_stmts("for (var i = 0; i < 10; i = i + 1) print i;"); + assert_eq!( + output.lines().collect::>(), + ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + ) + } + #[test] fn child_env_shadows_parent() { let (e, out) = execute_stmts("var foo = 1; { print foo; } { var foo = 2; }"); diff --git a/src/parser.rs b/src/parser.rs index 6d6c930..f8ec021 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -41,7 +41,7 @@ impl Parser { let mut errors = Vec::new(); while !self.at_end() { - match self.parse_decl() { + match self.parse_decl_or_pass() { Ok(s) => statements.push(s), Err(e) => { errors.push(e); @@ -61,11 +61,15 @@ impl Parser { /// Statement parsing impl Parser { #[tracing::instrument(name = "decl", skip_all)] - fn parse_decl(&mut self) -> Result> { - let Some(var) = self.pop_if_matches(MatchToken::Var) else { - return self.parse_block_or_pass(); - }; + fn parse_decl_or_pass(&mut self) -> Result> { + match self.pop_if_matches(MatchToken::Var) { + Some(var) => self.parse_decl_inner(var), + None => self.parse_block_or_pass(), + } + } + /// Parse everything after `var` in a `VarDecl` statement + fn parse_decl_inner(&mut self, var_token: Token) -> Result> { let name = self .pop_if_map(|t| match t.kind { TokenKind::Ident { name } => Some(name), @@ -85,7 +89,7 @@ impl Parser { let semi = self .pop_if_matches(MatchToken::Semicolon) .ok_or_else(|| ParserError::missing_semicolon(self.previous()))?; - Ok(Stmt::var_decl(&var, name, initializer, &semi)) + Ok(Stmt::var_decl(&var_token, name, initializer, &semi)) } #[tracing::instrument(name = "block", skip_all)] @@ -137,7 +141,7 @@ impl Parser { #[tracing::instrument(name = "while", skip_all)] fn parse_while_or_pass(&mut self) -> Result> { let Some(while_token) = self.pop_if_matches(MatchToken::While) else { - return self.parse_print_or_expr_stmt(); + return self.parse_for_or_pass(); }; self.pop_if_matches(MatchToken::LeftParen).ok_or_else(|| { @@ -155,6 +159,61 @@ impl Parser { Ok(Stmt::while_stmt(&while_token, condition, body)) } + #[tracing::instrument(name = "for", skip_all)] + fn parse_for_or_pass(&mut self) -> Result> { + let Some(for_token) = self.pop_if_matches(MatchToken::For) else { + return self.parse_print_or_expr_stmt(); + }; + + let left_paren = self.pop_if_matches(MatchToken::LeftParen).ok_or_else(|| { + ParserError::expected_token(TokenKind::LeftParen, self.peek().as_ref()) + })?; + + let initializer = if self.pop_if_matches(MatchToken::Semicolon).is_some() { + None + } else if let Some(var) = self.pop_if_matches(MatchToken::Var) { + // requires and consumes a semicolon + Some(self.parse_decl_inner(var)?) + } else { + let expr = self.parse_expr()?; + let semi = self + .pop_if_matches(MatchToken::Semicolon) + .ok_or_else(|| ParserError::missing_semicolon(self.previous()))?; + Some(Stmt::expr(expr, &semi)) + }; + + let condition = if self.pop_if_matches(MatchToken::Semicolon).is_some() { + None + } else { + let expr = self.parse_expr()?; + self.pop_if_matches(MatchToken::Semicolon) + .ok_or_else(|| ParserError::missing_semicolon(self.previous()))?; + Some(expr) + }; + + let increment = if self.pop_if_matches(MatchToken::RightParen).is_some() { + None + } else { + let expr = self.parse_expr()?; + let right_paren = self.pop_if_matches(MatchToken::RightParen).ok_or_else(|| { + ParserError::expected_token(TokenKind::RightParen, self.peek().as_ref()) + })?; + Some((expr, right_paren)) + }; + + let body = self.parse_block_or_pass()?; + + // desugar to a while loop wrapped in a block + Ok(Stmt::for_stmt( + &for_token, + &left_paren, + initializer, + condition, + increment, + body, + )) + } + /// Parse the body of a block into a group of statements. This may be a bare block statement or /// part of another piece of syntax, such as a while statement. /// @@ -172,7 +231,7 @@ impl Parser { return Err(ParseBlockError::ReachedEnd { last_stmt_span }); } - let stmt = self.parse_decl()?; + let stmt = self.parse_decl_or_pass()?; inner_stmts.push(stmt); }; @@ -650,7 +709,22 @@ mod tests { } #[test] - fn dangling_else_attches_to_nearest_if() { + fn desugars_for_to_while() { + let stmt = parse_stmt("for (var i = 0; i < 10; i = i + 1) print i;"); + let stmts = stmt.unwrap_block(); + assert_eq!(stmts.len(), 2); + assert_eq!(stmts[0].unwrap_var_decl(), "i"); + + let (condition, body) = stmts[1].unwrap_while(); + assert_matches!(condition, Expr::Binary { .. }); + let inner_stmts = body.unwrap_block(); + assert_eq!(inner_stmts.len(), 2); + assert_matches!(inner_stmts[0].as_ref(), Stmt::Print(_)); + assert_matches!(inner_stmts[1].as_ref(), Stmt::Expr(_)); + } + + #[test] + fn dangling_else_attaches_to_nearest_if() { let stmt = parse_stmt("if (a) if (b) true; else false;"); let (_, inner_if_else) = stmt.unwrap_if(); inner_if_else.unwrap_if_else(); -- 2.51.2