Open HertzDevil opened 2 years ago
Originally, these are the planned generic macro types:
class ArrayLiteral(T); end # T = union of element types
class HashLiteral(K, V); end # K = union of key types, V = union of value types
class TupleLiteral(T); end # T = union of element types
class NamedTupleLiteral(V); end # V = union of value types
I have been thinking if the last two should become variadic, mirroring their normal language counterparts. This makes it possible to declare:
class HashLiteral(K, V)
def to_a : ArrayLiteral(TupleLiteral(K, V)); end
# without variadic type vars: `ArrayLiteral(TupleLiteral(K | V))`
# current restriction: `ArrayLiteral(TupleLiteral)`
end
Instead of making TupleLiteral
variadic, an alternative is to make Tuple
a variadic subtype of TupleLiteral
. Then Tuple(*T)
is a subtype of TupleLiteral(Union(*T))
. This approach supports tuple literals too, because they always expand to ::Tuple(...)
:
x = {1, 'a'}
x.class_name # => "TupleLiteral"
x.is_a?(TupleLiteral) # => true
x.is_a?(Tuple) # => true
x.is_a?(TupleLiteral(NumberLiteral | CharLiteral)) # => true
x.is_a?({NumberLiteral, CharLiteral}) # => true
x << 2
x.is_a?(TupleLiteral(NumberLiteral | CharLiteral)) # => true
x.is_a?({NumberLiteral, CharLiteral}) # => false
This is similar to how tuples are implemented in TypeScript:
let x: [number, boolean] = [1, true];
let y: (number | boolean)[] = x; // okay
x = y; // Type '(number | boolean)[]' is not assignable to type '[number, boolean]'.
// Target requires 2 element(s) but source may have fewer.
and Ruby RBS:
def foo
[1, 'a']
end
def bar
x = []
x << 1
x << 'a'
x
end
# TypeProf generates the following:
class Object
private
def foo: -> [Integer, String]
def bar: -> (Array[Integer | String])
end
The benefits are not immediately obvious, because right now macro types only have an effect within #is_a?
.
As for short forms other than {}
, personally I see little issues expanding T?
to T | NilLiteral | Nop
, and _
to ASTNode
(this makes sense because everything is covariant). The ->
form might be related to ProcNotation
but I haven't figured how they could be potentially used.
these are the planned generic macro types
I have my doubts that these types should be generic at all.
The thing is, these types in macros work in a dynamic way. I'd dare to say, macros in Crystal is the mini-Ruby inside Crystal.
So for example, if we have this:
{% array = [1, "hello"] %}
then one could say that that's either ArrayLiteral
, or ArrayLiteral(IntegerLiteral, StringLiteral)
. But look at this:
{% begin %}
{% array = [1, "hello"] %}
{% puts array[0] + 1 %}
{% puts array[1].size %}
{% end %}
It "compiles" just fine. Because actually there's no compilation or type-checking involved: array[0]
will be an IntegerLiteral
, and array[1]
will be a StringLiteral
. It's not like accessing an element will have the type IntegerLiteral | StringLiteral
and then we can't resolve "size".
So... I think an array literal is not generic.
As a side note... what's the benefit of introducing all of this? I'm sure you have something in mind, but at least it's not explained here.
#is_a?
in the macro language is implemented as a string check:https://github.com/crystal-lang/crystal/blob/ef05e26d6ecb0c7d1f5f0568af76ed001896a601/src/compiler/crystal/macros/interpreter.cr#L520-L525
Crystal::ASTNode#class_desc_is_a?
roughly does the following: it uses some macro magic to obtain the ancestors of the built-in AST node types, extracts their#class_desc
(i.e.#class_name
in the macro language), then checks for membership of theconst_name
argument. ASplat
receiver, for example, translates this toconst_name.in?("Splat", "UnaryExpression", "ASTNode")
. This means#is_a?
is not actually backed by a type system within the macro interpreter. It also means the following fails, because no AST nodes ever have a#class_name
containing spaces:A type system is needed to assign meaning to those type expressions, so that we can reliably use the full set of macro types in
#is_a?
and other places that (may eventually) accept macro type expressions, such as the restrictions in built-in macro method docs, macros, a different kind of macros, and annotations. The following is an attempt to establish this type system:ASTNode
is the root of all macro types. It is also the top type; all other macro types are strict subtypes ofASTNode
. (In contrast,Object
is not truly a top type in the regular language.)ASTNode
, are classes.T
is abstract if no AST node can be constructed that belongs toT
but not to any strict subtype ofT
.ASTNode
and what one would callASTNode+
.sizeof
.ASTNode#class_name
is always that of the uninstantiated generic. Both[1, 2, 3].class_name
and%w(a b c).class_name
will continue to be"ArrayLiteral"
to avoid breakage.ArrayLiteral(T)
denotes an array literal whose element nodes all belong to subtypes ofT
. The documentation doesn't declare this type as such, but a great deal of return value restrictions try to instantiateArrayLiteral
already.ArrayLiteral
is covariant inT
. A part of its hierarchy would look like this:It follows that all array literals belong to
ArrayLiteral(ASTNode)
, and all empty ones belong toArrayLiteral(NoReturn)
.HashLiteral(K, V)
,TupleLiteral(T)
, andNamedTupleLiteral(V)
in a similar fashion, although the docs don't use these notations yet. All these type variables are also covariant.|
is the union operator; the resulting macro type is the smallest set-theoretic union of the operand macro types. It behaves slightly differently from unions in the regular language:NumberLiteral | BoolLiteral
is not reduced toASTNode
.NilLiteral
andNop
.A | B
is a subtype ofC | D
if and only if(A <= C || A <= D) && (B <= C || B <= D)
.And | Or
is a strict subtype ofBinaryOp
, even though the former exhausts the latter.NoReturn
is the empty union of macro types. It is the bottom type; no nodes have this type, and all other macro types are strict supertypes of it. (We needNoReturn
because::raise
has this return value restriction.)TypeNode
as a result of https://github.com/crystal-lang/crystal/issues/8835.self
,typeof
,_
, the single splat operator, and the short-hand notations?
,*
,[N]
,{}
,->
are all unsupported. (Perhapsself
could work in some contexts, but the docs don't use it.)NoReturn
. This means{{ 1.is_a?(Int32) }}
is false, but{{ ([] of Nil).is_a?(Array(Int32)) }}
is true.The description above is mostly complete; it leaves out a definition of the intersection operator, which would be needed by macro type restrictions.
#is_a?
does not need it because the macro language is interpreted and does not rely on type filters. Once we construct this type system, we could refactor#is_a?
into:and
lookup_macro_type
would be readily usable when we need to support macro types outside#is_a?
.