Запускаем код на Go снизу вверх
Всем привет! В продолжение прошлой статьи, где мы залезли в компилятор Go и разобрались в его работе, добавив поддержку while
на уровне компилятора, я хочу чуть глубже погрузиться в тему. Советую прочитать ту статью, если вы еще не сделали этого.
В этой статье, как небольшое дополнение к предыдущей, я хочу рассмотреть, как Go работает с AST, и заодно реализовать конструкцию InverseCode{}
(название Inverse
оказалось занято пакетом math
), которая будет читать код снизу вверх.
Вот примерный результат, который мы получим:
alek.dmitriev@alek-dmitriev habr % cat main.go
package main
import "fmt"
func main() {
inversecode {
fmt.Println("start")
fmt.Println("end")
}
}
alek.dmitriev@alek-dmitriev habr % go/bin/go run main.go
end
start
Предподготовка.
Первым делом добавим нашу конструкцию InverseCode{}
в компилятор. Чтобы не повторяться, отмечу, что большинство шагов схожи с теми, что описаны в предыдущей статье. Однако есть и некоторые отличия.
syntax/nodes.go
В отличие от цикла while
, в данном случае нам не нужны дополнительные операторы, такие как условие, инициализация или пост-действие. Поэтому в нашей структуре InverseCodeStmt
оставим только поле body, в котором будет находиться сам код.
WhileStmt struct {
Init SimpleStmt
Cond Expr
Post SimpleStmt
Body *BlockStmt
stmt
}
InverseCodeStmt struct {
Body *BlockStmt
stmt
}
Погружаемся в структуру Body
Давайте внимательнее рассмотрим, что представляет собой поле Body
. По сути, это ещё один statement
, с типомBlockStmt
. В Go BlockStmt
относится к оператору { }
, то есть к блоку кода. В Go можно использовать фигурные скобки без операторов, и внутри них будет своя область видимости. Хотя в реальной жизни я не встречал практического применения этой возможности, всё равно интересно, что она существует.
Внутри блока мы увидим следующий код:
BlockStmt struct {
List []Stmt
Rbrace Pos
stmt
}
Что такое BlockStmt?
Реализует интерфейс
Stmt
: это означает, что блок кода сам по себе является оператором.Хранит слайс других
Stmt
: внутри блока могут находиться другие операторы, которые также могут быть блоками. Это создает возможность для большой вложенности.
while True {
if a == b {
if b != c {
}
} else {
}
}

Все операторы (Stmt), которые находятся внутри блока, добавляются в слайс в определённом порядке. Именно этот порядок определяет, как код будет обрабатываться и выполняться. Чтобы реализовать нашу задумку — выполнение кода в обратном порядке — нам нужно изменить порядок записи этих операторов в слайс или изменить порядок чтения.
Меняем порядок выполнения кода.
В этой статье мы опустим полный цикл компилятора и сосредоточимся на том, как строится AST (абстрактное синтаксическое дерево) и как происходит его обход. Чтобы понять, как именно мы можем изменить порядок выполнения кода, нам нужно подробнее изучить пакеты cmd/compile/internal/syntax
и cmd/compile/internal/walk
. Именно здесь происходит построение, обход и преобразование дерева перед генерацией машинного кода.
Построение дерева
Чтобы изменить порядок выполнения программы на этапе построения AST, нам нужно изменить логику записи операторов (statements
) в слайс. Запись происходит во время парсинга, поэтому давайте перейдем в файл parser.go
и внимательно изучим, что там происходит.
func (p *parser) inverseCodeStmt() Stmt {
if trace {
defer p.trace("inverseCodeStmt")()
}
s := new(InverseCodeStmt)
s.pos = p.pos()
s.Body = p.blockStmt("inversecode clause")
return s
}
На примере WhileStmt
мы можем написать обработчик для inverseCode
. Однако, в отличие от WhileStmt
, нам не нужны переменные Init
и Cond
. В нашем случае они отсутствуют. Вместо этого нас интересует только Body
, который представляет собой обработчик для блока.
// context must be a non-empty string unless we know that p.tok == _Lbrace.
func (p *parser) blockStmt(context string) *BlockStmt {
if trace {
defer p.trace("blockStmt")()
}
s := new(BlockStmt)
s.pos = p.pos()
// people coming from C may forget that braces are mandatory in Go
if !p.got(_Lbrace) {
p.syntaxError("expected { after " + context)
p.advance(_Name, _Rbrace)
s.Rbrace = p.pos() // in case we found "}"
if p.got(_Rbrace) {
return s
}
}
s.List = p.stmtList()
s.Rbrace = p.pos()
p.want(_Rbrace)
return s
}
Обработка самого блока нас не интерсует. Давайте внимательно изучим метод stmtList
, который отвечает за заполнение слайса операторов (List
). Именно здесь происходит запись операторов в том порядке, в котором они встречаются в исходном коде. Этот метод вызывается при обработке блоков кода, таких как inverseCode
, while
, if
и других.
// StatementList = { Statement ";" } .
func (p *parser) stmtList() (l []Stmt) {
if trace {
defer p.trace("stmtList")()
}
for p.tok != _EOF && p.tok != _Rbrace && p.tok != _Case && p.tok != _Default {
s := p.stmtOrNil()
p.clearPragma()
if s == nil {
break
}
l = append(l, s)
// ";" is optional before "}"
if !p.got(_Semi) && p.tok != _Rbrace {
p.syntaxError("at end of statement")
p.advance(_Semi, _Rbrace, _Case, _Default)
p.got(_Semi) // avoid spurious empty statement
}
}
return
}
Что происходит в этом методе?
Цикл for:
Метод проходит по всем операторам в блоке кода, пока не достигнет конца файла (
_EOF
), закрывающей фигурной скобки (_Rbrace
) или ключевых словcase
/default
(в случаеswitch
).
Получение оператора:
s := p.stmtOrNil()
— получаем следующий оператор из исходного кода.Если оператор равен
nil
, цикл прерывается.
Запись оператора в слайс:
l = append(l, s)
— оператор добавляется в конец слайсаl
.Это ключевая строка, которая определяет порядок выполнения операторов.
Обработка точки с запятой:
В Go точка с запятой (;) не обязательна перед закрывающей фигурной скобкой (}).
Если точка с запятой отсутствует и следующий токен не является }, выводится ошибка синтаксиса.
Как изменить порядок записи операторов?
Чтобы изменить порядок выполнения кода на обратный, нам нужно изменить порядок записи операторов в слайс. Вместо добавления операторов в конец слайса, будем добавлять их в начало.
Измененный метод stmtList:
func (p *parser) stmtReverseList() (l []Stmt) {
if trace {
defer p.trace("stmtList")()
}
for p.tok != _EOF && p.tok != _Rbrace && p.tok != _Case && p.tok != _Default {
s := p.stmtOrNil()
p.clearPragma()
if s == nil {
break
}
// Добавляем оператор в начало слайса
l = append([]Stmt{s}, l...)
// ";" is optional before "}"
if !p.got(_Semi) && p.tok != _Rbrace {
p.syntaxError("at end of statement")
p.advance(_Semi, _Rbrace, _Case, _Default)
p.got(_Semi) // avoid spurious empty statement
}
}
return
}
Что изменилось?
l = append([]Stmt{s}, l...)
— оператор s добавляется в начало слайса l.В результате операторы будут записаны в обратном порядке.
Итого
Теперь, когда мы написали кастомный метод stmtReverseList
для записи операторов в обратном порядке, давайте интегрируем его в обработку нашего кастомного блока inverseCode
.
// context must be a non-empty string unless we know that p.tok == _Lbrace.
func (p *parser) blockReverseStmt(context string) *BlockStmt {
if trace {
defer p.trace("blockStmt")()
}
s := new(BlockStmt)
s.pos = p.pos()
// people coming from C may forget that braces are mandatory in Go
if !p.got(_Lbrace) {
p.syntaxError("expected { after " + context)
p.advance(_Name, _Rbrace)
s.Rbrace = p.pos() // in case we found "}"
if p.got(_Rbrace) {
return s
}
}
s.List = p.stmtList()
s.Rbrace = p.pos()
p.want(_Rbrace)
return s
}
func (p *parser) inverseCodeStmt() Stmt {
if trace {
defer p.trace("inverseCodeStmt")()
}
s := new(InverseCodeStmt)
s.pos = p.pos()
s.Body = p.blockInverseStmt("inversecode clause")
return s
}
Альтернативный подход: чтение statement’ов в обратном порядке
Запись дерева в обратном порядке — это не единственный способ решения задачи. Мы также можем изменить порядок чтения statement’ов (операторов) во время обхода AST. Давайте разберемся, как это можно сделать. Для этого перейдем в пакет walk
, где происходит обход и преобразование дерева перед генерацией машинного кода.
Шаг 1: Добавляем кейс для InverseCode
В методе walkStmt мы добавляем новый кейс для нашего оператора InverseCode. Этот кейс будет срабатывать, когда компилятор встречает наш оператор в AST. Внутри кейса мы преобразуем узел дерева в тип *ir.InverseCodeStmt и вызываем специальную функцию для его обработки.
case ir.OINVERSECODE:
n := n.(*ir.InverseCodeStmt)
return walkInverseCode(n)
Шаг 2: Реализуем функцию walkInverseCode
Функция walkInverseCode будет отвечать за обработку нашего оператора InverseCode. Её задача — пройтись по всем операторам внутри блока InverseCode и обработать их. Для этого мы вызываем вспомогательную функцию walkInverseCodeStmtList, которая отвечает за обход списка операторов.
func walkInverseCode(n *ir.InverseCodeStmt) ir.Node {
walkInverseCodeStmtList(n.Body)
return n
}
Шаг 3: Обход операторов в обратном порядке
Внутри функции walkInverseCodeStmtList мы обходим список операторов в обратном порядке. Для этого мы используем цикл, который начинается с последнего элемента списка и заканчивается первым. Каждый оператор обрабатывается с помощью функции walkStmt, которая рекурсивно обходит дерево.
func walkInverseCodeStmtList(s []ir.Node) {
// Обходим список операторов в обратном порядке
for i := len(s) - 1; i >= 0; i-- {
s[i] = walkStmt(s[i])
}
}
Итого
В итоге мы получаем две разные реализации одного и того же, но с одним большим НО. Возможно, внимательный читатель уже догадался об этом, но всё-таки полноценно этот код идти снизу вверх не будет. Так как операторы с вложенностью, такие как if
, else
, Block
и другие, он воспринимает как операторы целиком, не заглядывая внутрь блока. Команды в блоке будут работать в привычном нам порядке, но на первом уровне вложенности — в обратном. После компиляции мы получаем примерно следующее:
alek.dmitriev@alek-dmitriev habr % cat main.go
package main
import "fmt"
func main() {
inversecode {
if i == 1 {
if i == 1 {
fmt.Printf("start")
}
i = i + 1
}
if i == 0 {
if i == 0 {
fmt.Printf("end")
}
i = i + 1
}
i := 0
}
}
alek.dmitriev@alek-dmitriev habr % go/bin/go run main.go
end
start