Внутреннее устройство компилятора Nim
Исходный кодРедактировать"Абстракция — это наслаивание незнания поверх реальности." — Ричард Гейбл
Структура каталогов
Структура каталогов проекта Nim:
| Путь | Назначение |
|---|---|
bin |
сгенерированные двоичные файлы |
build |
сгенерированный C-код для установки |
compiler |
сам компилятор Nim; обратите внимание, что этот код был переведён из бутстраповой версии, написанной на Паскале, поэтому он не является образцом хорошего кода на Nim |
config |
файлы конфигурации для Nim |
dist |
дополнительные пакеты для дистрибутива |
doc |
документация; состоит из файлов reStructuredText |
lib |
библиотека Nim |
Запуск компилятора по принципу "bootstrap"
Примечание: Добавьте . в ваш PATH, чтобы koch можно было использовать без ./.
Компиляция компилятора — это простая задача, выполните:
nim c koch.nim koch boot -d:release
Для отладочной версии используйте:
nim c koch.nim koch boot
А для отладочной версии, совместимой с GDB:
nim c koch.nim koch boot --debuginfo --linedir:on
Программа koch — это скрипт обслуживания Nim. Он заменяет make и shell-скрипты, имея преимущество большей переносимости. Более подробную информацию о его параметрах можно найти в документации koch.
Воспроизводимые сборки
Установите метку времени компиляции с помощью переменной среды SOURCE_DATE_EPOCH.
export SOURCE_DATE_EPOCH=$(git log -n 1 --format=%at) koch boot # or `./build_all.sh`
Отладка компилятора
Бисекция для поиска регрессий
Иногда возникает ошибка, вызванная регрессией в компиляторе или стандартной библиотеке. Бисекция коммитов репозитория Nim — полезный инструмент для определения коммита, в котором произошла регрессия.
Даже если неизвестно, вызвана ли ошибка регрессией, бисекция может сократить время отладки, исключив это. Если ошибка окажется регрессией, вы сосредоточитесь на изменениях, внесённых конкретным коммитом.
koch temp возвращает 125 в качестве кода завершения в случае сбоя компиляции компилятора. Этот код завершения сообщает git bisect пропустить текущий коммит:
git bisect start bad-commit good-commit git bisect run ./koch temp -r c test-source.nim
Вы также можете выполнить бисекцию, используя пользовательские параметры для сборки компилятора, например, если вам не нужна отладочная версия компилятора (которая работает медленнее), вы можете заменить ./koch temp явным командным компиляцией, см. Запуск компилятора по принципу «bootstrap».
См. также:
- Многоплатформенная бисекция C/Cpp/Valgrind/JS в GitHub: https://github.com/juancarlospaco/nimrun-action#examples
Сборка отлаженного компилятора
Поскольку полезный метод отладки компилятора — это вставка отладочной регистрации или изменение кода с последующим наблюдением за результатом тестового случая, быстрее всего собрать отлаженный компилятор из существующей релизной сборки. koch temp предоставляет удобный способ сделать именно это.
По умолчанию koch temp создаст лёгкую версию компилятора с включённой -d:debug. Компилятор по умолчанию записывается в bin/nim_temp. Лёгкая версия компилятора не включает JS и генерацию документации.
bin/nim_temp можно напрямую использовать для запуска тестовых случаев или использовать с testament с testament --nim:bin/nim_temp r tests/category/tsometest.
koch temp создаст временный компилятор с включённой -d:debug. Вот параметры компилятора, которые могут быть полезны при отладке:
-
-d:debug: включаетassert-выражения, трассировки стека и все проверки во время выполнения -
--opt:speed: сборка с включёнными оптимизациями -
--debugger:native: включает--debuginfo --lineDir:onдля использования родного отладчика, такого как GDB, LLDB или CDB -
-d:nimDebug: вызывает вызовыquitдля поднятия исключения утверждения -
-d:nimDebugUtils: включает различные средства отладки; см.compiler/debugutils -
-d:stacktraceMsgs -d:nimCompilerStacktraceHints: добавляет некоторые дополнительные подсказки для трассировки стека; см. https://github.com/nim-lang/Nim/pull/13351 -
-u:leanCompiler: включение JS и генерации документации
Другой метод сборки и запуска компилятора — напрямую через koch:
koch temp [options] c test.nim # (will build with js support) koch temp [options] js test.nim # (will build with doc support) koch temp [options] doc test.nim
Отладочная регистрация
"Printf-отладка" по-прежнему является наиболее подходящим способом отладки многих проблем, возникающих при разработке компилятора. Обычно использование точек останова для отладки кода менее практично, так как почти все пути кода в компиляторе будут выполняться сотни раз до достижения конкретного участка тестируемой программы, где нужно активировать недавно разработанный код.
Для решения этой проблемы, вы обычно добавляете оператор if в код компилятора, более точно определяя условия, в которых используется тестируемая функция. Один из очень распространённых способов достижения этого — использование условия mdbg, которое будет истинным только в контекстах обработки выражений и операторов из текущего основного модуля, подлежащего компиляции:
# inside some compiler module if mdbg: debug someAstNode
Использование условия isCompilerDebug вместе с вставкой некоторых операторов в тестовый случай обеспечивает более точную регистрацию:
# compilermodule.nim
if isCompilerDebug():
debug someAstNode
# testcase.nim
proc main =
{.define(nimCompilerDebug).}
let a = 2.5 * 3
{.undef(nimCompilerDebug).} Регистрация также может быть ограничена конкретным именем файла. Это, конечно, будет соответствовать каждому модулю с таким именем.
if `??`(conf, n.info, "module.nim"): debug(n)
В приведенных выше примерах также используется процедура debug, которая способна выводить удобочитаемую форму произвольного дерева AST. Другие распространённые способы вывода информации о внутренних типах компилятора включают:
# pretty print PNode
# pretty prints the Nim ast
echo renderTree(someNode)
# pretty prints the Nim ast, but annotates symbol IDs
echo renderTree(someNode, {renderIds})
# pretty print ast as JSON
debug(someNode)
# print as YAML
echo treeToYaml(config, someNode)
# pretty print PType
# print type name
echo typeToString(someType)
# pretty print as JSON
debug(someType)
# print as YAML
echo typeToYaml(config, someType)
# pretty print PSym
# print the symbol's name
echo symbol.name.s
# pretty print as JSON
debug(symbol)
# print as YAML
echo symToYaml(config, symbol)
# pretty print TLineInfo
lineInfoToStr(lineInfo)
# print the structure of any type
repr(someVar) Вот некоторые другие полезные утилиты:
# how did execution reach this location? writeStackTrace()
Эти процедуры, возможно, ещё не импортированы в модуль, который вы редактируете. Вы можете импортировать их непосредственно для отладки:
from astalgo import debug from types import typeToString from renderer import renderTree from msgs import `??`
Родная отладка
Пошаговое выполнение компилятора с помощью родного отладчика — очень мощный инструмент для изучения и отладки. Тем не менее, всё ещё необходимо ограничивать моменты запуска точек останова. Те же методы, что и в Отладочной регистрации, могут быть применены здесь в сочетании с вызовами отладочных помощников enteringDebugSection() и exitingDebugSection().
- Скомпилируйте временный компилятор с
--debugger:native -d:nimDebugUtils - Установите необходимые точки останова или точки наблюдения.
- Настройте отладчик:
- GDB: выполните
source tools/compiler.gdbпри запуске - LLDB: выполните
command source tools/compiler.lldbпри запуске
- GDB: выполните
- Используйте одного из помощников по ограничению области, например, так:
if isCompilerDebug(): enteringDebugSection() else: exitingDebugSection()
Ограничение этого метода заключается в том, что все точки останова и точки наблюдения включены или выключены. Кроме того, из-за ошибки, для LLDB можно ограничить только точки останова.
Архитектура компилятора
Nim использует классическую архитектуру компилятора: лексический анализатор/сканер подаёт токены парсеру. Парсер строит дерево синтаксического анализа, которое используется генераторами кода. Это дерево синтаксического анализа — интерфейс между парсером и генератором кода. Понимание большей части кода компилятора тесно связано с пониманием этого дерева.
Семантический анализ отделён от синтаксического анализа.
Краткое описание модулей Nim
| Модуль | Описание |
|---|---|
| nim | главный модуль: парсит командную строку и вызывает main.MainCommand
|
| main | реализует диспетчеризацию команд верхнего уровня |
| nimconf | реализует чтение файла конфигурации |
| syntaxes | диспетчер для различных парсеров и фильтров |
| filter_tmpl | стандартный фильтр шаблонов (#? stdtempl) |
| lexbase | обработка буферов лексического анализатора |
| lexer | лексический анализатор |
| parser | парсер Nim |
| renderer | рендерер кода Nim (отображение AST в текстовую форму) |
| options | содержит глобальные и локальные параметры компилятора |
| ast | определения типов абстрактного синтаксического дерева (AST) и конструкторов узлов |
| astalgo | алгоритмы для контейнеров узлов AST; преобразование AST в YAML; таблица символов |
| passes | реализует менеджер проходов для проходов по AST |
| trees | некоторые алгоритмы для узлов; этот модуль менее важен |
| types | модуль для обхода графов типов; также содержит несколько помощников для работы с типами |
| sigmatch | содержит алгоритм сопоставления, используемый для вызовов процедур |
| semexprs | содержит фазу семантической проверки для выражений |
| semstmts | содержит фазу семантической проверки для операторов |
| semtypes | содержит фазу семантической проверки для типов |
| seminst | инстанцирование обобщённых процедур и типов |
| semfold | содержит код для работы со сворачиванием констант |
| sempass2 | второй семантический проход по AST |
| vm | содержит интерпретатор AST для вычислений во время компиляции |
| pragmas | семантическая проверка директив |
| idents | реализует общее отображение идентификаторов на внутреннее представление (PIdent), которое используется для того, чтобы простое сравнение идентификаторов достаточно было для установления эквивалентности двух идентификаторов Nim |
| transf | преобразования AST, которые необходимо выполнить перед генерацией кода |
| cgen | главный файл генератора C-кода |
| ccgutils | содержит помощников для генератора C-кода |
| ccgtypes | генератор C-типов |
| ccgstmts | генератор операторов |
| ccgexprs | генератор выражений |
| extccomp | этот модуль вызывает компилятор и компоновщик C; интересно, если вы хотите добавить поддержку нового компилятора C |
Дерево синтаксического анализа
Дерево синтаксического анализа состоит из узлов, которые могут иметь произвольное количество дочерних узлов. Типы и символы представлены другими узлами, потому что они могут содержать циклы. AST изменяет свою структуру после семантического анализа. Это необходимо, чтобы облегчить работу генераторам кода. См. модуль «ast» для определений типов. Модуль macros содержит много примеров, как AST представляет каждую синтаксическую структуру.
Средства выполнения
Nim имеет два различных средства выполнения: «старое средство выполнения» и «новое средство выполнения». Старое средство выполнения поддерживает старые сборщики мусора (markAndSweep, refc, Boehm), новое средство выполнения поддерживает ARC/ORC. Новое средство выполнения активно when defined(nimV2).
Рекомендации по программированию
- Мы следуем официальному руководству по стилю Nim, см. NEP1.
- Максимальная длина строки — 100 символов.
- Добавляйте пробелы вокруг бинарных операторов, если это улучшает читаемость.
- Используйте пробел после двоеточия, но не перед ним.
- (устарело) Начинайте типы с заглавной буквы
T, если это не указатели/ссылки, которые начинаются сP. - Предпочитайте
import packageвместоfrom package import symbol.
См. также документ проектирование имён API.
Перенос на новые платформы
Перенос Nim на новую архитектуру довольно просто, поскольку C является наиболее переносимым языком программирования (в определенных пределах), а Nim генерирует код C, поэтому перенос генератора кода не нужен.
Системы, совместимые с POSIX, на стандартном оборудовании обычно довольно легко портируются: добавьте платформу в platform (если она там ещё не указана), проверьте, что модули OS и System работают, и перекомпилируйте Nim.
Единственный случай, когда всё не так просто, — это когда сборщикам мусора старого средства выполнения требуется настройка на уровне ассемблера для работы. Реализация по умолчанию использует функцию C setjmp для сохранения всех регистров в стеке оборудования. Возможно, новой платформе потребуется заменить этот универсальный код собственным ассемблерным кодом.
Файлы, которые могут потребовать изменения для вашей платформы:
-
compiler/platform.nimДобавить свойства os/cpu. -
lib/system.nimДобавить os/cpu в документацию дляsystem.hostOSиsystem.hostCPU. -
compiler/options.nimДобавить проверки специальных свойств os/cpu вisDefined. -
compiler/installer.iniДобавить os/cpu в полеProject.Platforms. -
lib/system/platforms.nimДобавить os/cpu. -
std/private/osseps.nimДобавить специализации os. -
lib/pure/distros.nimДобавить os, обработчик пакетов. -
tools/niminst/makefile.nimfДобавить флаги компилятора/линкера os/cpu. -
tools/niminst/buildsh.nimfДобавить флаги компилятора/линкера os/cpu.
Если опции --os или --cpu не переданы компилятору, Nim определит текущую систему, процессор и порядок байтов из system.cpuEndian, system.hostOS и system.hostCPU. Эти значения получены из compiler/platform.nim.
Для того, чтобы новая платформа могла быть запущена с помощью csources, она должна:
- иметь
compiler/platform.nimобновленным - иметь
compiler/installer.iniобновленным - иметь
tools/niminst/buildsh.nimfобновленным - иметь
tools/niminst/makefile.nimfобновленным - быть перенесена обратно в версию Nim, используемую
csources - новый
csourcesдолжен быть отправлен - новая версия
csourcesдолжна быть обновлена вconfig/build_config.txt
Информация о типе во время выполнения
Примечание: Этот раздел описывает «старое средство выполнения».
Информация о типе во время выполнения (RTTI) необходима для нескольких аспектов языка программирования Nim:
- Сбор мусора
- Старые сборщики мусора используют RTTI для обхода произвольных типов Nim, но обычно только поле
markerсодержащее процедуру, выполняющую обход. - Сложные присваивания
- Последовательности и строки реализуются как указатели на изменяемые буферы, но Nim требует копирования при присваиваниях. Помимо RTTI, компилятор также генерирует процедуры копирования в качестве специализации.
Информация о типе уже известна компилятору как граф. Таким образом, нам нужно сериализовать этот граф в качестве RTTI для генерации кода C. Смотрите файл lib/system/hti.nim для получения дополнительной информации.
Магии и compilerProcs
Модуль system содержит часть RTL, которая нуждается в поддержке магией компилятора. Генератор кода C генерирует код C для него, как и для любого другого модуля. Однако вызовы некоторых процедур, таких как addInt, вставляются генератором. Поэтому существует таблица (compilerprocs ) со всеми символами, помеченными как compilerproc. compilerprocs необходимы генератору кода. Процедура magic не такая же, как compilerproc. magic — это процедура, для которой семантический анализ требует магии компилятора, а compilerproc — это процедура, используемая генератором кода.
Генерация кода для замыканий
Генерация кода для замыканий реализована с помощью подъёма лямбда-выражений.
Проектирование
Процедура closure var может вызывать обычные процедуры по умолчанию для вызова Nim. Но не наоборот! Замыкание реализуется как tuple[prc, env]. env может быть nil, что подразумевает вызов без замыкания. Это означает, что вызов через замыкание генерирует if, но межплатформенная совместимость стоит затрат на if. Также возможна генерация тхунков, но для их реализации требуется немного больше усилий.
Тесты с GCC на Amd64 показали, что очень выгодно, если указатель «окружения» передаётся в качестве последнего аргумента, а не первого.
Правильная генерация тхунков сложнее, потому что процедура, которая должна быть обернута, может исходить из сложного выражения:
receivesClosure(returnsDefaultCC[i])
Тхунк должен был бы как-то вызвать returnsDefaultCC[i] , а это потребовало бы дополнительной генерации замыканий... Хорошо, не совсем, но это требует передачи вызываемой функции. В итоге мы получим 2 косвенных вызова вместо одного. Ещё одна гораздо более серьёзная проблема с этим решением заключается в том, что передача указателя на процедуру через универсальный тип ref небезопасна для сборщика мусора.
Пример кода:
proc add(x: int): proc (y: int): int {.closure.} =
return proc (y: int): int =
return x + y
var add2 = add(2)
echo add2(5) #OUT 7 Это должно примерно так сгенерировать код:
type
Env = ref object
x: int # data
proc anon(y: int, c: Env): int =
return y + c.x
proc add(x: int): tuple[prc, data] =
var env: Env
new env
env.x = x
result = (anon, env)
var add2 = add(2)
let tmp = if add2.data == nil: add2.prc(5) else: add2.prc(5, add2.data)
echo tmp Будьте осторожны с вложенностью:
proc add(x: int): proc (y: int): proc (z: int): int {.closure.} {.closure.} =
return lambda (y: int): proc (z: int): int {.closure.} =
return lambda (z: int): int =
return x + y + z
var add24 = add(2)(4)
echo add24(5) #OUT 11 Это должно примерно так сгенерировать код:
type
EnvX = ref object
x: int # data
EnvY = ref object
y: int
ex: EnvX
proc lambdaZ(z: int, ey: EnvY): int =
return ey.ex.x + ey.y + z
proc lambdaY(y: int, ex: EnvX): tuple[prc, data: EnvY] =
var ey: EnvY
new ey
ey.y = y
ey.ex = ex
result = (lambdaZ, ey)
proc add(x: int): tuple[prc, data: EnvX] =
var ex: EnvX
ex.x = x
result = (lambdaY, ex)
var tmp = add(2)
var tmp2 = tmp.fn(4, tmp.data)
var add24 = tmp2.fn(4, tmp2.data)
echo add24(5) Мы можем избавиться от вложенных сред, всегда вставляя внутренние анонимные процедуры. Однако более полезным является анализ утечек и выделение памяти для среды на стеке.
Аккумулятор
proc getAccumulator(start: int): proc (): int {.closure} =
var i = start
return lambda: int =
inc i
return i
proc p =
var delta = 7
proc accumulator(start: int): proc(): int =
var x = start-1
result = proc (): int =
x = x + delta
inc delta
return x
var a = accumulator(3)
var b = accumulator(4)
echo a() + b() Внутренности
Подъём лямбда-выражений реализован как часть прохода transf. Проход transf генерирует код для настройки среды и передачи её. Однако этот проход не изменяет типы! Таким образом, у нас есть некое несоответствие: с одной стороны, выражение процедуры становится явным кортежем, с другой стороны, тип tyProc(ccClosure) не изменяется. Для генерации кода C также важно, чтобы скрытый формальный параметр был void*, а не что-то более специализированное. Однако более специализированный тип env должен каким-то образом передаваться в бэкенд. Мы решаем эту проблему, изменяя s.ast[paramPos] для включения скрытого формального параметра, но не s.typ!
Примечания о представлении типов и AST
Подлежит расширению.
Целочисленные литералы
В Nim существует избыточный способ указания типа целочисленного литерала. Во-первых, должно быть очевидно, что каждый узел имеет вид узла. Узел целочисленного литерала может иметь любое из следующих значений:
nkIntLit, nkInt8Lit, nkInt16Lit, nkInt32Lit, nkInt64Lit, nkUIntLit, nkUInt8Lit, nkUInt16Lit, nkUInt32Lit, nkUInt64Lit
Помимо этого, есть также поле typ для типа. Вид поля typ может быть одним из следующих, и он должен соответствовать виду литерала:
tyInt, tyInt8, tyInt16, tyInt32, tyInt64, tyUInt, tyUInt8, tyUInt16, tyUInt32, tyUInt64
Затем есть также тип целочисленного литерала. Это специфический тип, который неявно преобразуется в требуемый тип, если требуемый тип может вместить значение. Для этого тип должен знать конкретное значение литерала. Например, выражение 321 будет иметь тип int literal(321). Этот тип неявно преобразуется во все целочисленные типы и диапазоны, содержащие значение 321. Это будут все встроенные целочисленные типы, за исключением uint8 и int8 где 321 будет вне диапазона. Когда этот тип литерала присваивается новой переменной var или let, её тип будет разрешён просто до int, а не до int literal(321), в отличие от констант. Константа сохраняет полный тип int literal(321).
proc foo(arg: int8) = echo "def" const tmp1 = 123 foo(tmp1) # OK let tmp2 = 123 foo(tmp2) # Error
В контексте с несколькими перегрузками вид целочисленного литерала всегда будет отдавать предпочтение типу int перед всеми другими типами. Если ни одна из перегрузок не имеет типа int, возникнет ошибка из-за неоднозначности.
proc foo(arg: int) = echo "abc" proc foo(arg: int8) = echo "def" foo(123) # output: abc proc bar(arg: int16) = echo "abc" proc bar(arg: int8) = echo "def" bar(123) # Error ambiguous call
В компиляторе эти целочисленные типы литералов представлены с видом узла nkIntLit, видом типа tyInt и членом n типа, указывающим на узел целочисленного литерала в ast, содержащем целочисленное значение. Это свойства, которые остаются истинными для целочисленных типов литералов.
n.kind == nkIntLit n.typ.kind == tyInt n.typ.n == n
Другие типы литералов, такие как uint literal(123), которые автоматически преобразуются в другие целочисленные типы, но предпочитают стать uint, не являются частью языка Nim.
В непроверенном AST поле typ имеет значение nil. Проверяющий тип установит поле typ в соответствии с типом узла. Узлы типа nkIntLit получат целочисленный тип литерала (например, int literal(123)). Узлы типа nkUIntLit получат тип uint (тип tyUint ), и так далее.
Это также означает, что невозможно написать литерал в неконтролируемом AST, который после семантического анализа будет только типа int и не будет неявно преобразован в другие целочисленные типы. Это работает только для всех целочисленных типов, которые не являются int.
© 2006–2024 Andreas Rumpf
Licensed under the MIT License.
https://nim-lang.org/docs/intern.html