2013-03-14 16 views
6

Sto provando a scrivere un plug-in del compilatore Scala che consentirà la generazione di codice estremamente generica: qualcosa come la generalità del preprocessore C, ma un po 'più typesafe (non sono sicuro se questo è un'idea terribile, ma è un esercizio divertente). Il mio caso d'uso ideale simile a questa:Chiusura del passaggio al plugin del compilatore 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(...)) 

Il codice del plugin importante potrebbe assomigliare a questo (sulla base di 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 
      } 
    ... 

Qualsiasi idea di come fare questo? Per favore non dire "usa solo macro"; almeno in 2.10, non sono abbastanza generali.

BTW, vedo due problemi con l'approccio che ho delineato: 1) Il plug-in del compilatore accetta un AST, non una chiusura. Avrebbe bisogno di un modo per creare la chiusura, probabilmente aggiungendo una dipendenza di compilazione sul codice utente. 2) L'utente non ha accesso a scala.reflect.internal.Trees.Tree, solo scala.reflect.runtime.universe.Tree, quindi il plugin dovrebbe tradurre tra i due.

+0

È decisamente una pessima idea - ma un grande esercizio;) - devi pensare di guardare a fare macro impl di macro in paradiso. –

risposta

9

Le difficoltà di implementazione che affronti sono in parte la ragione per cui i macro in 2.10 non sono abbastanza generali. Sembrano molto impegnativi e persino fondamentali, ma sono ottimista sul fatto che alla fine possano essere sconfitti. Ecco alcune delle complicate domande di progettazione:

1) Come fai a sapere che la funzione che stai chiamando è la insertTree corretta? Cosa succede se l'utente ha scritto la propria funzione denominata insertTree - come si fa quindi a distinguere una chiamata magica alla funzione speciale e una chiamata normale a una funzione definita dall'utente? Per essere sicuro dovresti digitare typececk il riferimento alla funzione. Ma non è esattamente facile (vedi sotto).

2) Come valuta esattamente la chiamata createFooTree(...)? Proprio come prima, dovresti digitare la parte createFooTree per scoprire cosa rappresenta, il che non è facile.

3) E poi c'è un altro problema. Cosa succede se createFooTree è definito in uno dei file che stai attualmente compilando? Quindi in qualche modo dovresti separarlo e le sue dipendenze dal resto del programma, metterlo in una diversa compilazione, compilarlo e quindi chiamarlo. E poi, cosa succede se la compilazione della funzione o una di queste dipendenze porta a un'espansione macro, che dovrebbe mutare alcuni stati globali del compilatore. Come lo propagheremo al resto del programma?

4) Sto parlando di typechecking tutto il tempo. È un problema? A quanto pare, sì. Se le tue macro possono espandersi dovunque, allora il typechecking diventa davvero complicato. Ad esempio, come si fa a TYPECHECK questo:

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) Buone notizie, però, è che non c'è bisogno di usare scala.reflect.runtime.universe.Tree. È possibile che createFooTree abbia digitato in modo dipendente: def createFooTree[U <: scala.reflect.api.Universe with Singleton](u: Universe): u.Tree. Questo, o l'approccio con scala.reflect.macros.Context che usiamo in Scala 2.10. Non molto carina, ma risolve il problema della mancata corrispondenza dell'universo.

Come linea di fondo, la mia sensazione attuale è che le macro in un linguaggio tipizzato staticamente (in particolare, in un linguaggio orientato agli oggetti, dato che OO offre una serie incredibile di modi in cui i pezzi di codice dipendono l'uno dall'altro) sono davvero difficile. Un modello robusto per le macro digitate che modificano i frammenti arbitrari nel programma che si sta compilando è ancora da scoprire.

Se desideri, possiamo avere una discussione più dettagliata via email. Potremmo anche collaborare per portare a buon fine l'idea di macro corrette.In alternativa, se potessi condividere il tuo caso d'uso, potrei cercare di aiutarti a trovare una soluzione alternativa per la tua situazione particolare.

+0

Grazie, ti ho appena mandato un'e-mail. – emchristiansen

Problemi correlati