Add #nilable? to more macro types

Currently it is a major pain to figure out if a method arg’s restriction is nilable or not since it could be a multitude of different types.

https://play.crystal-lang.org/#/r/78lk

It would be super helpful if additional types had a .nilable? that would return true if the type includes nil. Which would be like:

  • Union.types.any? &.==(::Nil)
  • Generic.type_vars.any? &.==(::Nil)
  • Path.resolve.nilable?
1 Like

@asterite I been looking into implementing this, however im having some trouble with the Union and Generic.

interpret_argless_method(method, args) { BoolLiteral.new(types.any? &.==(::Nil)) } It seems the type of each “type” is ASTNode, thus doesn’t match the check or anything so i had to do like.

BoolLiteral.new(
  types.any? do |t|
    interpreter.resolve(t.as(Path)).as(TypeNode).type.nilable?
  end
)

I think ideally these would live on the ASTNodes themselves where each type implements the method as needed. Do you have any thoughts/ideas about this?

Your first snippet works almost perfectly except for the Union case, if you replace decl.type.nilable? with decl.type.resolve.nilable?. I think resolve is missing for Union. I’m not sure about adding nilable? to all AST nodes. And for Union I think you just need to interpret resolve for the union members.

(sorry if I misunderstood you)

I would be happy if i could just do that, Currently im doing like:

{% nilable = (type.is_a?(Path) ? type.resolve.nilable? : (type.is_a?(Union) ? type.types.any?(&.resolve.nilable?) : (type.is_a?(Generic) ? type.resolve.nilable? : type.nilable?))) %}

Yea its missing for Union. Would that just be like:

    def resolve(node : Union)
      type = @path_lookup.lookup_type(node, self_type: @scope, free_vars: @free_vars)
      TypeNode.new(type)
    end

    def resolve?(node : Union)
      resolve(node)
    rescue Crystal::Exception
      nil
    end

I dont really know what im doing.

I’ll see if I can do it. But to be honest I’m not sure resolve was a good idea (though apparently it’s used all over the place in Lucky).

1 Like

My main use case for this just to have a common .nilable? method you can call on the various types in macro land. I don’t know what the options are for having something like that, but it would certainly be helpful versus having to do different things depending on what type it is.

I’ll do it later today :+1:

2 Likes

Just heads up that we have removed most resolve calls in Lucky because it can cause annoying issues where you have cyclical requires. So I’d probably avoid that.

And thanks for making the common nilable? we’ve needed that in a number of places in Lucky! Another cool thing would be to have types available on TypeDeclaration even if not a Union. Right now we have to check if the type is a Union and then call types on it, otherwise call type. We’d love to do something like type_declaration.types and have it work for non-Unions too. That would clean up a ton of conditional code

https://play.crystal-lang.org/#/r/78r9

macro what_types(type_declaration)
  {% if type_declaration.type.is_a?(Union) %}
    {% p type_declaration.type.types %}
  {% else %}
    {% p type_declaration.type %}
  {% end %}

  # Would be wonderful if we could do for Union and non-Union
  # But it fails
  {% p type_declaration.type.types %}
end

what_types hi : String | Int32
what_types hi : String

Yeah, I think types returning an array of a single type for a TypeNode makes sense, for working uniformly with types :+1:

1 Like

Done! https://github.com/crystal-lang/crystal/pull/7970

Let me know if I got it right and what you think.

2 Likes