Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 *type_name* into a `Path`, `Generic` (also used for unions), `ProcNotation`, or `Metaclass`.
#
# The `#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
# ```
def parse_type(type_name : StringLiteral) : Path | Generic | ProcNotation | Metaclass
end

# Prints AST nodes at compile-time. Useful for debugging macros.
def puts(*expressions) : Nop
end
Expand Down
25 changes: 25 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,29 @@ 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?

begin
parser = Crystal::Parser.new type_name
parser.next_token
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
end

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