-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Restrict argument types having a default value #3834
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
Restrict argument types having a default value #3834
Conversation
For example: def foo(x : String = 1) x end foo I think above code is invalid, but current compiler accepts it. This commit fixes it. Another effect, below code works fine now: def foo(x : T.class = String) forall T {{ T }} end foo # => String (Before, we got "undefined constant T" error)
27de8e6
to
247407f
Compare
# Fictitious node to represent a type restriction | ||
# | ||
# It is used for type restrection of method arguments. | ||
class TypeRestrict < ASTNode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call this TypeRestriction
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I'll fix.
# | ||
# It is used for type restrection of method arguments. | ||
class TypeRestrict < ASTNode | ||
getter value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call this node
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, if this is node
, we use node.node
in MainVisitor#visit
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TypeRestriction
's semantics looks like Cast
, so I think it is good that these getters rename same as Cast
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good 👍
|
||
value.accept self | ||
|
||
unless context = match_context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking, maybe there's a different way to do this. The problem with this approach is that the type is checked once here (below). But what happens if the type of value
changes later? (rare, but can happen)
Another way to do this is to do something like:
# This line is currently in `visit(node : Path)`, so we could probably extract it to a helper method
restriction_type = (@path_lookup || @scope || @current_type).lookup_type_var(node, free_vars: @free_vars)
unless restriction_type.is_a?(Type)
node.raise "restriction must be a type, not #{restriction_type}"
end
# With this we make the node always have this type, and raise an error if it's not (also after a change)
value.freeze_type = restriction_type
In this way I think there's no need to change the visitor to have a MatchContext
instead of free_vars
.
Could you try this change and see if it works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
freeze_type
looks good. But, I think this way doesn't solve def foo(x : T.class = String)
problem, and fixing this problem needs real call of restrict
method, so match_context
is needed.
The problem with this approach is that the type is checked once here (below). But what happens if the type of value changes later? (rare, but can happen)
Could you show me sample code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the default value is a method call. But don't worry, this case won't be possible in the future, and right now it's actually very to trigger that. I think your implementation is more than good, so let's leave it like that :-)
@makenowjust This is excellent, thank you! I made a comment about a slightly different way of implementing this, but the basic idea you provided is really good. In fact this fixes #2636 |
# | ||
# It is used for type restrection of method arguments. | ||
class TypeRestriction < ASTNode | ||
getter value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@makenowjust Ah, OK. Let's leave it as value
for now :-)
@@ -44,7 +44,7 @@ module Crystal | |||
false | |||
end | |||
|
|||
def visit(node : TypeRestrict) | |||
def visit(node : TypeRestriction) | |||
@str << "# type restrict: " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover typo: restrict
-> restriction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Regarding #3834 (comment), I would allow method calls in default methos so this things still work It make me suspicious that a node (even fictitious one) is added. Could the issue be resolved by enforcing that, when instantiating a def, the default value is_a? value of the type restriction (if any)? |
if type = obj.type.restrict(to, context) | ||
node.type = type | ||
else | ||
node.raise "can't restrict #{obj.type} to #{to}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A spec is missing for this (the specs you added seem to be testing other things)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I just added.
yup, 👍 for keeping method calls in default argument values. class Foo
protected def default_name
preview? && "foo-preview" || "foo"
end
def save(name = default_name)
# ...
end
end |
@makenowjust Sorry, could you rephrase or expand? I don't follow you. |
@bcardiff I don't think all method calls in default value are banned by compiler, so don't worry. |
@@ -171,6 +171,16 @@ describe "Semantic: def" do | |||
"undefined method" | |||
end | |||
|
|||
it "errors when type restriction is incompatible with default value" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rephrase it: errors when default value is incompatible with type restriction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
@makenowjust Thank you for this! |
For example:
I think above code is invalid, but current compiler accepts it.
This pull request fixes it.
Another effect, below code works fine now:
(Before, we got "undefined constant T" error.)
Implementation point is introducing
TypeRestrict
fictitious node and applying it when instantiate a method with arguments having a default value.