-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NamedArgument.name == NamedArgument.value in method_missing #10381
Comments
I've though of the value of the named argument as a variable reference to the real value, but thinking about it now, there's no difference between the name and the value in this case. The only difference I've seen is that if the named arg is like: |
With macros, you're typically getting an expression (rather than a value) rendered in-place into the macro's call site. You can see similar behavior with instance variable default values. class Person
@name : String = pp "Jamie"
end
3.times { Person.new }
# "Jamie"
# "Jamie"
# "Jamie" This isn't happening inside of a method call that we wrote, it's just assigning This is why this seems like a bug. I'd expect the macro foo(call)
pp {{call.named_args.first.value}}
end
foo(not_even_a_real_method(foo: "bar")) # "bar" The expression passed to the |
When the compiler invokes the class Foo
macro method_missing(call)
{% p call %}
end
end
Foo.new.foo(1, 'a', x: "") { |f, g| }
Foo.new.foo(1, 'a', x: "") { |f, g| } # prints nothing because this shape has been handled
Foo.new.foo(2, 'a')
class Thing
macro method_missing(call)
{% arg = call.named_args.first %}
"{{arg.name}} - #{ {{arg.value}} }"
end
end
Thing.new.omg(foo: "bar") # => "foo - bar" |
@HertzDevil is right here. class Thing
macro method_missing(call)
{{ puts call }}
end
end
thing = Thing.new
thing.omg(foo: "bar")
thing.omg(foo: "baz")
thing.omg(foo: "qux") This will This is done to prevent creating one method per actual call. Alternatively the call could be inlined and no method would be generated, but that's not how it works right now, and it's a bit harder to implement. That said, I've been wanting to remove So in Lucky I would try to avoid |
=> #4701 |
@asterite Removing sth that provides value just because you can't think of a way to fix the quirks sounds IMO like a wrong reason to do so. |
@Sija Okay. Then someone needs to come up with a way to implement method missing. If nobody comes up with that then it will be forever broken. I usually like to remove broken things and reintroduce them once they work correctly. Because if it turns out that there's no way to fix it, then at least it's not broken in the first place (because the feature is not there). |
To put things in perspective: in the beginning, as we saw the language was actually working we experimented with how much Ruby we could get out of it. "Can we have something like method_missing?". So we came up with something. But we actually never needed that. And when coding in Ruby I personally don't use Over the time we found many bugs and issues with it and I tried to fix those. But it's never enough. The point is, we have to maintain a feature that just happens to exist because of experimentation, not actual need. |
Totally agree with Ary and put my vote to remove such features. IMHO such features are more abused then normal used for experimentation, people with strong scripting or dynamic languages background utilizes such methods/hacks to treat or expect language to behave in similar fashion they were used to. I understand Ruby has very strong influence on design of Crystal, but Crystal ain't Ruby. |
It sounds like class Thing
macro method_missing(call)
p self
end
end
# My mental model of it was that this line:
Thing.new.foo("bar")
# ... would compile to something resembling this:
with Thing.new do
p self
end This way there would be no new methods created at all. It'd just be a macro like other macros (minus the part where it's executed in the context of the original receiver). |
Is there any reason it couldn't compile to that? |
@jgaskins Yes: instance variables don't work in a |
Also I think it could be implemented with something like that, it's not clear to me how. When the compiler tries to resolve a call the resolution is a method. If a method doesn't exist it hits |
Whoa, TIL. I've only maybe used
I feel like that would be fair. Other Crystal macros do the same. For example, a template rendered by
It's definitely a tricky concept with a lot of nuance. And I get that it's difficult to pin down, but when it comes to proxying other objects (for example, a client with a connection pool passing calls along to an individual connection), In the file at the link above, I could probably have used the mixin architecture I already use to distinguish between serial / pipelined / transactional execution, but |
I think the majority of the use cases for |
We should probably focus on that before the discussion about removing |
Sth along the lines of #9927 perhaps? |
@HertzDevil In the forum post, @matthewmcgarvey shared some code, that fails to compile when using this technique. However, the compilation error isn't what I expected, and I think that's part of what's leading to some confusion. Reduced: class Foo
macro method_missing(call)
"{{call.named_args.first.name}} = #{ {{call.named_args.first.value}} }"
end
end
p Foo.new.foo(a: 1) # => "a = 1"
p Foo.new.foo("a": 1) # => "a = 1"
p Foo.new.foo("@a": 1) # Compilation Error:
# ...
# > 1 | "@a = #{ @a }"
# ^-
# Error: can't infer the type of instance variable '@a' of Foo
#
# The type of a instance variable, if not declared explicitly with
# ... The key part, I think, is that it's taking a NamedTuple that has a key of (Total aside, is it possible to expand a method_missing with EDIT: There's lots of fun things to play with - |
A rough patch like this: diff --git a/src/compiler/crystal/semantic/method_lookup.cr b/src/compiler/crystal/semantic/method_lookup.cr
index 60eb553a2..6fb7a3991 100644
--- a/src/compiler/crystal/semantic/method_lookup.cr
+++ b/src/compiler/crystal/semantic/method_lookup.cr
@@ -52,6 +52,10 @@ require "../types"
module Crystal
record NamedArgumentType, name : String, type : Type do
+ def initialize(@name : String, @type : Type)
+ raise "Invalid NamedArgumentType" unless @name =~ /\A[a-zA-Z0-9_]+\z/
+ end
+
def self.from_args(named_args : Array(NamedArgument)?, with_literals = false)
named_args.try &.map do |named_arg|
new(named_arg.name, named_arg.value.type(with_literals: with_literals)) allows the "good" values to go through, while blowing up when the "bad", non-identifier, is passed. I'm not sure how to code up a validation like this in an elegant way, however, such that it doesn't appear like a compiler error. |
Wouldn't that raise an error when a double splat is used on a normal method? def foo(**named_args)
pp named_args
end
foo("@a": 1) |
Yes...like I said it's a "rough patch" 😄 |
Less invasive: diff --git a/src/compiler/crystal/semantic/method_missing.cr b/src/compiler/crystal/semantic/method_missing.cr
index ceb95571a..e79b39faa 100644
--- a/src/compiler/crystal/semantic/method_missing.cr
+++ b/src/compiler/crystal/semantic/method_missing.cr
@@ -40,6 +40,8 @@ module Crystal
if named_args = signature.named_args
args_nodes_names << ""
named_args.each do |named_arg|
+ raise "Invalid NamedArgument name: #{named_arg.name}" unless named_arg.name =~ /\A[a-zA-Z0-9_]+\z/
+
named_args_nodes ||= [] of NamedArgument
named_args_nodes << NamedArgument.new(named_arg.name, MacroId.new(named_arg.name))
args_nodes_names << named_arg.name EDIT: Alternatively, perhaps it's more elegant to put the validation in |
The fix here is also simple: use crystal/spec/compiler/codegen/method_missing_spec.cr Lines 385 to 396 in 8ae10e8
crystal/spec/compiler/codegen/method_missing_spec.cr Lines 398 to 410 in 8ae10e8
Support for named arguments as a whole was originally implemented in #3654, but IMO we really shouldn't be allowing class Foo
getter to = 1
forward_missing_to to
end
# forward_missing_to expands to `to.upto(to: to)`
# method lookup finds Int#upto(to) because the parameter name matches
# the first and last `to` refer to the call's named argument, which is `3`
# a true delegation would have ultimately called `1.upto(3)` instead
Foo.new.upto(to: 3).to_a # => [3] class Foo
macro method_missing(call)
self
end
end
Foo.new.foo(self: 1)
# Module validation failed: Function return type does not match operand type of return inst!
# ret %Foo* %56, !dbg !75
# i32 (Exception)
# from /crystal/src/int.cr:528:9 in 'finish'
# from /crystal/src/compiler/crystal/codegen/codegen.cr:69:7 in 'codegen'
# from /crystal/src/compiler/crystal/compiler.cr:172:16 in 'compile'
# from /crystal/src/compiler/crystal/command.cr:210:5 in 'run_command'
# from /crystal/src/compiler/crystal/command.cr:117:7 in 'run'
# from /crystal/src/compiler/crystal.cr:11:1 in '__crystal_main'
# from /crystal/src/crystal/main.cr:110:5 in 'main'
# from src/env/__libc_start_main.c:94:2 in 'libc_start_main_stage2' Instead, the real arguments must be referred to using |
To further elaborate, when we do class A
macro method_missing(call)
x + y
end
end
A.new.b(x: 1, y: 2) The compiler should behave as if the following def is emitted: class A
def b(*, x _named_arg0, y _named_arg1)
x + y
end
end
class A
macro method_missing(call)
{{call.named_args[0].value}} + {{call.named_args[1].value}}
end
end It will be expanded to: class A
def b(*, x _named_arg0, y _named_arg1)
_named_arg0 + _named_arg1
end
end |
would still produce invalid code, though, right? Something like class A
def b(*, @x _named_arg0, y _named_arg1)
_named_arg0 + _named_arg1
end
end |
Quoted strings are allowed as external names: def b(*, "@x" _named_arg0, "Y" _named_arg1)
_named_arg0 + _named_arg1
end
b("@x": 1, Y: 2) # => 3 But it's not important here; Besides, if one has to use |
I had no idea param names could be enquoted like that...that's really cool. |
Actually, if we have: class A
def b(*, x _named_arg0, y _named_arg1)
x + y
end
macro method_missing(call)
x + y
end
end then class A
def x
x + y
end
def y
x + y
end
end It gives a runtime stack overflow, but there really isn't anything to do about this since class A
def method_missing(m, *args, **kwargs, &block)
# first call: m = :b, args = [], kwargs = {:x => 1, :y => 2}
# subsequent calls: m = :x, args = [], kwargs = {}
x + y
end
end
A.new.b(x: 1, y: 2) # SystemStackError (stack level too deep) |
The expected output here is
"[foo: \"bar\"]"
, but instead it's"[foo: foo]"
. It seems like bothname
andvalue
onCrystal::Macros::NamedArgument
are returning the same value. You can see that in the following code:The text was updated successfully, but these errors were encountered: