Запускаем код на Go снизу вверх
- пятница, 14 марта 2025 г. в 00:00:15
Всем привет! В продолжение прошлой статьи, где мы залезли в компилятор 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{} в компилятор. Чтобы не повторяться, отмечу, что большинство шагов схожи с теми, что описаны в предыдущей статье. Однако есть и некоторые отличия.
В отличие от цикла while, в данном случае нам не нужны дополнительные операторы, такие как условие, инициализация или пост-действие. Поэтому в нашей структуре InverseCodeStmt оставим только поле body, в котором будет находиться сам код.
WhileStmt struct {
Init SimpleStmt
Cond Expr
Post SimpleStmt
Body *BlockStmt
stmt
}
InverseCodeStmt struct {
Body *BlockStmt
stmt
}Давайте внимательнее рассмотрим, что представляет собой поле Body. По сути, это ещё один statement, с типомBlockStmt. В Go BlockStmt относится к оператору { }, то есть к блоку кода. В Go можно использовать фигурные скобки без операторов, и внутри них будет своя область видимости. Хотя в реальной жизни я не встречал практического применения этой возможности, всё равно интересно, что она существует.
Внутри блока мы увидим следующий код:
BlockStmt struct {
List []Stmt
Rbrace Pos
stmt
}Реализует интерфейс 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 точка с запятой (;) не обязательна перед закрывающей фигурной скобкой (}).
Если точка с запятой отсутствует и следующий токен не является }, выводится ошибка синтаксиса.
Чтобы изменить порядок выполнения кода на обратный, нам нужно изменить порядок записи операторов в слайс. Вместо добавления операторов в конец слайса, будем добавлять их в начало.
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'ов (операторов) во время обхода AST. Давайте разберемся, как это можно сделать. Для этого перейдем в пакет walk, где происходит обход и преобразование дерева перед генерацией машинного кода.
В методе walkStmt мы добавляем новый кейс для нашего оператора InverseCode. Этот кейс будет срабатывать, когда компилятор встречает наш оператор в AST. Внутри кейса мы преобразуем узел дерева в тип *ir.InverseCodeStmt и вызываем специальную функцию для его обработки.
case ir.OINVERSECODE:
n := n.(*ir.InverseCodeStmt)
return walkInverseCode(n)Функция walkInverseCode будет отвечать за обработку нашего оператора InverseCode. Её задача — пройтись по всем операторам внутри блока InverseCode и обработать их. Для этого мы вызываем вспомогательную функцию walkInverseCodeStmtList, которая отвечает за обход списка операторов.
func walkInverseCode(n *ir.InverseCodeStmt) ir.Node {
walkInverseCodeStmtList(n.Body)
return n
}Внутри функции 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