Comments 39
Нормас тема :) Я когда школяром был, дико интересовался темой компиляторов и интерпретаторов. По итогу ничего внятного тогда не запилил, но был интерпретатор самопального BASIC-подобного скриптового языка.
зачем писать собственный компилятор, когда есть GCC в исходниках?
зачем писать собственный компилятор, когда есть GCC в исходниках?
А как же академический интерес? Хочешь в чем-то разобраться - сделай минимальную версию.
К примеру, есть хорошая статья по созданию минимальной ОС, которая работает в защищенном режиме - весьма промывает мозги.
Хотел только написать, что писал для того, чтобы разобрать основные моменты устройства компиляторов (правда пока не до всех руки дотянулись), а за меня уже все написали. Спасибо.
Но до минимальной версии мой компилятор пока не особо тянет, думаю. Сейчас пытаюсь разобраться с функциями (тоже в таком же простейшем виде: доступ только к глобальным функциям, со своими двадцати шестью переменными). Может через неделю-две напишу продолжение статьи. Там как раз уже и семантика будет.
Ну и зачем здесь этот комментарий без ссылки на статью?
Тут: https://dev.to/frosnerd/series/9585 Даже серия из 5 статей.
А как же академический интерес? Хочешь в чем-то разобраться - сделай минимальную версию.
Насколько я это понимаю, это любительский подход, а не академический. Вы же не считаете что радиолюбители которые собирали самодельные радиоприемники в прошлом веке проявляли академический интерес.
По моему академический интерес состоит как раз в том чтобы понять как делают профессионалы, а не в том чтобы сделать самоделку.
Академичность во многом состоит в том чтобы анализировать уже существующие знания и обосновано расширять их по возможности, по моему.
Потому что дотягивается.
Хочешь что-то досконально изучить, напиши своё.
Здравствуйте, коллега :)
Не очень хорошо вы искали статьи, даже на хабре статей сильно больше того, что вы процитировали.
Зря вы акцентируете внимание на C++ вместо непосредственно вашего яызка. Гораздо интереснее было бы посмотреть на примеры программ на вашем языке, нежели на
char* data = new char[85];
memcpy(data, "{a = 11004; b = 10087; while(0 < b) {c = a - (a / b * b); a = b; b = c;} print(a);}", 85);
compiler comp = compiler(data, 85);
На самом деле, из вашей статьи совершенно непонятно, как работает, например, парсер. Ваша грамматика имеет неоднозначности, и (с первого взгляда) я не понимаю, как вы расставляете приоритеты у операций...
Типизация выражений, таблицы символов, оптимизация кода - всё прошло мимо :(
Спасибо за фидбек, ради него в основном и писал статью, потому что пока только погружаюсь в эту тему.
Примеры программ могу буду добавить, но особо нет идей, что написать на этом языке. Разве что действительно какие-то достаточно примитивные модели.
Про неоднозначность грамматики спорить не буду, слишком глубоко не вдавался в ее построение, если быть совершенно честным, то писал свой компилятор на основе упомянутой статьи, поэтому просто взял грамматику отсюда, несколько поправив ее под мой случай.
У операций нет приоритета, это было упомянуто в статье. Единственный приоритет - это скобки.
С последним абзацем также соглашусь. Пока это минимальная версия того, на что можно получить ответ. Я планирую дальше дорабатывать этот компилятор, правда пока не знаю в каком ключе лучше двигаться. Пока думаю добавить глобальные функции, но не знаю действительно ли это позволить показать что-то большее, кроме семантики.
За оптимизацию кода компилятором вообще страшно браться: вряд ли пока дорос до такого.
Давайте бояться вместе, я тоже ровно на этом пути (см мои последние статьи) :)
Я планирую дальше дорабатывать этот компилятор, правда пока не знаю в каком ключе лучше двигаться.
Проверка (а лучше вывод) типов. Можно вообще перейти к динамической типизации, к замыканиям и к сборщикам мусора. Ну а если статическая, добавляйте классы, множественное наследование и таблицы виртуальных методов, там есть чем заняться. А вообще самый шик - это сделать минимально рабочую версию компилятора, и расширять возможности языка на самом языке.
особо нет идей, что написать на этом языке
Основная проблема в случаях, когда рассматривается только академический интерес. Неужели в современных языках нет ничего, чтобы вам не нравилось и не хотелось бы улучшить? Для меня, например, таким раздражающим фактором является обилие скобочек и точек с запятой. Поэтому в своём игрушечном ЯП я наоборот, добавил ещё больше арифметических операций с другими приоритетами, чтобы например вместо (1+log(cosh(2*x)))/(1+pow(sinh(x),2))
можно было бы писать 1+log cosh 2 x//1+sinh^2(x)
А почему последний икс в скобках?
Лайк за наблюдательность) В данном случае для однозначной (в том числе и визуальной) интерпретации нужно разделять значение показателя степени (которое тоже может быть составным) от собственно аргумента функции. В целом я пока что не уверен, что возможность для подобного рода записи - это хорошая идея (из-за правоассоциативности возведения в степень), просто было интересно, можно ли подобное реализовать в принципе. Может быть позже ещё идеи появятся в плане синтаксиса для подобного рода конструкций.
Сами же скобки, как оператор группировки, никуда не делись. Для функций многих переменных от них тоже избавиться пока не получилось - но тут уже из-за желаемой возможности параллельного присваивания (типа a,b=a+b,a-b
).
Можно отбивку пробелом использовать, наверное
На пробел у меня повешено неявное умножение, как у Вольфрама. С той разницей, что у него приоритет выше, чем у обычного.
Я планирую дальше дорабатывать этот компилятор, правда пока не знаю в каком ключе лучше двигаться.
Выкинуть из языка «оператор print», отказаться от идеи генерировать готовые исполняемые файлы, а вместо это нужно генерировать объектные файлы (COFF или ELF — на выбор) и использовать внешний линкер (для начала — сторонний). Одно только это даст возможность с одной стороны взаимодействовать со всем внешним миром (дергать WinAPI, например), с другой стороны это огромное подспорье для тестов (TDD), потому что работу компилятора можно проверять автосатизированно, прилинковывая его выхлоп к заранее заготовленному приложению-тесту (написанному на чем-то другом).
И вообще, я бы не вашем месте изобретал неведомый синтаксис, а писал бы компилятор Си, а в качестве поблажки себе разрешил бы не беспокоиться о следовании стандарту, и, для не реализовывать для начала все возможности языка.
Пока не дочитал статью, возник вопрос.
Я частенько думаю о написании своего компилятора и для меня видится совсем бесполезным самостоятельно транслировать свой код под каждую архитектуру. Так почему же вы решили не использовать llvm? Я думаю IR ничуть не сложнее asm для dos
Все прозаично и, возможно, наивно: хотел попрактиковаться именно с ассемблером, некоторые его знания уже были. Я бы задал себе другой вопрос: почему я полез в ассемблер, который сейчас компилируется только в MS-DOS. Но на этот вопрос я сам не могу дать себе внятного ответа. Наверное проще было установить DOS, чем смотреть на отличия досовского asm от asm для винды.
Да кто ж будет игрушечный компилятор транслировать под каждую архитектуру? Лично мне нравятся проекты без внешних зависимостей, зачем в учебном коде llvm?
разве вам не хочется запустить хелло ворлд, написанный на своем языке, прямо на своем компьютере?)
А что вас смущает во внешних зависимостях?
А что, только LLVM это даёт?
Генерировать машинный код x86 достаточно просто.
Я-то запустил, и не только хелловорлд :)
А зависимости это... Неопрятненько, что ли. Кусок магии остаётся, нет чувства полного удовлетворения.
Да, я читал ваши статьи)
А зависимости это... Неопрятненько
так далеко не уедешь.
Как бы мне самому не было печально, но писать все с нуля далеко не лучший выбор
В прод - безусловно. Для обучения - сильно не факт
В прод - тоже не факт. На своей работе, в задачах автоматизации производства 24/7, я переписывал даже решения от Сименса. И не ради искусства и чсв, а ради надёжности и удобствп. Ну просто человек по другому относится к написанию кода, когда последствия ошибок лежат на нём лично, и с пользователями его софта он тоже контактирует лично.
asm для dos
это вы его определённо не пытались использовать. мало того что зависимость сама по себе достаточно жирная, так ещё и довольно комплексная. Одних обвязок с таргетами будет как весь имеющийся код.
В качестве языка самого компилятора я выбрал C++, потому что, по данным гугла, в основном для компиляторов используется он, либо чистый "Си".
На этом месте в процессе чтения просто руки опускаются — настолько ущербно это звучит. Не в том дело, что выбор неправильный — выбор правильный, но вот обоснование выбора просто дикое. Это как «почему я выбрал программирование — я загуглил, какая профессия самая лучшая, и там было написано так» или «почему я женился на этой женщине — да я вообще знал, кто это такая, мне просто сказали, что у неё красивая заколка в волосах». Как модно рассуждать о создании компиляторов и при этом в выборе ЯП полагаться на принцип «одна бабка сказала». Хотя выбор правильный.
Транслировать же будем в старенький asm, предназначенный для DOS.
И это тоже. Что такое ассемблер для DOS? Как может быть ассемблер под DOS? Под x86, под ARM, под MIPS — понятно, а под DOS это как?
Вообще, решение генерировать ассемблерный код в виде текста, а потом его скармливать ассемблеру — странное. Генерировать готовый машинный код — это не сложно, а рамках компилятора это вообще самое простое, по сравнению с другими задачами, которые приходится решать при написании компилятора.
Да и выбор 16-битного ассемблера не одобряем, просто потому что код для него генерировать сложнее, чем для того же 32-битного, хотя бы в силу меньшего числа поддерживаемых вариантов косвенной адресации.
Отставить легкие проверки! Только хардкор.
В смысле если забивать оперативку, то как джава?
while (!(now == ' ' || now == '\r' || now == '\n' || now == '\t'))
странно, что вы использовали isdigit, но не isspace.
memcpy(data, "{a = 11004; b = 10087; while(0 < b) {c = a - (a / b * b); a = b; b = c;} print(a);}", 85);
Вот этот мув совершенно не понял. То есть везде по коду используется std::string, но в этом месте вы решили ручками покопировать память. Зачем? Сделали ли бы string из сырой строки.
std::string program = R"({
a = 11004;
b = 10087;
while (0<b) {
c = a - (a/b*b);
a = b;
b = c;
}
print(a);
})";
std::string str = ""; // В эту строку будет записываться текущий токен
Вот эта штука будет делать ваш компилятор очень тормозным, ибо токенов в программе не мало и на каждый будет выделяться память под строку, которую следом же выкидываете. Лучше перейти на string_view и считать оффсеты от текущей глобальной позиции лексера. Что-то вроде
class Lexer {
sszize_t current_offset; // текущая позиция
ssize+t current_line; // текущая строка
std::string program; // исходник
public:
Lexer(std::string prog): program(std::move(prog)
{ /* etc */ }
Token getNextToken() {
auto token = Token::Nop;
auto offset = current_offset;
while (!isspace(prog[offset++])) {
// то что у вас там парсилось
...
}
current_offset += offset;
return token;
}
};
int main() {
string prog = "{a=1+1; print(a)}";
Lexer lex(prog);
while (auto token = lex.getNextToken(); token != Token::Eof) {
// обрабатываем токен
}
return 0;
}
Заодно, когда дойдёте до обработки ошибок будет проще писать в какой строке/столбце ошибка.
Про проверку isspace тупо забыл, что существует, на плюсах не так часто пишу.
Вот этот мув совершенно не понял. То есть везде по коду используется std::string, но в этом месте вы решили ручками покопировать память. Зачем? Сделали ли бы string из сырой строки.
Согласен, так даже проще, просто этот кусок кода был написан до основной части компилятора.
Лучше перейти на string_view и считать оффсеты от текущей глобальной позиции лексера. Что-то вроде
Не совсем понял следующий момент: токен может быть более, чем из одного символа. Или вы хотите сказать, что по глобальной позиции лексера и смещению мы можем выделить кусок строки, который является текущим проверяемым токеном? А в таком случае не будет ли на каждом шаге создаваться своя строка? А с тем, что так проще для парсинга ошибок согласен, спасибо за совет.
токен может быть более, чем из одного символа.
как в общем и string_view. оно фактически просто указывает на кусок оригинальной строки и её размер без копирования всей строки. Так что просто отсчитываете сколько вам символов нужно и конструируете.
std::string str = "1234567890";
auto ptr = str.data(); // для удобства
// | указатель на начало строки
// V v желаемый размер
std::string_view view1{ptr , 3 }; // "123" из str;
// V сдвинуть на 5 символов относительно начала
std::string_view view1{ptr+5, 3 }; // "678" из str;
auto it = str.begin(); // начиная с С++20 можно через итераторы
std::string_view view1{it+5, 3 }; // "678" из str;
Работать можно почти также как с обычной строкой - стравнение с другими строками, итерация по символам в цикле, брать подстроки(точнее подвьюхи) через substring
, искать всякое через find
/rfind
/_first_of
/_last_of
. Также в контейнерах хранить - map/set/vector.
Пока жива ваша строка с исходным кодом string_view
будут оставаться валидными. Взамен получаете меньше копирований, аллокаций и реаллокаций (а они в вашем коде вероятны, т.к. вы не резервируете память), что добавляет бонуса к пропускной способности вашего компилятора и помогает с выводом сообщений об ошибках.
По большому счёту токены вообще можно делать вида
struct Token {
TokenKind kind; // тип токена
size_t begin; // оффсет от начала кода программы
size_t len; // длина токена
};
А когда понадобится просто брать подстроку из исходной строки, но тут есть нюансы с постоянным прокидыванием оригинальной строки в функции, которые эти токены потом обрабатывать будут.
Создание собственного компилятора