Как язык нуждается в том, чтобы он мог скомпилироваться сам собой
Что должен знать язык перед тем, как он сможет собирать сам себя ## Введение в язык Lambda (lm) Язык Lambda (lm) — это небольшой низкоуровневый язык, который был разработан...


Что должен знать язык перед тем, как он сможет собирать сам себя
Введение в язык Lambda (lm)
Язык Lambda (lm) — это небольшой низкоуровневый язык, который был разработан для демонстрации и исследования различных концепций программирования. Этот язык отличается своей статической типизацией, явным управлением памятью, отсутствием закрытых функций и сборщика мусора. lm имеет четыре независимые backend-версии, которые выдают код на C, WebAssembly, ARM64 и машинный код для виртуальной машины.
Важной частью любого языка является его компилятор. Для lm компилятор написан на JavaScript и состоит из около 4,400 строк кода. Однако самая интересная часть — это первоначальная попытка сделать так, чтобы язык мог собирать сам себя. В этой статье мы рассмотрим, какие проблемы возникают при этом и как они решаются.
Сборка самого себя: первые шаги
Первым шагом к тому, чтобы язык мог собирать сам себя, стала попытка переписать его лексер. Лексер — это часть компилятора, которая разбивает исходный код на токены. В lm лексер занимает 355 строк кода, тогда как в оригинальном коде он составляет всего 129 строк. Разница в размерах объясняется несколькими ключевыми особенностями языка.
Отсутствие возможности получения адреса скалярного локального значения
Одним из основных отличий lm от многих других языков является отсутствие возможности получить адрес скалярного локального значения. Это означает, что функция не может изменять переменную своего вызывающего модуля напрямую. Вместо этого каждая функция, возвращающая три значения, должна использовать структуру, которая создается на каждой вызове функции.
// Пример кода из оригинального lm
function foo() {
return [1, 2, 3];
}
// Пример кода для lm
struct Result {
int val1;
int val2;
int val3;
}
Result foo() {
Result result = {1, 2, 3};
return result;
}
Управление колонками внутри строковых литералов
Другой важной особенностью lm является отсутствие поддержки UTF-8 внутри строковых литералов. Это приводит к необходимости обрабатывать продолжения байтов UTF-8 внутри строковых литералов.
// Пример кода из оригинального lm
console.log("Hello, world!");
// Пример кода для lm
console.log("\x48\x65\x6c\x6c\x6f, \x77\x6f\x72\x6c\x64\x21");
Оптимизация и тестирование
При переписывании лексера первоначальной целью было найти те функции, которые не могут быть реализованы в lm и проверить их работу до того, как код станет слишком большим для изменения. В src/lexer.js функция advance(n) используется для перемещения позиции, строки и столбца вместе. Эта функция вызывается из четырнадцати мест.
// Пример кода из оригинального lm
function advance(n) {
pos += n;
col += n;
line += Math.floor(n / 80);
}
// Пример кода для lm
function advance(n) {
for (let i = 0; i < n; i++) {
let ch = buffer[pos++];
if (ch === '\n') {
line++;
col = 0;
} else {
col++;
}
}
}
Заключение
Переписывание лексера lm показало, что язык не может поддерживать некоторые функции, которые существуют в других языках. Это помогло команде понять границы языка и улучшить его дизайн. Хотя процесс был сложным и требовал значительных усилий, он привел к более понятному и эффективному языку.
Практические советы
- При разработке собственного языка или компилятора важно заранее определить границы языка.
- Использование тестирования на ранних этапах разработки может помочь выявить проблемы и улучшить дизайн.
- Обеспечение полной совместимости с другими языками может потребовать дополнительных усилий, но это улучшает адаптивность вашего языка.