Как я могу повторно использовать поддеременты определения (AST) в макросе?

Я работаю над встроенным DSL Scala, и макросы становятся основным инструментом для достижения моих целей. Я получаю сообщение об ошибке при попытке повторно использовать поддерево из входящего макроопределения в полученный. Ситуация довольно сложная, но (надеюсь) я упростил ее для ее понимания.

Предположим, что у нас есть этот код:

val y = transform {
  val x = 3
  x
}
println(y) // prints 3

где "transform" - вовлеченный макрос. Хотя может показаться, что он ничего не делает, он действительно преобразует показанный блок в это выражение:

3 match { case x => x }

Выполняется эта реализация макроса:

def transform(c: Context)(block: c.Expr[Int]): c.Expr[Int] = {
  import c.universe._
  import definitions._

  block.tree match {
    /* {
     *   val xNam = xVal
     *   xExp
     * }
     */
    case Block(List(ValDef(_, xNam, _, xVal)), xExp) =>
      println("# " + showRaw(xExp)) // prints Ident(newTermName("x"))
      c.Expr(
        Match(
          xVal, 
          List(CaseDef(
            Bind(xNam, Ident(newTermName("_"))),
            EmptyTree,
            /* xExp */ Ident(newTermName("x")) ))))
    case _ => 
      c.error(c.enclosingPosition, "Can't transform block to function")
      block  // keep original expression
  }
}

Обратите внимание, что xNam соответствует имени переменной, xVal соответствует его ассоциированному значению и, наконец, xExp соответствует выражению, содержащему переменную. Ну, если я печатаю исходное дерево xExp, я получаю Ident (newTermName ( "x" )), и это именно то, что установлено в случае RHS. Поскольку выражение может быть изменено (например, x + 2 вместо x), это не подходит для меня. То, что я хочу сделать, - это повторно использовать дерево xExp (см. Комментарий xExp), изменяя значение "x" (это определение во входном выражении, но будет случайной переменной LHS на выходе), но оно запускает суммарная ошибка:

symbol value x does not exist in org.habla.main.Main$delayedInit$body.apply); see the error output for details.

Мое текущее решение состоит в синтаксическом анализе xExp для поддержки всех Идентов с новыми, но это полностью зависит от внутренних компонентов компилятора и, следовательно, временного обходного пути. Очевидно, что xExp поставляется с дополнительной информацией, которую предлагает showRaw. Как я могу очистить этот xExp для того, чтобы позволить "x" использовать переменную case? Может ли кто-нибудь объяснить всю картину этой ошибки?

PS: Я безуспешно пытаюсь использовать семейство методов substitute * из TreeApi, но мне не хватает основ, чтобы понять его последствия.

Ответ 1

Разборка входных выражений и их повторная сборка по-другому - важный сценарий в макрологии (это то, что мы делаем внутри макроса reify). Но, к сожалению, на данный момент это не особенно легко.

Проблема заключается в том, что входные аргументы макропроцессорной реализации макросов уже проверяются typechecked. Это и благословение, и проклятие.

Особый интерес для нас представляет тот факт, что переменные привязки в деревьях, соответствующие аргументам, уже установлены. Это означает, что все узлы Ident и Select имеют заполненные поля sym, указывая на определения, к которым относятся эти узлы.

Вот пример того, как работают символы. Я скопирую/вставляю распечатку с одного из моих разговоров (я не даю ссылку здесь, потому что большая часть информации на моих переговорах устарела до сих пор, но эта конкретная распечатка имеет вечную полезность):

>cat Foo.scala
def foo[T: TypeTag](x: Any) = x.asInstanceOf[T]
foo[Long](42)

>scalac -Xprint:typer -uniqid Foo.scala
[[syntax trees at end of typer]]// Scala source: Foo.scala
def foo#8339
  [T#8340 >: Nothing#4658 <: Any#4657]
  (x#9529: Any#4657)
  (implicit evidence$1#9530: TypeTag#7861[T#8341])
  : T#8340 =
x#9529.asInstanceOf#6023[T#8341];
Test#14.this.foo#8339[Long#1641](42)(scala#29.reflect#2514.`package`#3414.mirror#3463.TypeTag#10351.Long#10361)

Напомним, мы пишем небольшой фрагмент, а затем скомпилируем его с помощью scalac, попросив компилятор выгрузить деревья после фазы typer, распечатав уникальные идентификаторы символов, назначенных деревьям (если они есть).

В полученной распечатке мы видим, что идентификаторы связаны с соответствующими определениями. Например, с одной стороны, ValDef("x", ...), представляющий параметр метода foo, определяет символ метода с id = 9529. С другой стороны, Ident("x") в теле метода получил поле sym, установленное на тот же символ, который устанавливает привязку.

Хорошо, мы видели, как привязки работают в scalac, и теперь самое время представить фундаментальный факт.

If a symbol has been assigned to an AST node, 
then subsequent typechecks will never reassign it. 

Вот почему reify является гигиеничным. Вы можете взять результат reify и вставить его в произвольное дерево (которое, возможно, определяет переменные с конфликтующими именами) - исходные привязки останутся неповрежденными. Это работает, потому что reify сохраняет исходные символы, поэтому последующие typechecks не будут перепечатывать восстановленные узлы AST.

Теперь мы все настроены на объяснение ошибки, с которой вы сталкиваетесь:

symbol value x does not exist in org.habla.main.Main$delayedInit$body.apply); see the error output for details.

Аргумент макроса transform содержит как определение, так и ссылку на переменную x. Как мы только что узнали, это означает, что соответствующие ValDef и Ident будут синхронизированы с полями sym. Пока что так хорошо.

Однако, к сожалению, макрос искажает установленную привязку. Он воссоздает ValDef, но не очищает поле sym соответствующего идентификатора. Последующая typecheck присваивает новый символ вновь созданному ValDef, но не затрагивает оригинальный Иден, который скопирован в результирующий результат.

После typecheck исходный идентификатор указывает на символ, который больше не существует (это именно то, что говорило сообщение об ошибке:)), что приводит к сбою во время генерации байт-кода.

Итак, как мы исправим ошибку? К сожалению, нет простого ответа.

Одним из вариантов было бы использовать c.resetLocalAttrs, который рекурсивно стирает все символы в данном AST node. Последующая typecheck затем восстановит привязки, предоставленные тем, что созданный вами код не возится с ними (если, например, вы переносите xExp в блок, который сам определяет значение с именем x, тогда у вас проблемы).

Другой вариант - играть с символами. Например, вы можете написать свой собственный resetLocalAttrs, который удаляет только поврежденные привязки и не затрагивает действительные. Вы могли бы также попытаться назначить символы самостоятельно, но это короткий путь к безумию, хотя иногда один из них вынужден ходить по нему.

Не круто, согласен. Мы это осознаем и намерены иногда исправить эту фундаментальную проблему. Однако сейчас наши руки заполнены исправлением ошибок до окончательной версии 2.10.0, поэтому мы не сможем решить проблему в ближайшем будущем. UPD. Для получения дополнительной информации см. https://groups.google.com/forum/#!topic/scala-internals/rIyJ4yHdPDU.


Нижняя линия. Плохие вещи случаются, потому что привязки перепутаны. Сначала попробуйте resetLocalAttrs, и если это не сработает, подготовьтесь к тяжелой работе.