Skip to content

A symmetric meta programming framework #3634

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

Merged
merged 62 commits into from
Dec 21, 2017
Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Dec 5, 2017

A first implementation of the compiler parts of symmetric meta programming.

Still to be done in separate PRs:

  • Add unpickling
  • Implement Liftable natively
  • Decide on packaging -- where does quoted reside?
  • Work out how to represent TypeTags
  • Add more tests and examples

Copy link
Member

@dottybot dottybot left a comment

Choose a reason for hiding this comment

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

Hello, and thank you for opening this PR! 🎉

All contributors have signed the CLA, thank you! ❤️

Commit Messages

We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).

Please stick to these guidelines for commit messages:

  1. Separate subject from body with a blank line
  2. When fixing an issue, start your commit message with Fix #<ISSUE-NBR>:
  3. Limit the subject line to 72 characters
  4. Capitalize the subject line
  5. Do not end the subject line with a period
  6. Use the imperative mood in the subject line ("Added" instead of "Add")
  7. Wrap the body at 80 characters
  8. Use the body to explain what and why vs. how

adapted from https://chris.beams.io/posts/git-commit

Have an awesome day! ☀️

@nicolasstucki
Copy link
Contributor

I will integrate the TASTY reification from the prototype in https://github.com/dotty-staging/dotty/tree/tasty-quote

@@ -49,7 +49,8 @@ class Compiler {
List(new LinkAll), // Reload compilation units from TASTY for library code (if needed)
List(new FirstTransform, // Some transformations to put trees into a canonical form
new CheckReentrant, // Internal use only: Check that compiled program has no data races involving global vars
new ElimJavaPackages), // Eliminate syntactic references to Java packages
new ElimPackagePrefixes, // Eliminate syntactic references to Java packages
new ReifyQuotes), // Eliminate syntactic references to Java packages
Copy link
Contributor

Choose a reason for hiding this comment

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

We might need to do this before FirstTranform, when we will pickle the contents of the quotes we should probably give that pickler a tree that is in the same shape as the tree was in Pickler.

Copy link
Contributor Author

@odersky odersky Dec 5, 2017

Choose a reason for hiding this comment

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

FirstTransforms does some simplifications. It eliminates NamedArgs and Imports and replaces all trees representing types by TypeTree nodes. This is arguably good, since it simplifies the trees to reify quite a bit. The reason we keep the more complicated trees in the TASTY is for the IDE, so that all elements of source files are represented. But for macros and staging that should not be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On second thought, I now see you were right. It does become easier if we reify exactly in the pickled format.

@smarter
Copy link
Member

smarter commented Dec 5, 2017

The PR is based on the various TASTY fix PRs which are in turn based on #3602 - would be good to get that bunch in soon so we can consolidate.

I left some comments on #3494, once that's in the rest should be easy to get in.

@@ -0,0 +1,15 @@
import scala.meta._
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the plan to use this for the public API or only tests? scala.meta.{Tree,Type,Term,...} are already defined in https://github.com/scalameta/scalameta/blob/v2.1.3/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala and get ~50,000 monthly downloads.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could use scala.meta.quote instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could use scala.meta.quote instead.

Good idea.

Copy link
Member

Choose a reason for hiding this comment

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

Still seems a bit too easy to confuse with scala.meta. How about one of: scala.quote, scala.reify, scala.staging

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for scala.staging

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about scala.quoted? staging and reify is both expert-speak.

Copy link
Contributor Author

@odersky odersky Dec 6, 2017

Choose a reason for hiding this comment

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

Also, it's a stretch to call the macro part of this "staging".

@odersky
Copy link
Contributor Author

odersky commented Dec 8, 2017

test performance please

@dottybot
Copy link
Member

dottybot commented Dec 8, 2017

performance test scheduled: 1 job(s) in queue, 1 running.

@dottybot
Copy link
Member

dottybot commented Dec 8, 2017

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3634/ to see the changes.

Benchmarks is based on merging with master (cb758bd)

@nicolasstucki
Copy link
Contributor

I found a small blocker when inlining quotes in separate compilations

object Macros {
  inline def f(): Unit = '(1)
}
class Test {
  Macros.f()
}
-- Error: tests/pos/quoted2.scala:6:14 -----------------------------------------
6 |
  |              ^
  |undefined: <special-ops>.' # 51: TermRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),module <special-ops>),') at frontend
one error found

We are probably missing a case in the TreeUnpickler that trasforms calls to <special-ops>.' to Quoted trees.

@nicolasstucki
Copy link
Contributor

@odersky, @smarter, @OlivierBlanvillain, @allanrenucci @liufengyun we need to rebase all open all PRs on top of #3665. Without this, the CI will try to execute a test command that doesn't exist anymore.

@@ -17,7 +17,7 @@ object Test {
if (b) '(true) else '(false)
}

implicit def ListIsQuotable[T: Type: Quotable]: Quotable[List[T]] = new {
implicit def ListIsQuotable[T: Quotable]: Quotable[List[T]] = new {
Copy link
Contributor

Choose a reason for hiding this comment

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

Quotable -> Liftable
In several places of this file

There was a discrepancy in that value parameters had a Deferred flag set
before pickling but not after unpickling. This triggered a check that the
(missing) rhs of an inline parameter was a constant. This commit changes
the condition of the test to also exclude parameters. It also aligns
frontend and unpickler in that the frontend will no longer mark term
parameters as Deferred.
Previous condition did not take into account private static members. Their
"enclosing class" is the enclosing package, but we really want to count them
as static members of their lexical enclosing class here.
With the cleaned-up local scope handling we don't need them anymore.
Allow `~ stableId` as a type, where `stableId` is a stable term identifier.
This is not the same as the second part of SIP 33, which proposed `~` as an
operator over types.
Just came by this accidentally and noticed that it could be more idiomatic and
terminology could be improved.
That's a better description of what the miniphase does.
Can be used like this:

    <compute>
      .reporting(res => println(s"result is $res"))

To achieve this previously, we'd need to store result in a val and then
return the val. This is more effort, in particular if we want to just quickly
add some debug output, to be removed later.
No phase consistency principle yet, we compute just the basic types.
The body of reify is still missing, for @nicolasstucki can fill in.
For the moment, we just replace the tree to reify with its "_.show" output.
This is still somewhere between a paper draft and true reference docs.
But since it's the only thing we have right now, it's worthwhile to
integrate it now and change it later.
Rename ' to a unicode quote character so that syntax highlighters don't get confused.
Also, add AsFunction implicit class to Expr.
# Symmetric Meta Programming

Symmetric meta programming is a new framework for staging and certain
forms of macros. It is is expressed as strongly and statically typed
Copy link
Contributor

Choose a reason for hiding this comment

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

is is


object Macros {

def mapImpl[T, U](u: Type[U], arr: Expr[Array[T]], op: Expr[T => U])(implicit ctx: Context): Expr[Array[U]] = ’{
Copy link
Contributor

Choose a reason for hiding this comment

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

We do not need the implicit context for this one.

# Symmetric Meta Programming

Symmetric meta programming is a new framework for staging and for some
forms of macros. It is is expressed as strongly and statically typed
Copy link
Contributor

Choose a reason for hiding this comment

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

is is

takes expressions of type `Expr[T]` to expressions of type `T` and it
takes expressions of type `Type[T]` to types `T`.

The two types can be are defined in package `scala.quoted` as follows:
Copy link
Contributor

Choose a reason for hiding this comment

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

can be are defined

The phase consistency principle can be motivated as follows: First,
suppose the result of a program `P` is some quoted text `’{ ... x
... }` that refers to a free variable `x` in `P` This can be
represented only by referring to original the variable `x`. Hence, the
Copy link
Contributor

@allanrenucci allanrenucci Dec 19, 2017

Choose a reason for hiding this comment

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

referring to the original the variable


Here’s an application of `map` and how it rewrites to optimized code:

genSeq[Int]().map(x => x + 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

There are some inconsistencies: you map with x => x + 1 here but with x => x * x below

`Macro.assertImpl` becomes phase correct even if macro library and
program are conceptualized as local definitions.

But what about the call from `assert` to `assertImpl? Here, we need a
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing ` after asserImpl

contains a splice operation outside an enclosing quote is called a
_macro_. Macros are supposed to be expanded in a subsequent phase,
i.e. in a quoted context. Therefore, they are also type checked as if
they were in a quoted context, For instance, the definition of
Copy link
Contributor

Choose a reason for hiding this comment

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

s/context,/context.

representation of `Expr` trees. For instance, here is a possible
instance of `Liftable[Boolean]`:

implicit def BooleanIsLiftable: Liftable[Boolean] = new {
Copy link
Contributor

Choose a reason for hiding this comment

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

new Liftable {

possible implementation of `Liftable[Int]` that does not use the underlying
tree machinery:

implicit def IntIsLiftable: Liftable[Int] = new {
Copy link
Contributor

Choose a reason for hiding this comment

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

new Liftable {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, that's actually legal syntax now.

Since `Liftable` is a type class, instances can be conditional. For instance
a `List` is liftable if its element type is:

implicit def ListIsLiftable[T: Liftable]: Liftable[List[T]] = new {
Copy link
Contributor

Choose a reason for hiding this comment

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

new Liftable {


This would allow constructing applications from lists of arguments
without having to match the arguments one-by-one with the
corresponding formal parameter types of the function. We need then "at
Copy link
Contributor

Choose a reason for hiding this comment

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

We then need?

trees. So we should wait with this addition until we have more
use-cases that help us decide whether the loss in type-safety is worth
the gain in flexibility. In this context, it seems that deconstructing types is
less error-prone than deconstructing tersm, so one might also
Copy link
Contributor

Choose a reason for hiding this comment

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

terms


~(’t) --> t

t --> t’
Copy link
Contributor

Choose a reason for hiding this comment

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

We should avoid using both ' for quote and ' prime. Maybe we should use unstead if we use t --> t2.

@odersky odersky changed the title [WIP] Experiment with quotes and splices A symmetric meta programming framework Dec 19, 2017
@nicolasstucki
Copy link
Contributor

With separate compilation, this code produces an unexpected error message.

import scala.quoted._
object Macros {
  inline def assert(expr: => Boolean): Unit =  ~ assertImpl('(expr))
  def assertImpl(expr: Expr[Boolean]) = '{ () }
}
class Test {
  import Macros._
  val x = 1
  assert(x != 0)
}
8 |
  |                    ^^^^
  |         value of type scala.quoted.Type[Boolean] does not take parameters

@@ -225,7 +227,7 @@ object TastyFormat {

final val header = Array(0x5C, 0xA1, 0xAB, 0x1F)
val MajorVersion = 2
val MinorVersion = 0
val MinorVersion = 1
Copy link
Contributor

Choose a reason for hiding this comment

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

We do not need this since we have not published 2.0 yet

Copy link
Member

Choose a reason for hiding this comment

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

No sure what you mean by "published". We do have nightly builds out that use 2.0 and we should bump the version when we change something because it's good practice and because it's valuable information to anyone wanting to make a separate implementation of tasty.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK

/** A Macrotransform that maintains the necessary infrastructore to support
* contxtual implicit searches (type-scope implicits are supported anyway).
*/
abstract class MacroTransformWithImplicits extends MacroTransform {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this really go in if we do not use it? What are the other use cases where it might be needed?

Choose a reason for hiding this comment

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

someone might want to fix the typos in the doc-comment, too...

Copy link
Contributor

@nicolasstucki nicolasstucki left a comment

Choose a reason for hiding this comment

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

But we need to add an issue for the failing code that I posted. There are also a couple of comments to attend that could be left for a second PR.

Otherwise unpickler gets confused when it tries to resolve
references to these methods.
It's easier to handle this in PostTyper since then we do
not need to distinguish between inlined and normal code.

Also, need to record quotes as well as splices since ReifyQuotes
should also do macro expansion.

Also, add a missing recursive call to `transform` in ReifyQuotes.
This currently fails with "splice outside quote". Once
macro expansion is implemented it should compile.
@@ -1086,7 +1074,6 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
}

def typedQuote(tree: untpd.Quote, pt: Type)(implicit ctx: Context): Tree = track("typedQuote") {
ctx.compilationUnit.containsQuotes = true
Copy link
Contributor

Choose a reason for hiding this comment

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

We do need ctx.compilationUnit.containsQuotesOrSplices = true in case there are only quotes in the tree. I can add the fix with the test case in #3662.

Copy link
Contributor

@nicolasstucki nicolasstucki left a comment

Choose a reason for hiding this comment

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

Approve latest fixes

import scala.quoted._
object Macros {
inline def assert(expr: => Boolean): Unit = ~ assertImpl('(expr))
def assertImpl(expr: Expr[Boolean]) = '{ () }
Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, this one works fine as a run test in #3662

@odersky odersky merged commit 1e8c8a6 into scala:master Dec 21, 2017
@allanrenucci allanrenucci deleted the add-meta branch December 21, 2017 12:56
odersky added a commit to dotty-staging/dotty that referenced this pull request Dec 31, 2017
We did not use lazy evaluation for static objects, because these are usually evauated
lazily by the backend. But that does not hold if the static module is an (accessor) def
instead of a val.
odersky referenced this pull request Jan 13, 2018
Fix #3624: Lazily evaluate static module accessors
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.

7 participants