2012-06-14 5 views
8

Sto sviluppando un DSL e sto ottenendo un errore "a termine" durante l'espansione di una macro. Mi piacerebbe sapere se può essere evitato. Ho semplificato il problema alla seguente situazione.È possibile evitare questo errore di variabile a tempo libero (prodotto all'espansione macro)?

Supponiamo di avere questa espressione:

val list = join { 
    0 
    1 
    2 
    3 
} 
println(list) 

dove join è una macro il cui implementazione è:

def join(c: Ctx)(a: c.Expr[Int]): c.Expr[List[Int]] = { 
    import c.mirror._ 
    a.tree match { 
    case Block(list, ret) => 
     // c.reify(List(new c.Expr(list(0)).eval, 
     //    new c.Expr(list(1)).eval, 
     //    new c.Expr(list(2)).eval) :+ new c.Expr(ret).eval) 
     c.reify((for (expr <- list) yield new c.Expr(expr).eval) :+ new c.Expr(ret).eval) 
    } 
} 

Scopo della macro è quello di unire tutti gli elementi nel blocco discussione e ritorno li in una sola lista. Poiché il contenuto del blocco potrebbe essere variabile, non posso usare il reify commentato (che funziona bene). L'uno non commentata -con una di comprensione, che genera apporta tiri liberi il messaggio:

"espansione macro contiene libero termine elenco variabile definita dal partecipare Macros.scala: 48:. 18 Hai dimenticato di usare eval quando splicing di questa variabile in un reifee? Se hai problemi nel tracciare variabili di termine libere, considera l'utilizzo di termini -Xlog-free "

C'è un modo per introdurre la comprensione per (o un iteratore o qualsiasi altra cosa) senza ottenere questo errore? A proposito, sto usando 2,10 M3.

risposta

15

Il problema è che il codice si mescola a tempo di compilazione e di concetti di runtime.

La variabile "elenco" che si sta utilizzando è un valore in fase di compilazione (ovvero dovrebbe essere iterato durante la fase di compilazione) e si chiede a reify di mantenerlo fino al runtime (mediante splicing derivato valori). Questo enigma di livello intermedio porta alla creazione di un cosiddetto termine libero.

Insomma, termini liberi sono stub che fanno riferimento ai valori da fasi precedenti. Ad esempio, il seguente frammento:

val x = 2 
reify(x) 

sarà compilato come segue:

val free$x1 = newFreeTerm("x", staticClass("scala.Int").asTypeConstructor, x); 
Ident(free$x1) 

intelligente, eh? Il risultato mantiene il fatto che x è un Ident, conserva il suo tipo (caratteristiche in fase di compilazione), ma, comunque, si riferisce anche al suo valore (una caratteristica run-time). Ciò è reso possibile dall'ambito del lessico.

Ma se si tenta di restituire questo albero da una macro di espansione (che è inline nel sito di chiamata di una macro), le cose saltare in aria. Il sito di chiamata della macro molto probabilmente non avrà x nel suo ambito lessicale, quindi non sarebbe in grado di riferirsi al valore di x.

Ciò che è peggio. Se il frammento di cui sopra è scritto all'interno di una macro, allora x esiste solo durante la compilazione, cioè nella JVM che esegue il compilatore. Ma quando il compilatore termina, non c'è più.

Tuttavia, i risultati di espansione di macro che contengono un riferimento alla x dovrebbero essere eseguito in fase di esecuzione (molto probabilmente, in una JVM diversa). Per dare un senso a ciò, è necessaria la persistenza in più fasi, ovvero la capacità di serializzare in qualche modo valori arbitrari in fase di compilazione e di deserializzarli durante il runtime. Non so come farlo in un linguaggio compilato come Scala.


Si noti che in alcuni casi cross-stadio persistenza è possibile.Ad esempio, se x è un campo di un oggetto statico:

object Foo { val x = 2 } 
import Foo._ 
reify(x) 

Allora non finirebbe come termine libero, ma sarebbe reificato in modo semplice:

Select(Ident(staticModule("Foo")), newTermName("x")) 

Questo è un concetto interessante che è stato anche discusso nel discorso di SPJ agli Scala Days 2012: http://skillsmatter.com/podcast/scala/haskell-cloud.

Per verificare che alcune espressioni non contengano termini liberi, in Haskell aggiungono una nuova primitiva built-in al compilatore, il costruttore di tipo Static. Con i macro, possiamo farlo naturalmente usando reify (che di per sé è solo una macro). Vedi la discussione qui: https://groups.google.com/forum/#!topic/scala-internals/-42PWNkQJNA.


Okay ora abbiamo visto qual è esattamente il problema con il codice originale, quindi come possiamo farlo funzionare?

Purtroppo dovremo ripiegare alla costruzione AST manuale, perché reificare ha tempo difficile esprimere alberi dinamici. Il caso d'uso ideale per reify in macrology sta avendo un template statico con i tipi di buchi noti al momento della compilazione della macro. Fai un passo indietro e dovrai ricorrere a costruire alberi a mano.

Linea di fondo è che si deve andare con il seguente (funziona con recentemente rilasciato 2.10.0-M4, consultare la guida alla migrazione a scala-lingua per vedere esattamente cosa è cambiato: http://groups.google.com/group/scala-language/browse_thread/thread/bf079865ad42249c):

import scala.reflect.makro.Context 

object Macros { 
    def join_impl(c: Context)(a: c.Expr[Int]): c.Expr[List[Int]] = { 
    import c.universe._ 
    import definitions._ 
    a.tree match { 
     case Block(list, ret) => 
     c.Expr((list :+ ret).foldRight(Ident(NilModule): Tree)((el, acc) => 
      Apply(Select(acc, newTermName("$colon$colon")), List(el)))) 
    } 
    } 

    def join(a: Int): List[Int] = macro join_impl 
} 
+0

Ci sarà qualcosa di più semplice in scala 2.11? Le virgolette saranno utili? (Non ho studiato le macro nel profondo, ma ogni volta che provo io continuo a sbattere contro questo) – HRJ

+0

Quasiquotes stanno per aiutare un po ': https://gist.github.com/densh/6209261 –