Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions spec/compiler/macro/macro_methods_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2822,6 +2822,63 @@ module Crystal
assert_macro %({{compare_versions("1.10.3", "1.2.3")}}), %(1)
end

describe "#parse_type" do
it "path" do
assert_type(%[class Bar; end; {{ parse_type("Bar").is_a?(Path) ? 1 : 'a'}}]) { int32 }
assert_type(%[class Bar; end; {{ parse_type(:Bar.id.stringify).is_a?(Path) ? 1 : 'a'}}]) { int32 }
end

it "generic" do
assert_type(%[class Foo(A, B); end; {{ parse_type("Foo(Int32, String)").resolve.type_vars.size == 2 ? 1 : 'a' }}]) { int32 }
end

it "union - |" do
assert_type(%[class Foo; end; class Bar; end; {{ parse_type("Foo|Bar").resolve.union_types.size == 2 ? 1 : 'a' }}]) { int32 }
end

it "union - Union" do
assert_type(%[class Foo; end; class Bar; end; {{ parse_type("Union(Foo,Bar)").resolve.union_types.size == 2 ? 1 : 'a' }}]) { int32 }
end

it "union - in generic" do
assert_type(%[{{ parse_type("Array(Int32 | String)").resolve.type_vars[0].union_types.size == 2 ? 1 : 'a' }}]) { int32 }
end

it "proc" do
assert_type(%[{{ parse_type("String, Int32 -> Bool").inputs.size == 2 ? 1 : 'a' }}]) { int32 }
assert_type(%[{{ parse_type("String, Int32 -> Bool").output.resolve == Bool ? 1 : 'a' }}]) { int32 }
end

it "metaclass" do
assert_type(%[{{ parse_type("Int32.class").resolve == Int32.class ? 1 : 'a' }}]) { int32 }
assert_type(%[{{ parse_type("Int32").resolve == Int32.instance ? 1 : 'a' }}]) { int32 }
end

it "raises on empty string" do
expect_raises(Crystal::TypeException, "argument to parse_type cannot be an empty value") do
assert_macro %({{parse_type ""}}), %(nil)
end
end

it "raises on extra unparsed tokens before the type" do
expect_raises(Crystal::TypeException, %(Invalid type name: "100Foo")) do
assert_macro %({{parse_type "100Foo" }}), %(nil)
end
end

it "raises on extra unparsed tokens after the type" do
expect_raises(Crystal::TypeException, %(Invalid type name: "Foo(Int32)100")) do
assert_macro %({{parse_type "Foo(Int32)100" }}), %(nil)
end
end

it "raises on non StringLiteral arguments" do
expect_raises(Crystal::TypeException, "argument to parse_type must be a StringLiteral, not SymbolLiteral") do
assert_macro %({{parse_type :Foo }}), %(nil)
end
end
end

describe "printing" do
it "puts" do
String.build do |io|
Expand Down
29 changes: 29 additions & 0 deletions src/compiler/crystal/macros.cr
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,35 @@ module Crystal::Macros
def host_flag?(name) : BoolLiteral
end

# Parses `self` into a `Path` or `Generic` (also used for unions).
Comment thread
Blacksmoke16 marked this conversation as resolved.
Outdated
# The `Path#resolve` or `Generic#resolve` method could then be used
# to resolve the value into a `TypeNode`, if the *type_name* represents a type,
# otherwise the value of the constant.
#
# A compile time error is raised if the type/constant does not actually exist,
# or if a required generic argument was not provided.
#
# ```
# class Foo; end
#
# struct Some::Namespace::Foo; end
#
# module Bar(T); end
#
# MY_CONST = 1234
#
# {{ parse_type("Foo").resolve.class? }} # => true
# {{ parse_type("Some::Namespace::Foo").resolve.struct? }} # => true
# {{ parse_type("Foo|Some::Namespace::Foo").resolve.union_types.size }} # => 2
# {{ parse_type("Bar(Int32)|Foo").resolve.union_types[0].type_vars.size }} # => 1
# {{ parse_type("MY_CONST").resolve }} # => 1234
#
# {{ parse_type("MissingType").resolve }} # => Error: undefined constant MissingType
# {{ parse_type("UNKNOWN_CONST").resolve }} # => Error: undefined constant UNKNOWN_CONST
Comment thread
Blacksmoke16 marked this conversation as resolved.
# ```
def parse_type(type_name : StringLiteral) : Path | Generic
Comment thread
Blacksmoke16 marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this comment:

#11126 (comment)

it might be better to say that this returns ASTNode.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, thoughts on just adding them to the union to be more clear what it could return?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a minor thing, so we can merge this right away and always improve the docs later on.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already had it done, plus addressed your other comment, so I just pushed it up. Should be good to go now.

end

# Prints AST nodes at compile-time. Useful for debugging macros.
def puts(*expressions) : Nop
end
Expand Down
23 changes: 23 additions & 0 deletions src/compiler/crystal/macros/methods.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ module Crystal
interpret_env(node)
when "flag?", "host_flag?"
interpret_flag?(node)
when "parse_type"
interpret_parse_type(node)
when "puts"
interpret_puts(node)
when "p", "pp"
Expand Down Expand Up @@ -156,6 +158,27 @@ module Crystal
end
end

def interpret_parse_type(node)
interpret_check_args_toplevel do |arg|
arg.accept self
type_name = case last = @last
when StringLiteral then last.value
else
arg.raise "argument to parse_type must be a StringLiteral, not #{last.class_desc}"
end

arg.raise "argument to parse_type cannot be an empty value" if type_name.blank?

parser = Crystal::Parser.new type_name
parser.next_token_skip_statement_end
Comment thread
Blacksmoke16 marked this conversation as resolved.
Outdated
type = parser.parse_bare_proc_type
parser.check :EOF
@last = type
rescue ex : Crystal::SyntaxException
arg.raise "Invalid type name: #{type_name.inspect}"
end
end

def interpret_puts(node)
node.args.each do |arg|
arg.accept self
Expand Down