Skip to content

Commit

Permalink
Handle properly stringifying multiline macro expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
Blacksmoke16 committed Dec 21, 2024
1 parent b6b190f commit e6c9050
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 13 deletions.
5 changes: 3 additions & 2 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 %}"
Expand Down
6 changes: 6 additions & 0 deletions spec/compiler/parser/to_s_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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) ::))
Expand Down
18 changes: 13 additions & 5 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2187,24 +2187,32 @@ 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)
@exp.accept visitor
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
Expand Down
10 changes: 8 additions & 2 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 20 additions & 4 deletions src/compiler/crystal/syntax/to_s.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit e6c9050

Please sign in to comment.