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

Support cross-quotation references #61

Merged
merged 5 commits into from
Jan 14, 2019
Merged

Conversation

LPTK
Copy link
Member

@LPTK LPTK commented Jan 13, 2019

Cross-quotation references (CQR) allow one to write (this works with the changes in this PR):

code{ x: Int => ${
  println("Constructing code!")
  foo(code{ x + 1 })
}}

(notice the call to foo at code-generation time, passing it a piece of code that contains an identifier bound in the outer quote)

instead of having to either:

  • use an automatically-lifted function, if one doesn't care about contexts (this will only work with OpenCode types, as it is unsound otherwise):
    code{ x: Int => ${ (x: OpenCode[Int]) => foo(code{ ${x} + 1 }) } }
  • use a first-class variable symbol:
    val x = Variable[Int]
    code{ $x => ${ foo(code{ ${x} + 1 }) } }
  • use a first-class variable symbol with syntax sugar (currently only works in quasi-quotes):
    code"{ x: Int => ${ (x: Variable[Int]) => foo(code{ ${x} + 1 }) } }"

All of these workarounds are quite unwieldy, and the lack of CQR has always been a pain.

CQR is hard to do because of the limited control given to macros, and the order in which macros expand. One of the sticking points has been to make the feature scope-safe, which means that we need to track that the inner quote refers to the outer quote's binding, so it cannot be extruded.

The approach taken here is to use x.type as the phantom context requirement associated with a cross-stage reference to the x binding. The approach only works in quasi-code. I'm pretty sure it cannot be done with quasi-quotes without some sort of annotations (which defeats the purpose; at this point just use variable symbols), due to fundamental type checking and type inference reasons.

This way, one can even mention the context requirements explicitly:

val c1 = code{ x: String => ${
  val c: Code[Int,x.type] = code{x.length}
  code{${c} + 1}
}}
c1 == code{ a: String => a.length + 1 } // true

Note that in current Scala, writing the type explicitly only works if x extends AnyRef (otherwise Scala doesn't let you write x.type), a dumb limitations that is apparently gone from Scala 2.13, fortunately. To work around this problem, I wrote a singleton.scope macro in squid.utils.typing, so that one can always write singleton.scope.x to mean x.type, even for AnyVal types.

This paves the way towards allowing cross-quotation references. It is
necessary because there is no reliable way to know, inside an inner
quasiquote, if something is a cross-stage or cross-quotation reference.
This is because at this point the outer code has not been type checked
yet, so we don’t know, for example, whether it will be lifted to code
via an implicit conversion.
This allows variable references in nested quotes to refer to bindings
in outer quotes. Only works with quasiCODE; making it work with
quasiquotes without some more handholding by the user does not seem
possible because of the fundamental order in which the macros expand:
a quasiquote’s holes expand before the quasiquote, so cross-quotation
references would have no way of knowing the type of their binding. By
contrast, in quasicodes the binding already has a symbol and type when
the argument macros expand (even though the outer macro itself expands
later).
We used to generate a new Variable[T] symbol and associate it with the
symbol of the binding, but that was rather brittle; it resulted in
inferred existential types for let-bound code values, as in:
  val c = code{x+1} // Warning: inferred existential type
                    // Code[Int,x.Ctx] forSome { val x: Variable[Int] }
and it resulted in later type mismatches.

Now, we just use the singleton type of the binding, which seems more
robust; this way, we can even refer to the contexts explicitly! — that
is, if the type of the binding extends AnyRef (limitation of Scala 2.12,
apparently lifted in Scala 2.13).
@LPTK
Copy link
Member Author

LPTK commented Jan 13, 2019

In order to make CQR unambiguous, I had to change the implementation of cross-stage references, because it turned out that there was no reliable way of distinguishing one from the other in general. Since QSR are quite a niche feature, I think this is fine.

When one wants to refer to a value or expression in the current stage from within a quote, one needs to either:

  • surround the expression with %, as in val x = 'test; code"%{x.name}.length"

  • annotate the value binding with @persisted, as in @persisted val x = 'test.name; code"x.length"

This new restriction doesn't affect cross-stage persistence of members accessed via this, however, because there is no possible ambiguity there.

@LPTK LPTK merged commit c0cb9e6 into master Jan 14, 2019
@LPTK LPTK deleted the cross-quotation-references branch January 16, 2019 13:37
@LPTK LPTK mentioned this pull request Jan 28, 2019
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant