From e6c90504ac451bebddedf333e01d4690d4be3bc6 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Fri, 20 Dec 2024 23:29:33 -0500 Subject: [PATCH] Handle properly stringifying multiline macro expressions --- spec/compiler/parser/parser_spec.cr | 5 +++-- spec/compiler/parser/to_s_spec.cr | 6 ++++++ src/compiler/crystal/syntax/ast.cr | 18 +++++++++++++----- src/compiler/crystal/syntax/parser.cr | 10 ++++++++-- src/compiler/crystal/syntax/to_s.cr | 24 ++++++++++++++++++++---- 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr index 897e5bf7060c..a6aa70a37395 100644 --- a/spec/compiler/parser/parser_spec.cr +++ b/spec/compiler/parser/parser_spec.cr @@ -1118,7 +1118,7 @@ module Crystal it_parses "puts {{**1}}", Call.new(nil, "puts", MacroExpression.new(DoubleSplat.new(1.int32))) it_parses "{{a = 1 if 2}}", MacroExpression.new(If.new(2.int32, Assign.new("a".var, 1.int32))) it_parses "{% a = 1 %}", MacroExpression.new(Assign.new("a".var, 1.int32), output: false) - it_parses "{%\na = 1\n%}", MacroExpression.new(Assign.new("a".var, 1.int32), output: false) + it_parses "{%\n a = 1\n%}", MacroExpression.new(Assign.new("a".var, 1.int32), output: false, multiline: true) it_parses "{% a = 1 if 2 %}", MacroExpression.new(If.new(2.int32, Assign.new("a".var, 1.int32)), output: false) it_parses "{% if 1; 2; end %}", MacroExpression.new(If.new(1.int32, 2.int32), output: false) it_parses "{%\nif 1; 2; end\n%}", MacroExpression.new(If.new(1.int32, 2.int32), output: false) @@ -1128,7 +1128,8 @@ module Crystal it_parses "{% unless 1; 2; else 3; end %}", MacroExpression.new(Unless.new(1.int32, 2.int32, 3.int32), output: false) it_parses "{% unless 1\n x\nend %}", MacroExpression.new(Unless.new(1.int32, "x".var), output: false) it_parses "{% x unless 1 %}", MacroExpression.new(Unless.new(1.int32, "x".var), output: false) - it_parses "{%\n1\n2\n3\n%}", MacroExpression.new(Expressions.new([1.int32, 2.int32, 3.int32] of ASTNode), output: false) + it_parses "{%\n x unless 1\n%}", MacroExpression.new(Unless.new(1.int32, "x".var), output: false, multiline: true) + it_parses "{%\n 1\n 2\n 3\n%}", MacroExpression.new(Expressions.new([1.int32, 2.int32, 3.int32] of ASTNode), output: false, multiline: true) assert_syntax_error "{% unless 1; 2; elsif 3; 4; end %}" assert_syntax_error "{% unless 1 %} 2 {% elsif 3 %} 3 {% end %}" diff --git a/spec/compiler/parser/to_s_spec.cr b/spec/compiler/parser/to_s_spec.cr index 86464e197267..bfcce21aee2a 100644 --- a/spec/compiler/parser/to_s_spec.cr +++ b/spec/compiler/parser/to_s_spec.cr @@ -248,6 +248,12 @@ describe "ASTNode#to_s" do expect_to_s "1.//(2, &block)" expect_to_s %({% verbatim do %}\n 1{{ 2 }}\n 3{{ 4 }}\n{% end %}) expect_to_s %({% for foo in bar %}\n {{ if true\n foo\n bar\nend }}\n{% end %}) + expect_to_s "{% a = 1 %}" + expect_to_s "{{ a = 1 }}" + expect_to_s "{%\n 1\n 2\n 3\n%}" + expect_to_s "{%\n 1\n%}" + expect_to_s "{%\n 2 + 2\n%}" + expect_to_s %(asm("nop" ::::)) expect_to_s %(asm("nop" : "a"(1), "b"(2) : "c"(3), "d"(4) : "e", "f" : "volatile", "alignstack", "intel")) expect_to_s %(asm("nop" :: "c"(3), "d"(4) ::)) diff --git a/src/compiler/crystal/syntax/ast.cr b/src/compiler/crystal/syntax/ast.cr index 9ccd8dda1f69..66d18c308845 100644 --- a/src/compiler/crystal/syntax/ast.cr +++ b/src/compiler/crystal/syntax/ast.cr @@ -2187,13 +2187,21 @@ module Crystal end # A macro expression, - # surrounded by {{ ... }} (output = true) - # or by {% ... %} (output = false) + # surrounded by {{ ... }}` (output = true, multiline = false) + # or by `{% ... %}` (output = false, multiline = false) + # + # ``` + # # (output = false, multiline = true) + # {% + # ... + # %} + # ``` class MacroExpression < ASTNode property exp : ASTNode property? output : Bool + property? multiline : Bool - def initialize(@exp : ASTNode, @output = true) + def initialize(@exp : ASTNode, @output = true, @multiline : Bool = false) end def accept_children(visitor) @@ -2201,10 +2209,10 @@ module Crystal end def clone_without_location - MacroExpression.new(@exp.clone, @output) + MacroExpression.new(@exp.clone, @output, @multiline) end - def_equals_and_hash exp, output? + def_equals_and_hash exp, output?, multiline? end # Free text that is part of a macro diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr index 569bbd4d9409..3cf31410b41b 100644 --- a/src/compiler/crystal/syntax/parser.cr +++ b/src/compiler/crystal/syntax/parser.cr @@ -3353,7 +3353,13 @@ module Crystal def parse_macro_control(start_location, macro_state = Token::MacroState.default) location = @token.location - next_token_skip_space_or_newline + next_token_skip_space + multiline = false + + if @token.type.newline? + multiline = true + next_token_skip_space_or_newline + end case @token.value when Keyword::FOR @@ -3440,7 +3446,7 @@ module Crystal exps = parse_expressions @in_macro_expression = false - MacroExpression.new(exps, output: false).at(location).at_end(token_end_location) + MacroExpression.new(exps, output: false, multiline: multiline).at(location).at_end(token_end_location) end def parse_macro_if(start_location, macro_state, check_end = true, is_unless = false) diff --git a/src/compiler/crystal/syntax/to_s.cr b/src/compiler/crystal/syntax/to_s.cr index 4ce9ca7efc43..a6537027ce5e 100644 --- a/src/compiler/crystal/syntax/to_s.cr +++ b/src/compiler/crystal/syntax/to_s.cr @@ -728,13 +728,29 @@ module Crystal end def visit(node : MacroExpression) - @str << (node.output? ? "{{" : "{% ") - @str << ' ' if node.output? + @str << (node.output? ? "{{ " : node.multiline? ? "{%" : "{% ") + + if node.multiline? + newline + @indent += 1 + end + outside_macro do + # If the MacroExpression consists of a single node we need to manually handle appending indent and trailing newline if #multiline? + # Otherwise, the Expressions logic handles that for us + if !node.exp.is_a? Expressions + append_indent + end + node.exp.accept self end - @str << ' ' if node.output? - @str << (node.output? ? "}}" : " %}") + + if node.multiline? + @indent -= 1 + newline if !node.exp.is_a? Expressions + end + + @str << (node.output? ? " }}" : node.multiline? ? "%}" : " %}") false end