Skip to content
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

Merged

Conversation

makenowjust
Copy link
Contributor

@makenowjust makenowjust commented Jan 4, 2017

For example:

def foo(x : String = 1)
  x
end

foo

I think above code is invalid, but current compiler accepts it.
This pull request 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.)

Implementation point is introducing TypeRestrict fictitious node and applying it when instantiate a method with arguments having a default value.

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)
@makenowjust makenowjust force-pushed the fix/crystal/type-restrict-default branch from 27de8e6 to 247407f Compare January 4, 2017 10:51
# Fictitious node to represent a type restriction
#
# It is used for type restrection of method arguments.
class TypeRestrict < ASTNode
Copy link
Member

Choose a reason for hiding this comment

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

Maybe call this TypeRestriction?

Copy link
Contributor Author

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
Copy link
Member

Choose a reason for hiding this comment

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

Maybe call this node?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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 :-)

@asterite
Copy link
Member

asterite commented Jan 4, 2017

@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
Copy link
Member

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: "
Copy link
Contributor

@Sija Sija Jan 4, 2017

Choose a reason for hiding this comment

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

leftover typo: restrict -> restriction

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

@makenowjust makenowjust changed the title Restrict arguments having a default value Restrict argument types having a default value Jan 4, 2017
@bcardiff
Copy link
Member

bcardiff commented Jan 4, 2017

Regarding #3834 (comment), I would allow method calls in default methos so this things still work def foo(seed = T.default).

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}"
Copy link
Member

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I just added.

@Sija
Copy link
Contributor

Sija commented Jan 4, 2017

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
Copy link
Contributor Author

makenowjust commented Jan 5, 2017

@bcardiff @Sija I think all method calls in default value aren't denied, and some pathological method calls are denied (I considered such an example, but I can't imagine it.)

@bcardiff
Copy link
Member

bcardiff commented Jan 5, 2017

@makenowjust Sorry, could you rephrase or expand? I don't follow you.

@makenowjust
Copy link
Contributor Author

@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
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

@asterite
Copy link
Member

asterite commented Jan 6, 2017

@makenowjust Thank you for this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants