2013-03-14 16 views
6

Próbuję napisać wtyczkę kompilatora Scala, która pozwoli na generowanie generalnie bardzo generalnego kodu: coś takiego jak ogólność preprocesora C, ale trochę więcej typówafe (nie jestem pewien, czy to jest straszny pomysł, ale to zabawne ćwiczenie). Mój idealny przypadek użycia wygląda mniej więcej tak:Przejście do wtyczki kompilatora Scala

// 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(...)) 

Ważną kod wtyczki może wyglądać następująco (w oparciu o 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 
      } 
    ... 

Każdy pomysł jak to zrobić? Proszę nie mówić "po prostu używaj makr"; przynajmniej w 2.10, nie są one wystarczająco ogólne.

BTW, widzę dwa problemy z podejściem, które opisałem: 1) Wtyczka kompilatora przyjmuje AST, a nie zamknięcie. Będzie potrzebował jakiegoś sposobu na zamknięcie, prawdopodobnie dodając zależność od kodu użytkownika. 2) Użytkownik nie ma dostępu do scala.reflect.internal.Trees.Tree, tylko scala.reflect.runtime.universe.Tree, więc wtyczka musiałaby przetłumaczyć między tymi dwoma.

+0

To zdecydowanie przerażający pomysł - ale wspaniałe ćwiczenie;) - należy myśleć o tym, jak zajrzeć do makrofotografii w raju. –

Odpowiedz

9

Trudności z implementacją są po części powodem, dla którego makra w wersji 2.10 nie są wystarczająco ogólne. Wyglądają bardzo wymagająco, a nawet fundamentalnie, ale jestem optymistą, że mogą zostać ostatecznie pokonani. Oto niektóre z trudnych pytań projektowych:

1) Skąd wiadomo, że funkcja, do której dzwonisz, jest właściwa insertTree? Co jeśli użytkownik napisał własną funkcję o nazwie insertTree - jak można wtedy odróżnić wywołanie magiczne od funkcji specjalnej i normalne wywołanie funkcji zdefiniowanej przez użytkownika? Aby upewnić się, że konieczne jest sprawdzenie poprawności odwołania do funkcji. Ale to nie jest takie proste (patrz poniżej).

2) Jak dokładnie oceniasz połączenie createFooTree(...)? Tak jak poprzednio, musisz sprawdzić część createFooTree, aby dowiedzieć się, co ona oznacza, co nie jest łatwe.

3) A następnie jest jeszcze jeden problem. Co jeśli createFooTree jest zdefiniowany w jednym z plików, które aktualnie kompilujesz? Wtedy musiałbyś jakoś oddzielić to i jego zależności od reszty programu, umieścić go w innym przebiegu kompilacji, skompilować, a następnie wywołać. A co dalej, jeśli kompilacja funkcji lub jednej z tych zależności prowadzi do rozszerzenia makro, które ma zmutować jakiś globalny stan kompilatora. Jak zamierzamy ją propagować do końca programu?

4) Mówię o sprawdzaniu poprawności przez cały czas. Czy to problem? Najwyraźniej tak. Jeśli twoje makra mogą rozszerzyć się w dowolne miejsce, wtedy sprawdzanie typu staje się naprawdę trudne. Na przykład, w jaki sposób typecheck to:

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) Dobrą wiadomością jest jednak to, że nie trzeba używać scala.reflect.runtime.universe.Tree. Można wpisać createFooTree w sposób zależny: def createFooTree[U <: scala.reflect.api.Universe with Singleton](u: Universe): u.Tree. To lub podejście z scala.reflect.macros.Context używamy w Scali 2.10. Niezbyt ładna, ale rozwiązuje problem niedopasowania wszechświata.

Moim zdaniem, uważam, że makra w statycznie napisanym języku (szczególnie w języku obiektowym, ponieważ OO oferuje niesamowitą garść sposobów na to, że kawałki kodu zależą od siebie) są naprawdę zdradliwy. Solidny model dla makr wpisywanych, modyfikujących dowolne fragmenty w kompilowanym programie, nie został jeszcze odkryty.

Jeśli chcesz, możemy przeprowadzić bardziej szczegółową dyskusję przez e-mail. Możemy również współpracować, aby zrealizować ideę prawidłowych makr.Lub, jeśli możesz podzielić się swoją przypadkiem użycia, mógłbym spróbować pomóc w znalezieniu obejścia dla twojej konkretnej sytuacji.

+0

Dzięki, wysłałem Ci wiadomość e-mail. – emchristiansen

Powiązane problemy