2013-03-14 6 views
6

Ich versuche, ein Scala-Compiler-Plugin zu schreiben, das extrem allgemeine Code-Generierung erlaubt: etwas wie die Allgemeingültigkeit des C-Präprozessors, aber ein bisschen typsicherer (ich bin mir nicht sicher, ob das ist eine schreckliche Idee, aber es ist eine lustige Übung). Mein idealer Anwendungsfall sieht ungefähr so ​​aus:Pass closure zu Scala-Compiler-Plugin

// User code. This represents some function that might take some args 
// and outputs an abstract syntax tree. 
def createFooTree(...): scala.reflect.runtime.universe.Tree = ... 

// Later user code (maybe separate compilation?). Here the user generates 
// code programmatically using the function call to |createFooTree| and inserts 
// the code using insertTree. 
insertTree(createFooTree(...)) 

Der wichtige Plugin-Code könnte wie folgt aussehen (basierend auf this):

class InsertTreeComponent(val global: Global) 
    extends PluginComponent 
    with TypingTransformers { 
    import global._ 
    import definitions._ 

    override val phaseName = "insertTree" 

    override val runsRightAfter = Some("parser") 
    override val runsAfter = runsRightAfter.toList 
    override val runsBefore = List[String]("typer") 

    def newPhase(prev: Phase): StdPhase = new StdPhase(prev) { 
    def apply(unit: CompilationUnit) { 
     val onTransformer = new TypingTransformer(unit) { 
     override def transform(tree: Tree): Tree = tree match { 
      case orig @ Apply(
      function, 
      // |treeClosure| is the closure we passed, which should 
      // evaluate to a Tree (albeit a runtime Tree). 
      // The function.toString bit matches anything that looks like a 
      // function call with a function called |insertTree|. 
      treeClosure) if (function.toString == "insertTree") => { 
      // This function evaluates and returns the Tree, inserting it 
      // into the call site as automatically-generated code. 
      // Unfortunately, the following line isn't valid. 
      eval(treeClosure): Tree 
      } 
    ... 

Jede Idee, wie dies zu tun? Bitte sag nicht "benutze nur Makros"; zumindest in 2.10 sind sie nicht allgemein genug.

BTW, sehe ich zwei Probleme mit dem Ansatz, den ich skizziert habe: 1) Das Compiler-Plugin nimmt eine AST, keine Schließung. Es würde eine Art der Erstellung des Abschlusses benötigen, wahrscheinlich eine Build-Abhängigkeit vom Benutzercode hinzufügen. 2) Der Benutzer hat keinen Zugriff auf scala.reflect.internal.Trees.Tree, nur scala.reflect.runtime.universe.Tree, also müsste das Plugin zwischen den beiden übersetzen.

+0

Es ist definitiv eine schreckliche Idee - aber eine gute Übung;) - Sie sollten darüber nachdenken, in Hacking MakroImpl im Paradies zu suchen. –

Antwort

9

Die Implementierungsschwierigkeiten, mit denen Sie konfrontiert sind, sind teilweise der Grund, warum Makros in 2.10 nicht allgemein genug sind. Sie sehen sehr herausfordernd und sogar fundamental aus, aber ich bin optimistisch, dass sie letztendlich besiegt werden können. Hier sind einige der kniffligen Design-Fragen:

1) Woher wissen Sie, dass die Funktion, die Sie anrufen, die richtige insertTree ist? Was ist, wenn der Benutzer eine eigene Funktion namens insertTree geschrieben hat - wie unterscheiden Sie dann einen magischen Anruf zu Ihrer speziellen Funktion und einen normalen Anruf zu einer benutzerdefinierten Funktion? Um sicher zu sein, müssten Sie den Verweis auf die Funktion überprüfen. Aber das ist nicht gerade einfach (siehe unten).

2) Wie bewerten Sie den Anruf createFooTree(...)? Genau wie vorher müssten Sie den createFooTree Teil überprüfen, um herauszufinden, wofür er steht, was nicht einfach ist.

3) Und dann gibt es noch ein Problem. Was ist, wenn createFooTree in einer der Dateien definiert ist, die Sie gerade kompilieren? Dann müssten Sie es irgendwie und seine Abhängigkeiten vom Rest des Programms trennen, es in einen anderen Kompilierungslauf einfügen, es kompilieren und es dann aufrufen. Und was passiert, wenn die Kompilierung der Funktion oder eine dieser Abhängigkeiten zu einer Makro-Erweiterung führt, die einen globalen Status des Compilers verändern soll. Wie werden wir es für den Rest des Programms verbreiten?

4) Ich spreche über Typchecking die ganze Zeit. Ist das ein Problem? Anscheinend ja. Wenn sich Ihre Makros irgendwo ausdehnen können, wird die Typisierung sehr schwierig. Zum Beispiel, wie typecheck Sie dies:

class C { 
    insertTree(createFoo(bar)) // creates `def foo = 2`, requires `bar` to be defined to operate 
    insertTree(createBar(foo)) // creates `def bar = 4`, requires `foo` to be defined to operate 
} 

5) Eine gute Nachricht ist jedoch, dass Sie müssen nicht scala.reflect.runtime.universe.Tree verwenden. Sie könnten createFooTree abhängig-typed: def createFooTree[U <: scala.reflect.api.Universe with Singleton](u: Universe): u.Tree haben. Dies oder den Ansatz mit scala.reflect.macros.Context verwenden wir in Scala 2.10. Nicht sehr hübsch, aber löst das Problem der Unausgewogenheit des Universums.

Als Endergebnis ist mein derzeitiges Gefühl, dass Makros in einer statisch getippten Sprache (besonders in einer objektorientierten Sprache, da OO eine erstaunliche Menge an Möglichkeiten für Codeabschnitte bietet, voneinander abhängig zu sein) wirklich sind schwierig. Ein robustes Modell für typisierte Makros, die willkürliche Fragmente in dem zu kompilierenden Programm modifizieren, muss noch entdeckt werden.

Wenn Sie möchten, könnten wir eine detailliertere Diskussion per E-Mail haben. Wir könnten auch zusammenarbeiten, um die Idee der richtigen Makros zu verwirklichen.Oder alternativ, wenn Sie Ihren Anwendungsfall teilen könnten, könnte ich versuchen, einen Workaround für Ihre spezielle Situation zu finden.

+0

Danke, ich habe dir gerade eine E-Mail geschickt. – emchristiansen