Skip to content

Commit

Permalink
Let method_missing also generate a def
Browse files Browse the repository at this point in the history
  • Loading branch information
asterite committed Dec 1, 2016
1 parent 135fc1c commit 6b098eb
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 22 deletions.
14 changes: 14 additions & 0 deletions spec/compiler/codegen/method_missing_spec.cr
Expand Up @@ -367,4 +367,18 @@ describe "Code gen: method_missing" do
Foo.new(Wrapped.new).foo(1, 2, 3)
)).to_i.should eq(6)
end

it "does method_missing generating method" do
run(%(
class Foo
macro method_missing(call)
def {{call.name}}
{{call.name.stringify}}
end
end
end
Foo.new.bar
)).to_string.should eq("bar")
end
end
33 changes: 33 additions & 0 deletions spec/compiler/semantic/method_missing_spec.cr
Expand Up @@ -40,4 +40,37 @@ describe "Semantic: method_missing" do
Foo(Int32).new.foo
)) { int32 }
end

it "errors if method_missing expands to an incorrect method" do
assert_error %(
class Foo
macro method_missing(call)
def baz
1
end
end
end
Foo.new.bar
),
"wrong method_missing expansion"
end

it "errors if method_missing expands to multiple methods" do
assert_error %(
class Foo
macro method_missing(call)
def bar
1
end
def qux
end
end
end
Foo.new.bar
),
"wrong method_missing expansion"
end
end
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/call.cr
Expand Up @@ -234,7 +234,7 @@ class Crystal::Call
end

if matches.empty?
defined_method_missing = owner.check_method_missing(signature)
defined_method_missing = owner.check_method_missing(signature, self)
if defined_method_missing
matches = owner.lookup_matches(signature)
end
Expand Down
93 changes: 72 additions & 21 deletions src/compiler/crystal/semantic/method_missing.cr
Expand Up @@ -4,18 +4,18 @@ module Crystal
class Type
ONE_ARG = [Arg.new("a1")]

def check_method_missing(signature)
def check_method_missing(signature, call)
if !metaclass? && signature.name != "initialize"
# Make sure to define method missing in the whole hierarchy
virtual_type = virtual_type()
if virtual_type == self
method_missing = lookup_method_missing
if method_missing
define_method_from_method_missing(method_missing, signature)
define_method_from_method_missing(method_missing, signature, call)
return true
end
else
return virtual_type.check_method_missing(signature)
return virtual_type.check_method_missing(signature, call)
end
end

Expand All @@ -35,7 +35,7 @@ module Crystal
nil
end

def define_method_from_method_missing(method_missing, signature)
def define_method_from_method_missing(method_missing, signature, original_call)
name_node = StringLiteral.new(signature.name)
args_nodes = [] of ASTNode
args_nodes_names = Set(String).new
Expand All @@ -62,22 +62,63 @@ module Crystal
fake_call = Call.new(nil, "method_missing", [call] of ASTNode)

expanded_macro = program.expand_macro method_missing, fake_call, self, self
generated_nodes = program.parse_macro_source(expanded_macro, method_missing, method_missing, args_nodes_names) do |parser|
parser.parse_to_def(a_def)
end

a_def.body = generated_nodes
a_def.yields = block.try &.args.size
# Check if the expanded macro is a def. We do this
# by just lexing the result and seeing if the first
# token is `def`
expands_to_def = starts_with_def?(expanded_macro)
generated_nodes =
program.parse_macro_source(expanded_macro, method_missing, method_missing, args_nodes_names) do |parser|
if expands_to_def
parser.parse
else
parser.parse_to_def(a_def)
end
end

if generated_nodes.is_a?(Def)
a_def = generated_nodes
else
if expands_to_def
raise_wrong_method_missing_expansion(
"it should only expand to a single def",
expanded_macro,
original_call)
end

a_def.body = generated_nodes
a_def.yields = block.try &.args.size
end

owner = self
owner = owner.base_type if owner.is_a?(VirtualType)

if owner.is_a?(ModuleType)
owner.add_def(a_def)
true
else
false
return false unless owner.is_a?(ModuleType)

owner.add_def(a_def)

# If it expanded to a def, we check if the def
# is now found by regular lookup. It should!
# Otherwise there's a mistake in the macro.
if expands_to_def && owner.lookup_matches(signature).empty?
raise_wrong_method_missing_expansion(
"the generated method won't be found by the original call invocation",
expanded_macro,
original_call)
end

true
end

private def raise_wrong_method_missing_expansion(msg, expanded_macro, original_call)
str = String.build do |io|
io << "wrong method_missing expansion\n\n"
io << "The method_missing macro expanded to:\n\n"
io << Crystal.with_line_numbers(expanded_macro)
io << "\n\n"
io << "However, " << msg
end
original_call.raise str
end
end

Expand All @@ -86,18 +127,18 @@ module Crystal
end

class VirtualType
def check_method_missing(signature)
def check_method_missing(signature, call)
method_missing = base_type.lookup_method_missing
defined = false
if method_missing
defined = base_type.define_method_from_method_missing(method_missing, signature) || defined
defined = base_type.define_method_from_method_missing(method_missing, signature, call) || defined
end

defined = add_subclasses_method_missing_matches(base_type, method_missing, signature) || defined
defined = add_subclasses_method_missing_matches(base_type, method_missing, signature, call) || defined
defined
end

def add_subclasses_method_missing_matches(base_type, method_missing, signature)
def add_subclasses_method_missing_matches(base_type, method_missing, signature, call)
defined = false

base_type.subclasses.each do |subclass|
Expand All @@ -111,19 +152,29 @@ module Crystal

# Check if the subclass redefined the method_missing
if subclass_method_missing && subclass_method_missing.object_id != method_missing.object_id
subclass.define_method_from_method_missing(subclass_method_missing, signature)
subclass.define_method_from_method_missing(subclass_method_missing, signature, call)
defined = true
elsif method_missing
# Otherwise, we need to define this method missing because of macro vars like @name
subclass.define_method_from_method_missing(method_missing, signature)
subclass.define_method_from_method_missing(method_missing, signature, call)
subclass_method_missing = method_missing
defined = true
end

defined = add_subclasses_method_missing_matches(subclass, subclass_method_missing, signature) || defined
defined = add_subclasses_method_missing_matches(subclass, subclass_method_missing, signature, call) || defined
end

defined
end
end
end

private def starts_with_def?(source)
lexer = Crystal::Lexer.new(source)
while true
token = lexer.next_token
return true if token.keyword?(:def)
break if token.type == :EOF
end
false
end

0 comments on commit 6b098eb

Please sign in to comment.