Окружения, байткод и образы
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Окружения, байткод и образы
Код мы строить научились. Теперь разберёмся, где он живёт, и главная мысль урока такая: всё, что выглядит потрохами компилятора, в Janet лежит в обычной таблице и доступно как данные. Окружение это таблица, макрос это пометка в ней, образ это она же в байтах, а компилятор это функция над ней.
Окружение это таблица
Откуда компилятор знает, что означает символ +? Из окружения, таблицы, которая связывает символы со значениями. Текущее окружение доступно как обычное значение:
(type (curenv)) # :table, обычная таблица, ничего особенного
И в ней можно посмотреть любое связывание:
Вывод показан для запуска из файла. В REPL на месте имени файла будет repl, а при janet -e строка eval.
Связывание это не просто значение, а таблица с метаданными: само значение, место в исходнике, докстрока, признак макроса. Именно отсюда (doc ...) берёт документацию.
Проверим на макросе:
Макрос это обычное связывание с пометкой :macro true. Никакой отдельной подсистемы: компилятор, встретив вызов, смотрит в окружение и, если связывание помечено как макрос, вызывает его во время компиляции, а не выполнения.
root-env это окружение со стандартной библиотекой. (curenv) окружение текущего модуля, у которого root-env в прототипах (про них будет 31-janet/18 · Прототипы, методы и полиморфизм). Поэтому имена из стандартной библиотеки видны везде, а твои определения её не засоряют.
Пощёлкай по символам и посмотри, где каждый находится и что лежит в его связывании:
Раскрытие макросов
Между парсингом и компиляцией лежит ещё один шаг, раскрытие макросов. Его тоже можно вызвать руками, и это лучший способ понять, что происходит.
macex1 раскрывает один верхний уровень:
Видно, что when, unless и -> не встроенные конструкции, а макросы, которые переписывают себя в if. macex раскрывает рекурсивно, до упора.
Более интересный пример:
Обрати внимание на _000010, сгенерированное имя. and должен вычислить a ровно один раз (вдруг там побочный эффект), поэтому сохраняет результат во временную переменную. Имя выбрано так, чтобы гарантированно не совпасть с твоими переменными, за это отвечает gensym:
(= (gensym) (gensym)) # false, каждый вызов даёт новое имя
Это называется гигиеной макросов, ей посвящён 31-janet/09 · Гигиена макросов и три ловушки.
Раскрытие может содержать не только символы. Посмотри на раскрытие each:
У тебя вывод будет длиннее и с другими сгенерированными именами: здесь он обрезан до строк, ради которых пример и приведён.
<function next> это не символ next, а сама функция, вставленная в дерево кода как значение. Дерево кода это данные, а в данных могут лежать любые значения, включая функции.
Практическое следствие: раскрытое дерево не всегда можно напечатать через %j или записать в файл. Для отладки бери %p (pretty-print).
compile и disasm
Компилятор тоже обычная функция над всё той же таблицей-окружением. Принимает дерево и окружение, возвращает функцию без аргументов:
Если код не компилируется, compile вернёт не функцию, а таблицу с описанием ошибки. Именно вернёт, а не бросит, поэтому ловить её через try бесполезно, проверяй результат через function?:
Заглянуть в получившийся байткод позволяет disasm:
Здесь видно устройство функции: арность, константы, слоты регистров, сам байткод. В повседневной работе disasm не нужен, но он бесценен, когда хочешь понять, во что разворачивается твоя конструкция и не делает ли она лишней работы. Если тебе знакома дизассемблированная сборка из уроков про машинный уровень, ощущение будет то же, только байткод читается легче.
Динамические переменные
Вот единственное исключение из правила урока: эти связывания живут не в таблице-окружении, а в текущем файбере, и доступны по ключевому слову:
Их сила в динамической области видимости: with-dyns устанавливает значение на время выполнения блока, и оно видно всем вызванным оттуда функциям на любой глубине:
Так устроены многие механизмы самого языка. (dyn :current-file) возвращает имя текущего файла, а стандартные потоки ввода-вывода тоже берутся из динамических переменных, поэтому вывод можно перенаправить, не меняя ни одной функции печати.
Когда это правильный инструмент. Динамические переменные годятся для окружающего контекста: текущее соединение с базой, уровень логирования, локаль, отступ при форматировании. Всё, что иначе пришлось бы протаскивать лишним параметром через десяток функций.
Обратная сторона та же, что у глобальных переменных: неявная зависимость. Бери их для контекста, не для передачи данных. Ту же границу мы проводили в уроке про отслеживание контекста в асинхронном коде.
Образы
Раз окружение это таблица со значениями, его можно сохранить целиком. Образ это ровно такая таблица, превращённая в байты:
make-image сериализует значение вместе со всем, на что оно ссылается, включая функции. load-image восстанавливает.
Зачем это нужно: если программа при старте делает дорогую работу (разбирает большую таблицу, компилирует грамматики, строит индекс), результат можно один раз сохранить в образ, а при запуске просто загрузить. Сам Janet устроен так же: стандартная библиотека вкомпилирована в исполняемый файл как образ, поэтому и стартует за миллисекунды. Это тот самый график холодного старта, который ты крутил в 31-janet/01 · Зачем Janet, лисп на один мегабайт.
Родственная пара функций это marshal и unmarshal из урока про значения и ссылки. Они делают то же самое, но без привязки к окружению.
Что запомнить
- Окружение это обычная таблица, связывание это таблица с метаданными.
- Макрос это связывание с пометкой
:macro true, отдельной подсистемы нет. (curenv)это модуль,root-envстандартная библиотека, связь через прототип.macex1раскрывает один уровень,macexдо упора. Это главный инструмент отладки макросов.gensymдаёт заведомо уникальное имя, на нём держится гигиена.- В дереве кода могут лежать не только символы, но и функции. Печатай через
%p. dynиsetdynдают динамическую область видимости, для контекста, не для данных.make-imageиload-imageзамораживают состояние в байты и восстанавливают.
Упражнения
Дальше
Инструменты в руках: код это данные, окружение это таблица, раскрытие видно глазами. Дальше пишем свои макросы.
домашка