Pull to refresh

Comments 31

Синтаксер по списку токенов строит синтаксическое дерево (или по научному, преобразует регулярную грамматику в контекстно-свободную).

Если отказ от погружения в теорию — не принципиальная позиция и челлендж, лучше это сделать, чтобы больше такого, как в скобках, не писать.

В компиляторе неудачно то, что нет отдельного шага типизации. Из-за того, что она делается вместе с построением AST, появляются не очень логичные obj->field vs obj.method() (а если поле — указатель на функцию?), оператор <- и необходимость добавлять повсеместно &. Вы планируете полиморфизм, значит, у вас будут конструкции, которые синтаксически одинаковы, но типизированы по-разному.

Соглашусь с коллегой выше. Си-подобный синтаксис очень сильно (и неочевидно) контекстно-зависим, и у вас при внимательном рассмотрении начнут вылезать неразрешимые альтернативы в грамматике. Если бы вы использовали bison или другой компилятор компиляторов, это привело бы к ошибкам на этапе компиляции грамматики. А так будут просто нежданчики в поведении компилятора.

Ну и, судя по написанному в статье, вы себе не очень хорошо представляете, что такое closure (замыкание).

Использовать генератор парсера нынче не очень практично, можно и руками парсить. Это не так сложно и контекстонезависимость не является проблемой, если парсинг отделён от генерации промежуточного кода.

Для этого наверное надо попробовать и так, и так. Я делал и так, и так. И там и там есть свои плюсы и минусы, но лексер-генератор на мой взгляд практичнее.

Приветствую!
Скажите пожалуйста, а сколько времени ушло на всё-провсё?
Я тоже хочу написать свой системный язык программирования, и меня волнует в первую очередь, сколько времени уйдёт на стандартную библиотеку.

Если вы хотите системный язык программирования, в нем вообще не должно быть никакой стандартной библиотеке.

Зачем мне ваш printf, если я пишу под код, который будет работать в среде где вообще нет понятия терминала (например это ЭБУ коробки передач или мозги квадрокоптера).

Зачем мне ваши функции управления процессами, если в среде, где будет выполняться код, может вообще не быть понятия процесса. Или быть, но это будут не процессы-в-духе-unix или процессы-в-духе-Windows.

А если если все-таки я пишу свою экзотическую ОС в которой все-таки будет концепция процессов, что будут делать ваши стандартнобиблиотечные фунции для запуска нового процесса или работы с терминалом, если для этого они должны обращаться к системным API, а таковых нет, потому что стандартная библиотека о них знать не знает, потому что я как раз и придумываю их здесь и сейчас, реализую их на новомоднои системном ЯП.

Стандартная библиотека это не только функции взаимодействия с системой (вроде printf), но и контейнеры с алгоритмами. Их можно применять в любой среде, даже без ОС.

В вас говорит плюсовик.

Первоначально термин «стандартная библиотека» предполагал именно стандартную библиотеку функций.

Скрытый текст

Хотя вы можете возразить, что и в стандартной библиотеки Си есть функция qsort, которая представляет собой алгоритм с callback-ом.

Во мне говорит нормальный человек. Во множестве языков в стандартной библиотеке есть функционал, не завязанный на взаимодействие с системой и существующий исключительно для упрощения жизни программиста - чтобы это не пришлось самостоятельно писать. Такого нету разве что в в языках, соревнующихся с Си в убогости.

Я сейчас занят написанием стандартной библиотеки для своего языка. Сама по себе она не очень сложная - в сумме на неё затрачено около нескольких месяцев разработки. Но вот сам язык сильно сложнее, его разработка длится годы.

Хм... Скажите, а почему годы?

Вы не очень много времени можете уделять ему?

Годы потому, что мой язык весьма продвинутый - богат функционалом. Какой-нибудь плохенький и кривой подражатель Си можно и за тройку месяцев написать, но вот что-то более сложное и удобное в использовании требует гораздо большего времени.

Здравствуйте. Я начал писать этот компилятор с лета 2023 года. Писал с большими перерывами, так как я сейчас учусь в вузе. Я начал его писать на базе другого компилятора для более простого языка, который я писал в 2022-2023 учебном году, и который был моим дипломным проектом.

Могу отметить, что при написании такого сложного для меня проекта (всё другое, что я писал, было намного проще) требуется большая концентрация и "видение всего проекта", иначе можно придумать что-то неудачно, потратить на это время, и обнаружить проблему позже. Поэтому писать медленно и по чуть-чуть не получалось — только рывками помногу.

Стандартная библиотека у меня очень мала, и с нормальным знанием языка пишется очень быстро (за несколько дней). Написание её на alias-е тоже прошло быстро и без проблем, что доставило мне большое чуство удовлетворённости от того, что хотя бы это на моём языке можно написать.

Конечно же, время зависит от опыта и знаний системного программирования. Я заранее не знал всё это и изучал по ходу написания. Поэтому оно и длится несколько лет.

В C компоновщик генерирует свою функцию _start

Вы это серьёзно? Компоновщик генерирует код? В C какой-то особенный, отдельный компоновщик? Обычный, работающий с COFF там или ELF без оглядки на то, из чего он был получен, не подходит?

Ну, если совсем строго говорить, компоновщик временами код генерирует-таки -- по крайней мере, на определённых платформах. Но уж точно не код _start.

Например, для 32-разрядных ARMов он при необходимости генерирует так называемые veneer -- куски кода для обеспечения перехода от одной подпрограммы к другой, если обеспечить переход модификацией адреса в команде перехода невозможно (в ARM невозможно указать в командах переходов полный 32-разрядный адрес, поэтому, если на этапе компоновки окажется, что целевой адрес слишком далёк, приходится лепить этот самый veneer, что и выполняется компоновщиком).

Ну, я бы не стал относить это к кодогенерации. Это самая что ни на есть прямая работа линкера — наведение связей между модулями..

Кстати, в далёком прошлом мне доводилось решать на PDP-11 обратную задачу — после довольно примитивного компилятора в числе прочего по возможности заменять дальние переходы, которые занимают по два слова, на короткие ветвления, которым хватает и одного. Слово в 56КБ RAM всё же ценится сильно дороже слова в нынешних гигазах :)

Но, тем не менее, код компоновщиком генерится. А можно ещё про LTO вспомнить (link-time optimization) -- частным случаем чего можно рассматривать Ваши преобразования переходов на ПДПшке (подозреваю, что, скорей, СМке или какой-нить ДВКшке и прочей Электронике-60, но суть дела не меняется).

что в языке сделано неудачно,

Отвратительный вырвиглазный синтаксис.

Я не понимаю, отчего каждому изобретателю своего ЯП так охота придумать какие-то диковинные конструкции и закорючистый синтаксис.

Вот решили вы делать не просто язык, а C-подобный — ну и отличное решение. Прекрасное решение. Я считаю синтаксис С идеальным синтаксисом, почти что «золотым сечением» в области языков программирования, результатом эволюции подходов, отброса неудачнях подходов и закрепления удачных.

Ну и оставайтесь в рамках C-подобности. Зачем какое-то func, какие-то дурацкие ->?

Вместо этих извращений возьмите конвенциональный сишный синтаксис, но добавьте в язык то, чего не хватает в Си.

Например поддержку явного указания endianness отдельно взятых переменных или полей структур.

Пробовали когда-нибудь писать код, который должен компилироваться и в little-endian и в big-endian (из одного и того же исходника, без правки) и при этом должны работать с файлом или сетевым трафиком, в котором есть поля, endianness которых жестко задан какой-то спецификацией и не зависит от целевой архитектуры? Пусть в языке будет возможность явно указать порядок байтов, пусть для таких типов компилятор сам генерирует код переворачивания байтов.

Или, например, такая фишка, как структуры переменного размера с полями переменного размера, где размер полей привязан к значению предешствующих полей, а смещения полей вычисляются в рантайме?

Или 24-битные переменные из коробки. Или переменные произвольной битности — при этом, если архитектура позволяет, компилятор использует особенности архитектуры, а если же нет — эмулирует переменные нужной битности. Хотите 96-битное целое — да пожалуйста, объявляйте, присваивайте значения, складывайте (компилятор сам развернет одну операцию сложения в несколько с применением флагового бита переноса разряда).

Отсутствие формата "проекта". Организация даже тривиального проекта весьма сложна (посмотрите на Makefile в примере GitHub).

Ни в коем случае. Если вы не делаете моноязык, который «вещь в себе», а провозглашаете такие принципы как линкуемость с другими языками (си, асм), то какого черта ваш язык должен навязывать какой-то свой формат проекта?

Пишет Вася программный продукт — четверть на Алиасе, четверть на Си, четверть на ассемблере, четверть на (прости-господи) Паскале. Все это вася линкует в один исполняемый файл. Вопрос: с какого перепуга проджект-файл должен быть составлен именно по правилам и законам, которые задает язык Alias — один из четырёх примененных в проекте языков?

Процесс сборки — это надъязыковая зона ответственности. Как может быть надгосударственный институт власти, так и все, что касается сборки, должно быть надъязыковым. Ни один язык не должен лезть в эту сферу ответственности и навязывать свои правила игры.

Все что мне нужно от языка и компилятора: пожалуйства, пусть он возьмёт исходник и в ответ выплюнет объектный файл в формате COFF, ELF, или ещё какой. Чтобы я потом взял свой любимый линкер и слинковал много объектных файлов (написанных, возможно, на разных языках) в один исполняемый. Или скормил их своему союственному линкеру, генерирующиму исполняемые файлы моего собственного формата под мою собственную ОС, которую я сам пишу. Иначе что это за системное программирование, если язык и компилятор не оставляют мне такой возможности.

Линкуемость с другими объектными файлами и использование одинакового ABI — это все, что мне нужно. Не нужно никакой стандартной библиотеки. Если мне нужна будет какая-то библиотека, я слинкуюсь с ней сам, например, со стандартной библиотекой Си от GNU. Или, если вы условия не позволяют, сам реализую нужные функции.

Язык без стандартной библиотеки — это не чемодан без ручки. Напротив, язык, предполагающий стандартную библиотеку, это туристический чемодан, куда производитель заботливо положил ласты и маску, опрометчиво предположив, что я не только всенепременно поеду на отдых, но и увлекаюсь нырянием.

Отсутствие пакетного менеджера

В общем-то, те же самые аргументы.

Вдохновляюще! Меня постоянно останавливают сомнения, а Вы взяли и сделали.

Понимаю, откуда берётся обилие амперсандов: пытаетесь не допустить «ссылки» как сущности в концепт языка. Если задуматься, то в C «ссылки» возникают помтоянно, но о них не принято говорить, прикрываясь терминами lvalue. Но когда в C++ пытаются обобщить внутренние механики разименования C на результаты вызовов функций (а не только проход по полям структур и поинтерам), то без явной сущности «ссылка» уже не вырулить. Мне не очень понятно, смогли вы распутать клубок или пошли не в ту сторону. В любом случае, о причине мы явно думаем похожим образом.

Попытку уйти от «ссылки» я видел еще в одном языке. К сожалению, быстро не вспомню. Поищу в закладках потом.

else в циклах - это зачёт! Питон имеет else в циклах, но почему функциональные языки до него не «догадываются» (тот же Ocaml, например), для меня загадка.

Что плохо - способ описания примитивных типов и поинтеров на типы. Вырвиглазненько.

Ещё не очень понимаю потребность в eval. Зачем вам такое хардкорное разделение на стейтменты и экспрешены? Мне кажется, разделения на декларации и экспрешены будет достаточно: если следующий элемент не декларация, начинающаяся с ключевого слова (тот же func), значит это - экспрешн. Присвоение тоже можно оставит экспрешеном, как в С.

Нашел язык: https://austral-lang.org/tutorial/borrowing . Они, правда, обзывают поинтеры ссылками (reference). Проблему решают несколько иначе: вместо неявного взятия значения и явного взятия адреса поступают ровно наоборот - цепочка перехода по поинтерам имеет простой синтаксис, а вот взятие значения по принтеру потом явное.

Мне кажется, должен быть какой-то третий вариант. И синтаксис разименования ближе к паскалевскому.

type Vec2 struct {
  x: float
  y: float
}

type VecPair struct {
  v1: Vec2
  v2: ^Vec2
}

var v VecPair

&v^&v1^&x ^= 1
&v^&v1^&y ^= v.v1.x + 4
&v^&v2 ^= malloc(sizeof(Vec2))
v.v2^&x ^= 2
v.v2^&y ^= v.v2^.x + 5

Выглядит, правда, по-наркомански или слегка шизоидно. Зато все операции довольно однозначны, и нет «ссылок».

Хотя, я туплю: если изначально считать переменные «указателями на место на стеке», и оператор . - взятием адреса на элемент структуры, то всё выходит более-менее стройно без вырвиглазного синтаксиса:

type Vec2 struct {
  x: float
  y: float
}

type VecPair struct {
  v1: Vec2
  v2: ^Vec2
}

var v VecPair

v.v1.x ^= 1 // используя адрес v, получить адрес v1,
            // потом адрес x и записать по этому адресу 1
v.v1.y ^= v.v1.x^ + 4 // нужно получить значение, лежащее по адресу v.v1.x
v.v2 ^= malloc(sizeof(Vec2)) // записали поинтер на выделенную память
                            // по адресу поля v.v2
v.v2^ ^= v.v1^ // получили значение v.v1, как структуру (а не её адрес),
               // и записали её по адресу, хранящемуся в v.v2
v.v2^.x ^+= v.v2^.y^

Конечно, получение значения полей через разименование всё ещё выглядит наркомански.

К тому же возникает вопрос, какова сущность v.v1^ ? Это получается «значение структуры без адреса». Можно ли у него взять значение поля x(тоже без адреса)? Каждой для этого может быть синтаксис? Можно ли дать ему имя, не копируя по другому адресу? И потом получить значение его поля x?

Можно предположить, что оператор ., примененный к безадресному значению, возвращает безадресное значение:

xv = v.v1^ // xv - безадресное значение, и потому "иммутабельно"
// xv.x = 3 - так сделать нельзя
// xv.x ^= 3 - и так тоже
v.v2^.x ^= xv.x // а так можно. xv.x не нужно разименовывать,
                // т.к. это уже значение, а не адрес
v.v2^ ^= xv // и так можно

Но не очень нравится, что оператор . становится полиморфным.

Задам свой любимый вопрос: а с отладкой как обстоят дела?
80% времени и нервов уходит именно на этот процесс. Что Вы сделали, чтоб программисту на Вашем языке было удобно отлаживать свой код?

Здравствуйте. Пока я об отладке не задумывался, и dwarf-секции не генерируются, поэтому отлаживать нормально код нельзя. Лично я баги компилятора изучаю, страдая читая выходной код на ассемблере (там есть немного комментариев).

Есть у меня ряд замечаний и предложений:

Не понятно, зачем имена типов имеют префикс # - это весьма ухудшает читаемость. Предположу, что синтаксический разбор просто на подобные имена завязан, что в теории вовсе не обязательно.

Почему привычный if не возможен - не ясно. Зачем вместо него городить eval?

Подход с ручным выделением памяти и передачей аллокаторов как в Zig - тупиковый. Код становится запутанным и чреватым ошибками. Подход с RAII, как в C++ или Rust гораздо удачнее.

Генерация asm вручную - слишком жёстко. Гораздо лучше использовать библиотеку LLVM. Это позволит получить поддержку всех возможных архитектур и систем и даст возможность фокусироваться на дизайне языка.

Протокол языкового сервера не очень то сложный, там тупо туда-сюда JSON шлются. Я под свой язык реализовал языковой сервер где-то за полтора месяца, включая кучу функционала, в том числе нетривиального.

Полиморфизм и вычисления времени компиляции сейчас необходимы, без них никуда. Да даже древний Си умел как-то константы во время компиляции вычислять и имел хоть и страшно-неудобный, но хоть как-то работающий полиморфизм через препроцессор.

Свой формат проекта тоже сейчас необходим. Вызывать компилятор руками слишком неудобно.

Свой формат проекта тоже сейчас необходим. Вызывать компилятор руками слишком неудобно.

Логика очень распространённая, но совершенно извращённая по сути. Неудобно вызывать компилятор – давайте наваяем ещё какие-то прибабахи в непонятном статусе относительно языка программирования. Хотя логичным здесь является упростить вызов компилятора и поручить ему самому разбираться с взаимосвязями модулей, как в модульных языках.

Ну превратить компилятор по сути в самодостаточную систему сборки - вполне рабочее, но не бесспорное решение. Ведь иногда (в редких случаях) нужно уметь просто собрать какой-то кусок кода, не заморачиваясь зависимостями и инкрементальной сборкой, для чего наличие просто компилятора имеет смысл. Rust тому пример - там есть cargo, но rustc вручную вызывать никто не запрещает.

Неплохо было бы поделится литературой и ресурсами которые использовались при создании всего этого? 🙂

Будем дробить expression-ы на primaries с помощью простого алгоритма со стеком операторов и стеком primaries. (Я не знаю, как этот алгоритм называется. Обычно его упоминают с обратной польской записью, но она скорее не используется здесь, а помогает догадаться об алгоритме.)

Очень похоже на алгоритм сортировочной станции Дийкстры.

Только тут два стека для операторов и операндов, насколько я помню, это упрощает написание то ли самого парсера, то ли чего-то ещё.

Обратная польская нотация тут используется, но опосредованно, как мне кажется, ведь:

2 + 2
// превращается в:
+ 2 2
// раскладываем по стекам операторов и операндов:
+
2 2

Но она от этого вроде бы не перестаёт быть обратной польской?

Если что, поправьте.

Sign up to leave a comment.

Articles