-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Properly encode splice hole using PolyFunction #17072
Conversation
dc8cda5
to
9b2571a
Compare
f8144ef
to
79dcb61
Compare
This avoids leaking references to types defined within the quotes when the contents of the holes are extracted from the quote.
79dcb61
to
92ff00a
Compare
@@ -44,12 +45,13 @@ object Splicing: | |||
* contains the quotes with references to all cross-quote references. There are some special rules |
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.
In the description a couple of lines above
(<capturedTerms>*) =>
should probably be
[<capturedTypes>*] => (<capturedTerms>*) =>
@@ -198,10 +200,57 @@ class Splicing extends MacroTransform: | |||
val newTree = transform(tree) | |||
val (refs, bindings) = refBindingMap.values.toList.unzip | |||
val bindingsTypes = bindings.map(_.termRef.widenTermRefExpr) | |||
val methType = MethodType(bindingsTypes, newTree.tpe) | |||
val types = bindingsTypes.collect { | |||
case AppliedType(tycon, List(arg: TypeRef)) if tycon.derivesFrom(defn.QuotedTypeClass) => arg |
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 think a dealias might be needed in case the user defines an alias such as type TInt = Type[Int]
?
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.
These types are created in the SpliceTransformer
. They should always have this shape.
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.
Even if they have the same shape, they might not be referentially equal (they are only referentially equal if they are cached by Uniques.scala, but not all types are cacheable).
@@ -198,10 +200,57 @@ class Splicing extends MacroTransform: | |||
val newTree = transform(tree) | |||
val (refs, bindings) = refBindingMap.values.toList.unzip | |||
val bindingsTypes = bindings.map(_.termRef.widenTermRefExpr) | |||
val methType = MethodType(bindingsTypes, newTree.tpe) | |||
val types = bindingsTypes.collect { |
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.
There's a lot of types involved in this code :), maybe we can use a more precise name like quotedTypes
?
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.
Renamed to capturedTypes
.
spliceOwner, | ||
UniqueName.fresh(tpe.symbol.name.toTypeName), | ||
Param, | ||
TypeBounds.empty |
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.
Shouldn't the type bounds of the type params match the type bounds of the original type for the encoding to always typecheck?
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.
Seems that I am missing something. I will add some test cases and update these bounds.
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.
Speaking of bounds, if types can refer to terms in their bounds (val x = ...; type U <: x.T; ${ x.foo: U }
) then the encoding where types always appear before terms wouldn't work if we need to also store these bounds.
* (q: Quotes) ?=> f('{ g(${x1$}, ${x2$}) }) | ||
* {{{ 0 | T3 | U, x1, x2 | | ||
* [U$1] => (U$2: Type[U$1], x1$: Expr[T], x2$: Expr[U$1]) => // body of this lambda does not contain references to U, x1 or x2 | ||
* (q: Quotes) ?=> f('{ @SplicedType type U$3 = [[[ 0 | U$2 | | U$1 ]]]; g[U$3](${x1$}, ${x2$}) }) |
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.
Is the [[[ ... ]]] syntax used here documented somewhere?
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.
Not really. It is the syntax defined in the RefinedPrinter
. I plan to update this syntax in #17144.
@@ -198,10 +200,57 @@ class Splicing extends MacroTransform: | |||
val newTree = transform(tree) | |||
val (refs, bindings) = refBindingMap.values.toList.unzip | |||
val bindingsTypes = bindings.map(_.termRef.widenTermRefExpr) | |||
val methType = MethodType(bindingsTypes, newTree.tpe) |
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.
The code example in the doc comment above SpliceTransformer also needs to be updated to use a polymorphic function.
case tp: TypeRef if tp.typeSymbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) => tp | ||
case tp: TypeRef => | ||
typeIndex.get(tp) match | ||
case Some(idx) => newTypeParams(idx).typeRef |
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.
Having a Map where the keys are Types might be a little bit fragile since we could have two equivalent Types which are not referentially equal, is this not a problem here?
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.
These should only contain TypeRef
s (and TermRef
s after some other fix). This worked in the tests I have. As far as I can see, both of those are cached and should have referential equality.
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 think the prefix of a TypeRef could be an uncacheable type, for example if it contains a LazyRef
val sel1 = splice.changeOwner(ddef.symbol.owner, ctx.owner).select(nme.apply) | ||
val appTpe = if typeParamCount == 0 then sel1 else sel1.appliedToTypes(List.fill(typeParamCount)(defn.AnyType)) | ||
val app1 = appTpe.appliedToArgs(spliceArgs) | ||
val sel2 = app1.select(nme.apply) | ||
sel2.appliedTo(args(2).asInstance(quotesType)) |
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 comment with an example showing the shape of the generated tree would make this code easier to follow.
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 added an example and refactored a bit to make it more readable.
@@ -670,6 +670,9 @@ object TreeChecker { | |||
else assert(tpt.typeOpt =:= pt) | |||
|
|||
// Check that the types of the args conform to the types of the contents of the hole | |||
val typeArgsTypes = args.collect { case arg if arg.isType => |
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.
Will the new checker still work with tasty files generated by previous compiler versions?
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.
TASTy files will not contain Holes so it will work.
Pickled quotes will not contain content
and we should not run this check on them. I will add a guard.
Background
The splicing phase replaces the spaces with holes that do not refer to any term of the surrounding quote. This is done using a lambda that receives the captured references as
Expr[...]
and splices them where they are used in the splice.becomes
then the lambda is extracted from the quote in the
pickleQuotes
phase. This can be done safely because there are no more references to any local variables.Limitation
If there is a type defined in the quote, we currently generate the equivalent
Type[..]
. We end up generating something likebecomes
This takes care correctly of the run-time aspect of the transformation. But when we extract the lambda from the quote we end up with references to
T
that are out of scope. This extrusion happens to work but is not ideal. Erasure removes those types making the tree well formed again.The Solution
Use polymorphic function types to abstract the content of the hole fully.
This PR implements this change.
Other
Based on #17070, #17059, #17054