在 GitHub 上編輯此頁面

巨集規格

形式化

  • 使用生成式和分析式巨集進行多階段程式設計[^2]
  • 多階段巨集微積分,Scala 3 中可擴充元程式設計第 4 章[^1]。包含並延伸了具有類型多態性的使用生成式和分析式巨集進行多階段程式設計微積分。

語法

選擇使用 '$ 的引號語法,以模仿 Scala 的字串內插語法。與雙引號字串一樣,單引號區塊可以包含拼接。但是,與字串不同的是,拼接可以包含使用相同規則的引號。

s" Hello $name"           s" Hello ${name}"
'{ hello($name) }         '{ hello(${name}) }
${ hello('name) }         ${ hello('{name}) }

引號

引號有四種形式:引號識別碼、引號區塊、引號區塊模式和引號類型模式。Scala 2 使用引號識別碼來表示 Symbol 文字。它們在 Scala 3 中已棄用,允許語法用於引號。

SimpleExpr ::= ...
             |  `'` alphaid                           // quoted identifier
             |  `'` `{` Block `}`                     // quoted block
Pattern    ::= ...
             |  `'` `{` Block `}`                     // quoted block pattern
             |  `'` `[` Type `]`                      // quoted type pattern

引號區塊和引號區塊模式包含等於一般程式碼區塊的表達式。在進入任一區塊時,我們會追蹤我們處於引號區塊 (inQuoteBlock) 的事實,這用於拼接識別碼。在進入引號區塊模式時,我們還會追蹤我們處於引號模式 (inQuotePattern) 的事實,這用於區分拼接區塊和拼接模式。最後,引號類型模式僅包含類型。

拼接

拼接分為三種:拼接識別碼、拼接區塊和拼接模式。Scala 將包含 $ 的識別碼指定為有效的識別碼,但僅保留給編譯器和標準函式庫使用。不幸的是,許多函式庫在 Scala 2 中使用了此類識別碼。因此,為了降低遷移成本,我們仍支援它們。我們透過僅允許在引號區塊或引號區塊模式 (inQuoteBlock) 中使用拼接識別碼[^3] 來解決此問題。拼接區塊和拼接模式分別可以包含任意區塊或模式。它們根據周圍的引號 (inQuotePattern) 區分,引號區塊將包含拼接區塊,而引號區塊模式將包含拼接模式。

SimpleExpr ::= ...
             |  `$` alphaid         if  inQuoteBlock    // spliced identifier
             |  `$` `{` Block `}`   if !inQuotePattern  // spliced block
             |  `$` `{` Pattern `}` if  inQuotePattern  // splice pattern

引號模式類型變數

引號模式和引號類型模式中的引號模式類型變數不需要額外的語法。在建構類型時,假設任何名稱由小寫組成的類型定義或參考都是模式類型變數定義。帶有小寫反引號的類型名稱會被解釋為對該名稱類型的參考。

實作

執行時期表示

標準函式庫定義了 Quotes 介面,其中包含所有邏輯和抽象類別 ExprType。編譯器實作 Quotes 介面,並提供 ExprType 的實作。

class Expr

類型為 Expr[T] 的表達式由以下抽象類別表示

abstract class Expr[+T] private[scala]

Expr 的唯一實作在編譯器中,以及 Quotes 的實作。它是一個類別,封裝一個建構類型 AST 和一個沒有自己方法的 Scope 物件。Scope 物件用於追蹤目前的拼接範圍並偵測範圍外溢。

object Expr

Expr 的 companion 物件包含一些有用的靜態方法;apply/unapply 方法可輕鬆使用 ToExpr/FromExprbetaReducesummon 方法。它還包含用於建立表達式的方法,這些表達式來自表達式的清單或序列:blockofSeqofListofTupleFromSeqofTuple

object Expr:
  def apply[T](x: T)(using ToExpr[T])(using Quotes): Expr[T] = ...
  def unapply[T](x: Expr[T])(using FromExpr[T])(using Quotes): Option[T] = ...
  def betaReduce[T](e: Expr[T])(using Quotes): Expr[T] = ...
  def summon[T: Type](using Quotes): Option[Expr[T]] = ...
  def block[T](stats: List[Expr[Any]], e: Expr[T])(using Quotes): Expr[T] = ...
  def ofSeq[T: Type](xs: Seq[Expr[T]])(using Quotes): Expr[Seq[T]] = ...
  def ofList[T: Type](xs: Seq[Expr[T]])(using Quotes): Expr[List[T]] = ...
  def ofTupleFromSeq(xs: Seq[Expr[Any]])(using Quotes): Expr[Tuple] = ...
  def ofTuple[T <: Tuple: Tuple.IsMappedBy[Expr]: Type](tup: T)(using Quotes):
      Expr[Tuple.InverseMap[T, Expr]] = ...
class Type

型別 Type[T] 的型別由下列抽象類別表示

abstract class Type[T <: AnyKind] private[scala]:
  type Underlying = T

Type 唯一的實作在編譯器中,連同 Quotes 的實作。它是一個類別,用於包裝型別的 AST 和一個 Scope 物件,沒有自己的方法。T 的上限是 AnyKind,表示 T 可能是一個高階型別。Underlying 別名用於從 Type 的執行個體中選取型別。使用者永遠不需要使用這個別名,因為他們總是可以直接使用 TUnderlying 用於在編譯程式碼時進行內部編碼(請參閱型別癒合)。

object Type

Type 的 companion 物件包含一些有用的靜態方法。第一個也是最重要的,是 Type.of 給定的定義。當沒有其他執行個體可用時,預設會呼叫 Type[T] 的這個執行個體。of 作業是一個內部作業,編譯器會將它轉換成在執行時產生 Type[T] 的程式碼。其次,Type.show[T] 作業會顯示型別的字串表示,這在除錯時通常很有用。最後,這個物件定義了 valueOfConstant(和 valueOfTuple),它可以將單例型別(或單例型別的元組)轉換成它們的值。

object Type:
  given of[T <: AnyKind](using Quotes): Type[T] = ...
  def show[T <: AnyKind](using Type[T])(using Quotes): String = ...
  def valueOfConstant[T](using Type[T])(using Quotes): Option[T] = ...
  def valueOfTuple[T <: Tuple](using Type[T])(using Quotes): Option[T] = ...
Quotes

Quotes 介面定義了報價系統中大部分的原始操作。

報價定義所有 Expr[T] 方法作為擴充方法。Type[T] 沒有方法,因此不會出現在這裡。只要在目前的範圍中隱含地給予 Quotes,這些方法就會可用。

Quotes 執行個體也是透過 reflect 物件進入 反射 API 的入口點。

最後,Quotes 提供在報價模式配對 (QuoteMatching) 中報價取消序列化 (QuoteUnpickler) 所使用的內部邏輯。這些介面會加入特質的自訂類型,以確保它們會在這個物件上實作,但對 Quotes 的使用者不可見。

在內部,Quotes 的實作也會追蹤其目前的拼接範圍 Scope。這個範圍會附加到使用這個 Quotes 執行個體建立的任何表達式。

trait Quotes:
  this: runtime.QuoteUnpickler & runtime.QuoteMatching =>

  extension [T](self: Expr[T])
    def show: String
    def matches(that: Expr[Any]): Boolean
    def value(using FromExpr[T]): Option[T]
    def valueOrAbort(using FromExpr[T]): T
  end extension

  extension (self: Expr[Any])
    def isExprOf[X](using Type[X]): Boolean
    def asExprOf[X](using Type[X]): Expr[X]
  end extension

  // abstract object reflect ...
Scope

拼接內容會表示為 Scope 物件的堆疊 (不可變清單)。每個 Scope 都包含拼接的位置 (用於錯誤回報) 和對封裝拼接範圍 Scope 的參考。如果另一個範圍包含在父範圍中,則範圍會是另一個範圍的子範圍。當使用 Quotes 中的目前範圍中提供的 ScopeExprType 中的 Scope 將表達式拼接至另一個表達式時,會執行這個檢查。

進入點

多階段程式設計的兩個進入點是巨集和 run 操作。

巨集

內聯巨集定義將內聯頂層拼接(未嵌套在引號中的拼接)。此拼接需要在編譯時評估。在避免完整解釋器[^1]中,我們說明了以下限制

  • 頂層拼接必須包含對已編譯靜態方法的單一呼叫。
  • 函數參數為文字常數、引號表達式(參數)、類型參數的 Type.of 和對 Quotes 的參照。

這些限制使得解釋器的實作相當簡單。Java Reflection 用於呼叫頂層拼接中的單一函數呼叫。該函數的執行完全在已編譯的位元組碼上完成。這些是 Scala 靜態方法,可能並不總是變成 Java 靜態方法,它們可能位於模組物件中。由於模組編碼為類別實例,因此我們需要在呼叫方法之前,解釋方法的前綴以實例化它。

參數的程式碼尚未編譯,因此需要由編譯器解釋。解釋文字常數就像從表示文字的 AST 中提取常數一樣簡單。在解釋引號表達式時,引號的內容保留為 AST,並包裝在 Expr 的實作中。對 Type.of[T] 的呼叫也會將類型的 AST 包裝在 Type 的實作中。最後,對 Quotes 的參照應該是對拼接提供的引號的參照。此參照被解釋為 Quotes 的新實例,其中包含沒有父項目的全新初始 Scope

透過 Java Reflection 呼叫方法的結果將傳回一個包含由該巨集實作產生的新 AST 的 Expr。檢查此 Expr 的範圍,以確保它沒有從某些拼接或 run 作業中擠出。然後從 Expr 中提取 AST,並將其插入為包含頂層拼接的 AST 的替換。

執行階段多階段程式設計

為了能夠編譯程式碼,scala.quoted.staging 函式庫定義了 Compiler 特質。staging.Compiler 的實例是對一般 Scala~3 編譯器的包裝。要實例化,它需要應用程式的 JVM 類別載入器實例。

import scala.quoted.staging.*
given Compiler = Compiler.make(getClass.getClassLoader)

編譯器需要類別載入器來得知已載入哪些相依性,並使用相同的類別載入器載入已產生的程式碼。以下是傳遞給 staging.run 的範例方法 mkPower2

def mkPower2()(using Quotes): Expr[Double => Double] = ...

run(mkPower2())

若要執行前一個範例,編譯器會建立等同於下列類別的程式碼,並使用沒有父項目的新 Scope 編譯它。

class RunInstance:
  def exec(): Double => Double = ${ mkPower2() }

最後,run 會詮釋 (new RunInstance).exec() 以評估引號的內容。為此,結果的 RunInstance 類別會使用 Java 反射載入 JVM,實例化,然後呼叫 exec 方法。

編譯

引號和接合在已產生的已類型化抽象語法樹中是原始形式。這些需要使用一些額外規則進行類型檢查,例如,需要檢查分段層級,並且需要調整對一般類型的參照。最後,需要編碼 (序列化/醃漬) 和解碼 (反序列化/解除醃漬) 會在執行階段產生的引號表達式。

類型化引號表達式

使用 Expr 進行引號表達式和接合的類型化程序相對簡單。其核心是,引號會簡化為對 quote 的呼叫,接合會簡化為對 splice 的呼叫。我們會在簡化為這些方法時追蹤引號層級。

def quote[T](x: T): Quotes ?=> Expr[T]

def splice[T](x: Quotes ?=> Expr[T]): T

如果使用者直接撰寫對這些方法的呼叫,將無法追蹤引號層級。若要得知是否呼叫其中一個方法,我們需要先對其進行類型化,但若要對其進行類型化,我們需要得知是否為這些方法之一以更新引號層級。因此,這些方法只能由編譯器使用。

在執行階段,接合需要參照建立其周圍引號的 Quotes。為了簡化後續階段,我們會追蹤目前的 Quotes,並使用 nestedSplice (而非 splice) 直接在接合中編碼參照。

def nestedSplice[T](q: Quotes)(x: q.Nested ?=> Expr[T]): T

有了這個新增功能,原始的 splice 只用於頂層 splice。

這些層級主要用於識別在輸入時需要評估的頂層 splice。我們不使用引號層級來影響輸入程序。層級檢查會在後續階段執行。這可確保引號中的來源表達式與引號外的來源表達式具有相同的精緻度。

引號模式比對

模式比對定義在特質 QuoteMatching 中,它是 Quotes 自我類型的部分。它由 Quotes 實作,但 Quotes 的使用者無法使用。若要存取它,編譯器會產生從 QuotesQuoteMatching 的強制轉型,然後選取其兩個成員之一:ExprMatchTypeMatchExprMatch 定義一個 unapply 抽取器方法,用於編碼引號模式,而 TypeMatch 定義一個 unapply 方法,用於引號類型模式。

trait Quotes:
  self: runtime.QuoteMatching & ...  =>
  ...

trait QuoteMatching:
  object ExprMatch:
    def unapply[TypeBindings <: Tuple, Tup <: Tuple]
               (scrutinee: Expr[Any])
               (using pattern: Expr[Any]): Option[Tup] = ...
  object TypeMatch:
    ...

這些抽取器方法僅供編譯器產生的程式碼使用。已產生的對抽取器的呼叫具有已精緻的形式,無法以來源撰寫,即明確的類型參數和明確的內容參數。

此抽取器會傳回一個元組類型 Tup,無法從方法簽章中的類型推論。此類型會在輸入引號模式時計算,並會明確新增到抽取器呼叫中。若要參照 Tup 中任意位置的類型變數,我們需要在使用之前定義它們,因此我們有 TypeBindings,它會包含所有模式類型變數定義。抽取器也會接收一個給定的參數,類型為 Expr[Any],其中會包含表示模式的表達式。編譯器會明確新增此模式表達式。我們使用給定的參數,因為這些是我們允許在模式位置新增到抽取器呼叫中的唯一參數。

這個萃取器有點複雜,但它編碼了所有特定於引號的功能。它將模式編譯成模式比對器編譯階段可以理解的表示形式。

引號模式編碼成兩個部分:負責萃取比對結果的元組模式,以及代表模式的引號表達式。例如,如果模式沒有 $,我們將使用 EmptyTuple 作為模式,並使用 '{1} 來代表模式。

case '{ 1 } =>
// is elaborated to
  case ExprMatch(EmptyTuple)(using '{1}) =>
//               ^^^^^^^^^^  ^^^^^^^^^^
//                pattern    expression

在萃取表達式時,包含在拼接 ${..} 中的每個模式將按順序放置在元組模式中。在以下情況中,fx 會被放置在元組模式 (f, x) 中。元組的類型編碼在 Tup 中,而不在元組本身中。否則,萃取器會傳回一個元組 Tuple,必須測試其類型,而這又因為類型擦除而不可能。

case '{ ((y: Int) => $f(y)).apply($x) } =>
// is elaborated to
  case ExprMatch[.., (Expr[Int => Int], Expr[Int])]((f, x))(using pattern) =>
// pattern = '{ ((y: Int) => pat[Int](y)).apply(pat[Int]()) }

引號的內容會透過將拼接替換為標記表達式 pat[T](..) 而轉換成有效的引號表達式。類型 T 取自拼接的類型,而參數是 HOAS 參數。這表示 pat[T]() 是封閉模式,而 pat[T](y) 是可以參考 y 的 HOAS 模式。

引號模式中的類型變數會先正規化,將所有定義置於模式的開頭。對於模式中類型變數 t 的每個定義,我們會在 TypeBindings 中新增一個類型變數定義。每個定義都會有一個對應的 Type[t],如果模式相符,則會將其提取出來。這些 Type[t] 也會列在 Tup 中,並新增至元組模式。此外,在模式中標記為 using,以使其在此情況分支中隱式可用。

case '{ type t; ($xs: List[t]).map[t](identity[t]) } =>
// is elaborated to
  case ExprMatch[(t), (Type[t], Expr[List[t]])]((using t, xs))(using p) =>
//               ^^^  ^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^  ^^^^^^^
//     type bindings        result type            pattern     expression
// p = '{ @patternType type u; pat[List[u]]().map[u](identity[u]) }

引號的內容會轉換成有效的引號表達式,方法是將類型變數替換為不會超出引號範圍的新變數。這些變數也會加上註解,以便輕鬆辨識為模式變數。

層級一致性檢查

層級一致性檢查會在程式輸入類型後執行,作為靜態檢查。若要檢查層級一致性,我們會由上而下遍歷樹狀結構,記住階段化層級的內容。範圍中的每個區域定義都會記錄其層級,並將每個定義的術語參照與目前的階段化層級進行比對。

// level 0
'{ // level 1
  val x = ... // level 1 with (x -> 1)
  ${ // level 0 (x -> 1)
    val y = ... // level 0 with (x -> 1, y -> 0)
    x // error: defined at level 1 but used in level 0
  }
  // level 1 (x -> 1)
  x // x is ok
}

類型修復

在後續階段使用泛型類型 T 時,範圍中必須有指定的 Type[T]。編譯器需要辨識這些參照,並將它們連結至 Type[T] 的執行個體。例如,請考慮以下範例

def emptyList[T](using t: Type[T])(using Quotes): Expr[List[T]] =
  '{ List.empty[T] }

對於每個定義在層級 0,並在層級 1 或更高層級使用的泛型類型 T 參照,編譯器會呼叫 Type[T]。這通常是提供為參數的指定類型,在本例中為 t。我們可以使用類型 t.Underlying 來取代 T,因為它是該類型的別名。但 t.Underlying 包含額外的資訊,指出會在引號的評估中使用 t。在某種意義上,Underlying 會像類型的拼接。

def emptyList[T](using t: Type[T])(using Quotes): Expr[List[T]] =
  '{ List.empty[t.Underlying] }

由於一些技術限制,無法總是將類型參照替換為包含 t.Underlying 的 AST。為了克服此限制,我們可以在引號開頭定義一個類型別名清單,並在那裡插入 t.Underlying。這樣做有一個額外的好處,就是我們不必重複在引號中插入 t.Underlying

def emptyList[T](using t: Type[T])(using Quotes): Expr[List[T]] =
  '{ type U = t.Underlying; List.empty[U] }

這些別名可以在引號內的任何層級使用,且此轉換僅對處於層級 0 的引號執行。

'{ List.empty[T] ... '{ List.empty[T] } ... }
// becomes
  '{ type U = t.Underlying; List.empty[U] ... '{ List.empty[U] } ... }

如果我們在層級 1 或更高層級定義一個泛型類型,它將不受此轉換影響。在某些未來的編譯階段,當泛型類型的定義處於層級 0 時,它將受到此轉換影響。這簡化了轉換邏輯,並避免將編碼洩漏到巨集可以檢查的程式碼中。

'{
  def emptyList[T: Type](using Quotes): Expr[List[T]] = '{ List.empty[T] }
  ...
}

Type.of[T] 執行類似的轉換。T 中的任何泛型類型都需要在範圍內具有隱式給定的 Type[T],這也將用作路徑。範例

def empty[T](using t: Type[T])(using Quotes): Expr[T] =
  Type.of[T] match ...
// becomes
def empty[T](using t: Type[T])(using Quotes): Expr[T] =
  Type.of[t.Underlying] match ...
// then becomes
def empty[T](using t: Type[T])(using Quotes): Expr[T] =
  t match ...

運算 Type.of[t.Underlying] 可以最佳化為僅 t。但這並非總是如此。如果泛型參照嵌套在類型中,我們將需要保留 Type.of

def matchOnList[T](using t: Type[T])(using Quotes): Expr[List[T]] =
  Type.of[List[T]] match ...
// becomes
def matchOnList[T](using t: Type[T])(using Quotes): Expr[List[T]] =
  Type.of[List[t.Underlying]] match ...

透過執行此轉換,我們確保在 Type.of 中使用的每個抽象類型 U 在範圍內具有隱式 Type[U]。此表示使識別類型中靜態已知的部份與動態已知的部份變得更簡單。類型別名也會新增到 Type.of 的類型中,儘管這些不是有效的原始碼。如果以原始碼撰寫,這些看起來會像 Type.of[{type U = t.Underlying; Map[U, U]}]

拼接正規化

拼接的內容可能會參考封閉引號中定義的變數。這會讓引號內容序列化過程變得複雜。為了簡化序列化,我們會先轉換每個層級 1 拼接的內容。考慮以下範例

def power5to(n: Expr[Int]): Expr[Double] = '{
  val x: Int = 5
  ${ powerCode('{x}, n) }
}

變數 x 在引號中定義,並在拼接中使用。正規形式會萃取所有對 x 的參照,並用 x 的分段版本取代它們。我們會用 $y 取代型別為 Tx 參照,其中 y 的型別為 Expr[T]。然後我們用定義 y 的 lambda 包裝拼接的新內容,並將其套用至 x 的引號版本。經過此轉換後,我們會有 2 個部分,一個不參照引號的 lambda,它知道如何計算拼接的內容,以及一系列參照 lambda 中定義變數的引號引數。

def power5to(n: Expr[Int]): Expr[Double] = '{
  val x: Int = 5
  ${ ((y: Expr[Int]) => powerCode('{$y}, n)).apply('x) }
}

一般而言,拼接正規形式的格式為 ${ <lambda>.apply(<args>*) },並有以下限制

  • <lambda> 一個不參照外部引號中定義變數的 lambda 表達式
  • <args> 一系列引號表達式或 Type.of,其中包含參照封閉引號中定義變數,但不參照封閉引號外部定義的區域變數
函式參照正規化

參照接收參數的函式 f 在 Scala 中不是有效值。此類函式參照 f 可 eta 展開為 x => f(x),以用作 lambda 值。因此,函式參照無法像其他表達式一樣直接透過正規化轉換,因為我們無法用方法參照型別表示 '{f}。我們可以在正規化形式中使用 f 的 eta 展開形式。例如,考慮以下對 f 的參照。

'{
  def f(a: Int)(b: Int, c: Int): Int = 2 + a + b + c
  ${ '{ f(3)(4, 5) } }
}

若要將此程式碼正規化,我們可以 eta 展開對 f 的參照,並將其置於包含適當表達式的引號中。因此,參數 '{f} 的正規化形式會變成引號中的 lambda '{ (a: Int) => (b: Int, c: Int) => f(a)(b, c) },且為類型 Expr[Int => (Int, Int) => Int] 的表達式。eta 展開會為每個參數清單產生一個 curried lambda。應用程式 f(3)(4, 5) 不會變成 $g(3)(4, 5),而是 $g.apply(3).apply(4, 5)。我們加入 apply,因為 g 不是對函式的引號參照,而是 curried lambda。

'{
  def f(a: Int)(b: Int, c: Int): Int = 2 + a + b + c
  ${
    (
      (g: Expr[Int => (Int, Int) => Int]) => '{$g.apply(3).apply(4, 5)}
    ).apply('{ (a: Int) => (b: Int, c: Int) => f(a)(b, c) })
  }
}

然後,我們可以在產生程式碼時套用它,並對應用程式進行 beta 簡約。

(g: Expr[Int => Int => Int]) => betaReduce('{$g.apply(3).apply(4)})
變數指派正規化

指派運算式左側的可變變數參照無法直接轉換,因為它不在表達式位置中。

'{
  var x: Int = 5
  ${ g('{x = 2}) }
}

我們可以使用相同於函式參照的策略,透過 eta 展開指派運算 x = _ 成為 y => x = y

'{
  var x: Int = 5
  ${
    g(
      (
        (f: Expr[Int => Unit]) => betaReduce('{$f(2)})
      ).apply('{ (y: Int) => x = $y })
    )
  }
}
類型正規化

在引號中定義的類型會受到類似的轉換。在此範例中,T 在引號中的層級 1 中定義,並在層級 1 中的 splice 中再次使用。

'{ def f[T] = ${ '{g[T]} } }

正規化會將 Type[T] 加入 lambda,我們會插入此參照。不同的是,它會加入一個類似於類型修復中使用的別名。在此範例中,我們會建立一個 type U,作為階段類型別名。

'{
  def f[T] = ${
    (
      (t: Type[T]) => '{type U = t.Underling; g[U]}
    ).apply(Type.of[T])
  }
}

序列化

引用的程式碼需要被醃製,以便在下次編譯階段中在執行階段使用。我們透過將 AST 醃製為 TASTy 二進位檔來實作這項功能。

TASTy

TASTy 格式是 Scala 3 的型別抽象語法樹序列化格式。它通常會在型別檢查後醃製完全展開的程式碼,並與產生的 Java 類別檔一起保留。

醃製

我們使用 TASTy 作為引號內容的序列化格式。為了說明如何執行序列化,我們將使用以下範例。

'{
  val (x, n): (Double, Int) = (5, 2)
  ${ powerCode('{x}, '{n}) } * ${ powerCode('{2}, '{n}) }
}

在正規化拼接時,此引號會轉換成以下程式碼。

'{
  val (x, n): (Double, Int) = (5, 2)
  ${
    ((y: Expr[Double], m: Expr[Int]) => powerCode(y, m)).apply('x, 'n)
  } * ${
    ((m: Expr[Int]) => powerCode('{2}, m)).apply('n)
  }
}

拼接正規化是序列化程序的關鍵部分,因為它僅允許在拼接中 lambda 的引數中參照引號中定義的變數。這使得可以輕鬆建立引號的封閉表示。第一步是移除所有拼接,並以洞取代它們。洞類似於拼接,但它不知道如何計算拼接的內容。相反地,它知道洞的索引和拼接引數的內容。我們可以在以下範例中看到此轉換,其中洞由 << idx; holeType; args* >> 表示。

${ ((y: Expr[Double], m: Expr[Int]) => powerCode(y, m)).apply('x, 'n) }
// becomes
  << 0; Double; x, n >>

由於這是第一個洞,因此它的索引為 0。洞的型別為 Double,現在需要記住它,因為我們無法從拼接的內容推斷它。拼接的引數為 xn;請注意,它們不需要引號,因為它們已從拼接中移出。

對已修復型別的參照會以類似的方式處理。考慮 emptyList 範例,它顯示插入引號中的型別別名。

'{ List.empty[T] }
// type healed to
'{ type U = t.Underlying; List.empty[U] }

我們不取代拼接,而是以型別洞取代 t.Underlying 型別。型別洞由 << idx; bounds >> 表示。

'{ type U = << 0; Nothing..Any >>; List.empty[U] }

在此,Nothing..Any 的界限是原始 T 型別的界限。Type.of 的型別會以相同的方式轉換。

有了這些轉換,保證引號或 Type.of 的內容已封閉,因此可以進行封存。AST 封存到 TASTy 中,TASTy 是位元組序列。這個位元組序列需要在位元組碼中實例化,但遺憾的是它無法以位元組形式傾印到類別檔案中。為了具體化它,我們將位元組編碼成 Java String。在以下範例中,我們以人類可讀的形式顯示此編碼,並使用虛構的 |tasty"..."| 字串文字。

// pickled AST bytes encoded in a base64 string
tasty"""
  val (x, n): (Double, Int) = (5, 2)
  << 0; Double; x, n >> * << 1; Double; n >>
"""
// or
tasty"""
  type U = << 0; Nothing..Any; >>
  List.empty[U]
"""

引號或 Type.of 的內容並非總是封存的。在某些情況下,最好產生等效(較小和/或較快)的程式碼來計算表達式。文字值編譯成對 Expr(<literal>) 的呼叫,使用 ToExpr 的實作來建立引號表達式。這目前僅在文字值上執行,但可以延伸到我們在標準函式庫中定義 ToExpr 的任何值。類似地,對於非泛型類型,我們可以使用它們各自的 java.lang.Class,並使用反射 API 中定義的基元操作 typeConstructorOf 將它們轉換成 Type

解封存

現在我們已經了解引號是如何封存的,我們可以看看如何解封存它。我們將繼續之前的範例。

使用孔洞來取代引號中的接合處。當我們執行這個轉換時,我們也需要記住接合處中的 lambda 和它們的孔洞索引。在解封存孔洞時,對應的接合處 lambda 會用來計算孔洞的內容。lambda 會接收孔洞引數的引號版本作為參數。例如,要計算 << 0; Double; x, n >> 的內容,我們會評估以下程式碼

((y: Expr[Double], m: Expr[Int]) => powerCode(y, m)).apply('x, 'n)

評估並非看起來那麼簡單,因為 lambda 來自編譯碼,而其餘必須解釋的代碼。我們將 xn 的 AST 放入 Expr 物件中以模擬引號,然後使用 Java 反射呼叫 apply 方法。

引號中可能有很多洞,因此可能有很多 lambda。為避免實例化許多 lambda,我們可以將它們合併成一個 lambda。除了參數清單之外,此 lambda 還會擷取正在評估的洞的索引。它會對索引執行切換比對,並在每個分支中呼叫對應的 lambda。每個分支也會根據 lambda 的定義擷取參數。原始 lambda 的應用會進行 beta 縮減,以避免額外的負擔。

(idx: Int, args: Seq[Any]) =>
  idx match
    case 0 => // for << 0; Double; x, n >>
      val x = args(0).asInstanceOf[Expr[Double]]
      val n = args(1).asInstanceOf[Expr[Int]]
      powerCode(x, n)
    case 1 => // for << 1; Double; n >>
      val n = args(0).asInstanceOf[Expr[Int]]
      powerCode('{2}, n)

這類似於我們對接合進行的操作,當我們替換帶有洞的類型別名時,我們會追蹤洞的索引。我們不會有 lambda,而是會有指向 Type 實例的參考清單。從以下範例中,我們會擷取 tu 等。

'{ type T1 = t1.Underlying; type Tn = tn.Underlying; ... }
// with holes
  '{ type T1 = << 0; ... >>; type Tn = << n-1; ... >>; ... }

由於類型洞位於引號的開頭,因此它們會具有前 N 個索引。這表示我們可以將參考放入序列 Seq(t, u, ...) 中,其中序列中的索引與洞索引相同。

最後,引號本身會替換為對 QuoteUnpickler.unpickleExpr 的呼叫,這會取消封裝 AST、評估洞(即接合),並將結果 AST 包裝在 Expr[Int] 中。此方法會擷取封裝的 |tasty"..."|、類型和洞 lambda。類似地,Type.of 會替換為對 QuoteUnpickler.unpickleType 的呼叫,但只會收到封裝的 |tasty"..."| 和類型。由於 QuoteUnpicklerQuotes 類別自類型的一部分,因此我們必須轉換實例,但知道此轉換總是會成功。

quotes.asInstanceOf[runtime.QuoteUnpickler].unpickleExpr[T](
  pickled = tasty"...",
  types = Seq(...),
  holes = (idx: Int, args: Seq[Any]) => idx match ...
)

[^1]: Scala 3 中的可擴充巨程式設計 [^2]: 使用產生式巨集和分析巨集的多階段程式設計。[^3]: 在引號中,以 $ 開頭的識別碼必須以反引號 (`$`) 包圍。例如,scala.Predef 中的 $conforms