Хабрахабр

[Из песочницы] Как я 12 лет создавал свой ЯП и компилятор к нему

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

Меня зовут Александр, родился я в небольшом городке (меньше 10000 человек) в Беларуси. Здравствуй читатель! Не смотря на то, что семья была бедной, у матери были не бедные родственники, которые иногда дарили нам какие либо не дешевые вещи. Моя семья была бедной, игрушек крайне мало, про компьютер и какие либо приставки вообще можно не заикаться. Радости моей не было предела! И вот однажды (где то в 2001 году) эти самые родственники, дарят нам компьютер «Байт»(советский аналог ZX Spectrum 48k). Игры на этом компьютере загружались с обычных аудиокассет с магнитной лентой. Сразу же я начал, запускать на нем игры. Чтобы увеличить вероятность успешной загрузки, мне приходилось протирать спиртом и регулировать положение считывающей головки магнитофона. Загрузка одной игры длилась примерно 5 минут и с не малой вероятностью, могла прекратиться из-за некачественного сигнала. Но вместе с «Байт»-ом мне также подарили книгу, по работе с этим компьютером. Весь этот шаманизм при загрузке, длительность загрузки и невозможность сохраняться в играх, привели к тому, что постепенно я начал терять интерес к играм. В книге оказался учебник по встроенному в «Байт» языку программирования «Бэйсик».
Я просто взял и переписал первую попавшуюся программу из книги в компьютер, а затем запустил. Я решил прочитать эту книгу, чтобы узнать больше о возможностях «Байт»-а. Придя в себя, я начал менять различные числа в программе и после запуска фигура изменяла свою форму, я менял числа и запускал программу снова и снова, было ощущение, что я попал в другую вселенную и начал её изучать. Когда я увидел на экране круг, я просто обомлел — это были непередаваемые ощущения. Вначале писал их на листочке, компилировал и запускал в голове, а затем переносил это на компьютер и смотрел, что получится. Прочитав всё, что было в этой книге по бэйсику, я начал создавать свои собственные программы. Я стал целыми днями писать на листочке различные программы (включая игры). После того как я сам без учебника написал свою первую программу, я понял — это моё, это то чем я хочу в жизни заниматься.

Со временем на уроках нас начинают учить программированию в программе «ИнтАл». Примерно через год, после событий описанных выше, у меня в школе появляется информатика. Но в нашей школе был факультатив по информатике и я решил туда записаться, с надеждой, что на факультативе мне дадут больше программировать. На уроках мы программировали мало, так как было много других материалов не связанных с программированием, а также работали за компьютерами по очереди (компьютеров было меньше, чем учеников). Там были записи(они же структуры), юниты(они же модули и библиотеки), процедуры и функции. На факультативе мы начали изучать Turbo Pascal, с первого момента мне язык очень понравился. Начав изучать паскаль, я окончательно решил, что хочу стать программистом. Это был какой то новый прекрасный мир, прекраснее чем все, что я знал до этого.

Я решил пойти в лицей, по специальности оператор ЭВМ с углубленным изучением физики, математики, астрономии и информатики. Закончив 9 классов школы, у меня появился выбор, пойти в какой либо колледж, либо пойти в 10 класс. На решение вступительного экзамена давалось три часа, я использовал только 30 минут, из которых 10 я проверял, все ли правильно. Многие люди говорили, что если я отучусь по этой специальности, то мне будет проще поступить в ВУЗ на программиста. В лицее я продолжил изучать Pascal, но также начал изучать Photoshop, Corel Draw, Excel, командную строку Windows и .bat файлы. Поступил!

Родители скинулись и сделали апгрейд, купив 64 Mb оперативной памяти и видео карту Matrox Milenium g400 16 Mb, c поддержкой 16 миллионов цветов, разрешение стало 800 x 600 (ограничение монитора). Узнав, что я поступил, те самые родственники которые подарили мне «Байт», подарили мне новый компьютер со следующими характеристиками: одно-ядерный Intel Celeron 800 Mhz, 64 Mb оперативной памяти, 14 Gb жёсткий диск, встроенное видео с разрешением 640 x 480 и 16 цветов. В то время доступа в интернет почти не у кого не было, вся информация в основном бралась с дисков, которые продавались в магазинах.

Один из моих друзей купил несколько дисков по программированию на языке Delphi, на дисках был Borland Delphi 7, куча учебников, куча компонентов и программа с большим количеством вопросов по Delphi и с ответами на эти вопросы (эдакий офлайн StackOverflow на минималках).

Так же в библиотеке лицея был учебник по программированию на языке C. Все было настолько просто и удобно, что программы писались одна за другой. C сложнее и не так удобен, единственное, что мне в нем понравилось — вместо begin end используются фигурные скобки. Прочитав её, я понял, что мне C совершенно не нравится, Pascal и тем более Delphi мне нравились в разы больше. Например мне хотелось, чтобы строки и массивы были структурами со следующими полями: указатель на область памяти с данными, количество элементов, максимальное количество элементов которое может вместить область памяти на которую указывает указатель, чтобы несколько массивов или строк могли указывать на одну и туже область памяти и можно было создавать массив из куска другого массива не копируя элементы. Но не нужно думать, что Pascal был для меня идеальным языком, со временем многие вещи в языке стали меня раздражать(помимо begin end). Мне тогда захотелось написать транслятор который переводит из такого изменённого паскаля в обычный, но я отказался от этой идеи. Как позже выяснилось, такие массивы как я хотел, называются слайсами.

Я даже не знал, что это такое, но он бесплатно дал мне 2 диска. Тот самый друг который познакомил меня с Delphi, сказал, что некая компания бесплатно рассылает диски с какими-то Ubuntu и Kubuntu. Я был поражён — оказывается кроме Windows и dos есть, что-то ещё. Ubuntu у меня не запустилась, а вот Kubuntu запустилась и очень хорошо работала. К тому же к этому времени у меня уже появился интернет (5 Kb/s), а модем в Kubuntu не работал. Очень хотелось поставить Kubuntu на компьютер и по изучать его, но 14 Gb диск был категорически против. Неожиданно все тот же друг, начал бредить каким то ассемблером, все показывал мне какие то программы на нём, но у меня они вызывали лишь улыбку, поскольку 50 строк ассемблера заменялись одной строкой Delphi, но друг всё же уговорил меня попробовать и дал диск с FASM и учебниками по ассемблеру. Поэтому диск с Kubuntu был закинут на полку. Ассемблер мне не понравился, хотя макросы в FASM — классная штука.

Но не задолго до окончания лицея, я начал подрабатывать в интернете. И вот я закончил лицей, пришло время поступать в ВУЗ. Если этим мошенникам дать доллар, то как ни странно, через 2 недели они действительно возвращают 2 доллара, но если дать 10 то через 2 недели, будет указано, что у меня на счету 20 долларов, но снять их будет невозможно. В интернете есть куча мошенников которые, притворяются успешными предпринимателями и предлагают доверчивым пользователям следующую сделку — «Дайте нам хотя бы доллар, а через 2 недели мы вернем вам 2 доллара». Учитывая то, что я таким образом уже немного зарабатывал, меня стала посещать мысль — «А может мне не идти в ВУЗ? Я создавал множество почтовых ящиков и используя различные почтовые ящики и меняя ip адрес (у меня был динамический ip), регистрировал на сайтах мошенников множество аккаунтов, каждый раз кладя на них 1 доллар. К этой мысли меня также склоняло и то, что в интернете часто писали, что программисту вышка не нужна. Программировать в своё удовольствие я могу и без ВУЗ'а.». Мой отец зарегистрировался на сайте знакомств, нашёл себе новую любовь и кинул меня с матерью. В то время как я размышлял — «поступать или не поступать?», жизнь за меня решила сама. После лицея я обязан был отработать год на каком либо предприятии. Понимая, что мать одна не сможет меня содержать, пока я буду учиться, я не стал поступать в ВУЗ. Немного поработав в магазине, я написал программу которая анализирует базу данных продуктов в магазине и ищет потенциальные проблемы. Я устроился по специальности в продуктовый магазин, занимался выпиской ТТН и ввод пришедшего в магазин товара в компьютер. И я остался дальше работать в магазине. После этого в кампании, которая владела магазином, предложили перевестись к ним на должность программиста, но узнав, что у меня нет высшего образования — передумали. Низкими доходы были потому, что регистрация аккаунтов занимала много времени, а в Беларуси было очень тяжело работать с интернет деньгами. Зарплата у меня была не плохая, а вот доход от мошенников был очень скромным. Liberty Reserve — интернет деньги, работу которых обеспечивал банк в Коста-Рике, который не выдавал информацию о своих клиентах правительству, из-за чего большинство мошенников и использовали Liberty Reserve.

Я перестал обманывать мошенников и получал доход, только от работы. Покупать эти деньги в Беларуси было особо затруднительно, так как в автоматическом обменники приходилось платить в семь раз дороже(если память не изменяет), а покупать у людей было рискованным занятием. У меня была на руках не плохая сумма денег и я решил обновить свой компьютер. Отработав год, решил поискать, что нибудь по лучше. Это важно!), 1 Gb оперативной памяти, 80 Gb жёсткий диск. Характеристики нового ПК: AMD Athlon 64 x2 2600 (с поддержкой SSE2 инструкций, которых не было в предыдущем процессоре. Сходив в магазин компьютерных дисков, мною был приобретен диск с openSUSE 10. Вспомнив о том, что я хотел по изучать Kubuntu но у меня был слишком маленькие жёсткий диск, я решил поставит Linux и Windows одновременно, поскольку с диском у меня проблем уже не было. Моей новой работой стала починка компьютеров в одной из компаний моего города, с испытательным сроком в один месяц. 2. Я согласился. Начальник этой компании, а также его жена разрабатывали некое бухгалтерское ПО, узнав, что я увлекаюсь программированием, они предложили присоединится за процент от будущих продаж, но поскольку разработку они вели на Visual FoxPro и SQL, мне необходимо было выучить эти языки. Несколько месяцев безуспешных поисков привели к тому, что я пошёл на стройку подсобным рабочим. Через месяц оказалось, что они и не собирались меня брать на работу, а нужен я им был, чтобы подменить ушедшего в отпуск сотрудника, когда он вернулся, они сказали, что я им больше не нужен, но все же хотели, чтобы я им помог с разработкой, я разумеется отказался и начал искать новую работу.

5 раза меньше чем у оператора ЭВМ в магазине, но надо сказать, что и работа на порядок проще(я был крепким парнем). Зарплата у подсобного рабочего была в 3. Поэкспериментировав я понял, что в некоторых задачах, эти инструкции могут значительно увеличить производительность. В выходные дни, используя FASM, я начал изучать эти новые для меня SSE2 инструкции. Конечно можно в начале программы узнать, есть ли у процессора поддержка SSE2, и в зависимость от результата выполнять разный код, но в таком случае увеличивается сложность разработки и тестирования, а также увеличенное потребление оперативной памяти и кэша процессора. Мне стало интересно, как разработчики ПО встраивают в свои приложения SSE2 инструкции, ведь если все задачи в которых имеет смысл использовать SSE2, будут решены с их использованием — то программа не будет запускаться на компьютерах без поддержки SSE2, но если их не использовать — то программа будет работать медленнее. И тут я задался вопросом «А почему компиляторы не компилируют в ассемблер с макросами, а на компьютере конечного пользователя некая утилита не заполнит необходимые для макросов константы и только после этого создаётся бинарник?». Проанализировав несколько программ, я увидел, что большинство программ не использует SSE2. Так же мной было принято решение, добавить в язык дженерики и язык стал выглядеть примерно так: И я, вспомнив про видоизмененный паскаль, который я придумал в лицее, решил написать такой компилятор.

type Point(a){ x, y: a;
} type Line(a){ a, b: Point(a);
} function createLine(a, b: Point(a)) Line(a){ result.a = a; result.b = b;
}

Со временем у меня появился интернет с нормальной скоростью и я полностью отказался от Windows в пользу Linux(было много дисков с программами для Windows, а качать их аналоги для Linux с медленным интернетом проблематично). На тот момент у меня был дистрибутив Ubuntu 8.10, но я активно интересовался и другими дистрибутивами. В какой то момент, я решил попробовать дистрибутив под названием Gentoo. Разбираясь как устанавливается Gentoo я узнал про use-флаги и сразу понял, что их поддержку нужно добавить в свой язык. То есть, при установке программы написанной на моём языке, утилита которая добавляет константы в ассемблерный код, будет также спрашивать значение констант которые определил программист при разработке и в зависимость от этих констант, макросы будут вставлять различный код. Почти закончив разработку компилятора, я из форумов узнаю о каком-то LLVM, в котором есть некий ассемблероподобный язык LLVM IR. Изучив немного, что это и с чем его едят, я решил компилировать не в FASM, а в LLVM IR, поскольку он умеет оптимизировать код, а мой компилятор практически ничего не оптимизировал, не говоря о том, что разработчики LLVM со временем наверняка будут добавлять новые оптимизации, а это значит, что мне не надо будет этого делать и я смогу сосредоточится на других вопросах. И я начал переписывать свой компилятор. В LLVM IR помимо инструкций, были также параметры функций и параметры аргументов функций, например можно было указать, что аргумент функции, который является ссылкой, не будет скопирован. И я решил добавить такие параметры, только указывать их нужно было не при объявлении функции, а при вызове и в отличии от LLVM IR, эти параметры носили не информативный характер, для оптимизации, а были требованием поведения. Пример:

function inc(a: ^Integer) Integer{ result = a^ + 1;
} procedure foo(){ var a: ^Integer; b: Integer; ... //некий код ... b = inc(ro nocp a) //если бы функция inc изменяла значение по ссылке или копировала ссылку, то код бы не компилировался
}

Со временем я уехал в Минск и устроился грузчиком, зарплата была очень хорошей (800 $/месяц), хотя времени на разработку стало меньше. Читая форумы, я стал очень часто натыкаться на упоминания языка Haskell, упоминания обычно сопровождались фразами вроде: «взрывает мозг», «очень необычно», а также упоминалось, что там есть какие-то монады, которые тяжело понять. Я не выдержал и решил изучить этот язык. Скачав целую кучу учебников по Haskell и начал учить. Взял первый учебник, написано очень мутно — удалил, взял второй, читаю и испытываю ощущение, будто все страницы перетасованы — удалил, беру третью, книга написана толково, но она как будто рассчитана на изучение через примеры(а я люблю изучить теорию, а затем подкрепить её практикой) — удалил, дело доходит до четвёртой, написано толково, изучается через теорию — похоже то, что надо. Книга называлась «Изучай Haskell во имя добра». Во время прочтения у меня не возникло ни одного вопроса, никаких проблем с монадами у меня так же не возникло. Мне настолько понравилась книга, что я пошёл и купил её бумажный экземпляр. Прочитав до конца книгу я понял, что все языки которые я видел до этого, попросту меркнут на фоне Haskell, это просто какой-то новый, недостижимый для других языков уровень. Я решил изучить Haskell глубже, прошёл курсы по Haskell(а заодно по основам статистики и языку Python), начал смотреть лекции по функциональному программированию и лямбда исчислению Чёрча.

Я писал на Haskell всякие мелкие утилиты. Очень хотел изучить теорию категорий, но не нашёл никаких материалов объясняющих эту теорию простым и понятным языком. Написал резюме и откликнулся на вакансию. В какой-то момент я наткнулся на вакансию Junior Haskell Developer прочитав требования я понял, что подхожу под все требования и решил попробовать. Данная вакансия висела на сайте ещё год. Думая, что мне пришлют для проверки навыков тестовое задание, я отрыл IDE, firefox + google и стал ждать ответа, ответ пришёл примерно через полтора часа где меня вежливо послали. Haskell стал моим основным языком и я начал писать на нём всё подряд.

Я забросил Haskell и продолжил разработку своего компилятора, но решил добавить в свой язык функции высшего порядка и убрать фигурные скобки, а для структурирования кода использовать отступы (как в Haskell). Со временем я понял, что Haskell очень хорош, только если использовать декларативное программирование, но при таком раскладе программы получаются медленными и потребляющими чрезмерное количество памяти, на Haskell можно писать эффективно, но в этом случае приходится использовать императивный подход, а писать на Haskell императивно — сомнительное удовольствие! Язык стал выглядеть так:

function lineLength(line: Line) Real result = sqrt(sqr(line.a.x - line.b.x) + sqr(line.a.y - line.b.y))

В какой-то момент случился кризис и мои 800$ превратились в 400$, аренда комнаты 120$ + дорога на работу и с работы + коммуналка и с зарплаты остаётся примерно 250$. А зачем мне жить в арендной комнате и получать 250$, если я могу жить в нормальной квартире и получать примерно те же деньги у себя в городе? Я решил уволится. После увольнения у меня оставалась некоторая сумма денег и подумав, я решил — сниму жильё в любом населенном пункте Беларуси и там, нигде не работая и не на что не отвлекаясь, допишу компилятор. Я нашёл в одном городке целый дом за 35$ в месяц. Дом не большой, старый, туалет на улице, нет душа и ванной комнаты. Но ценник был очень соблазнительный, да и хозяйка сказала, то могу выедать всё, что есть в огороде, а там было много овощей и фруктов, из-за чего я мог сильно сэкономить на еде. Я согласился и заплатил аренду за 2 месяца. На перевозку всех вещей у меня ушёл целый день и заселился я только ночью, из-за чего придя домой я сразу лёг на кровать и уснул. Проснувшись, я увидел, что хлеб, который я оставил в пакете на столе, погрызен крысами, «Ну и чёрт с ним, что крысы в доме. Буду прятать продукты в металлический холодильник» — подумал я в тот момент и пошёл на улицу. Выйдя на улицу, я почувствовал покусывания в ногах, закатав колоши, я увидел кучу блох (14 штук). Изучив квартиру, я обнаружил, что они обитают в определенном месте в доме, которое находится далеко от комнаты, где я сплю, но чтобы выйти на улицу, я должен пересечь их логово. В общем, большую часть времени я находился в безопасной комнате (и блох на мне действительно в это время не появлялось), а когда нужно было выйти на улицу, я быстро пробегал через блохастую комнату, иногда даже выходя на улицу не подцепив ни одной блохи, но чаще всего 1-2 все же цеплялись. Периодически я созванивался с матерью и в одном из разговоров я рассказал про блох. Пообщавшись, мы с матерью договорились, что я возвращаюсь домой, но 3 месяца не ищу работу, а буду писать компилятор и в это время она не будет меня донимать. Вернувшись домой, я менее чем за 3 месяца дописал компилятор. Прежде чем продолжить, я хочу вам рассказать о подходе который я использую при разработке. Свой подход я со временем выработал сам и старался всегда его придерживаться(хотя иногда меня заносит и я забываю его использовать).

Я пишу эту недостающую функцию (используя всю мощь функций и типов из выдуманной библиотеки), затем запускаю компилятор и если не хватает типа, создаю его, а если не хватает функции, то эта функция становится той самой которую мне необходимо написать. Подход заключается в следующем: я представляю, что существует библиотека со всеми возможными типами и функциями, кроме одной, той самой которую мне сейчас надо написать. Когда я закончил компилятор, разумеется в нем было куча ошибок. Сразу скажу, что сейчас я изменил свой подход, и вместо представления о том, что в библиотеке есть функции и типы, я представляю, что там есть классы и методы (даже если язык не объектно ориентированный). Долгое время исправляя ошибки(а к слову говоря, больше чем искать ошибки, я ненавижу только писать юнит тесты), я наткнулся на ошибку для исправления которой необходимо переписать 60% кода. Я начал поиск и исправление ошибок, но из-за большого количества возможностей(дженерики, параметры аргументов функций, классы типов, функции высшего порядка), ошибок было крайне много, а из-за подхода который я использовал при разработке, изменение одной функции могло приводить к изменению большой группы функций. Я решил попробовать взять за основу ООП и для того, чтобы язык был простым, придерживаться следующих правил: Я впал в отчаянье и думал, как мне мне сделать так, чтобы язык был простым, но поддерживал все мне необходимое.

  1. В языке должны быть только классы, свойства и методы(ничего больше).
  2. Каждый метод, в конкретном классе должен компилироваться в одну LLVM IR функцию. То есть, в классе A есть метод m, который в не зависимости от того какие аргументы ему будут переданы, всегда компилируется в единственную LLVM IR функцию, но если B наследует A, то метод m уже компилируется в другую LLVM IR функцию.
  3. Управление памятью должно быть автоматическим.
  4. Язык не должен иметь ссылок.

Я приступил к обдумыванию, как сделать необходимые для языка возможности не нарушая правила. И вот, что я придумал:

  • Как сделать аналог классов типов из Haskell?

    Пример: Создать абстрактные классы, которые могут иметь методы, которые при наследовании в не абстрактный класс необходимо перезаписать.

    abstract class Printable require method (Printable) print()
    method (Printable) printLn() this.print() String.eol().print()

  • А если нужно унаследовать от абстрактного класса существующий класс?

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

    class Some(SomeParent) a, b String class Some(Some, Printable) override method (Some) print() this.a.print() this.b.print()

  • Как отказаться от ссылок?

    Работать с переменными только как со значениями(но 2 переменные могут содержать одно и то же значение, изменяя которое в одной переменной — изменяет и во второй).

  • А как без указателей передавать аргументы в функцию, которая должна менять эти аргументы?

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

    method (Some) foo(a :String, b String) // a - можно изменять, b - нет c := 10 // c можно изменять d .= 12 // d нельзя изменять с:someMethod() // вызывается метод someMethod, c передается как изменяемый объект c.someMethod() // вызывается метод someMethod, c передается как неизменяемый объект //2 метода выше, хоть и имеют одно имя, являются двумя разными методами, которые могут иметь разные сигнатуры

  • Как сделать дженерики?

    Пример: При указании какого класса является объект, можно указать видоизмененный класс.

    class Point x,y Number method (Some) foo(a Point<x UInt64, y UInt64>) //аргумент a является экземпляром класса, который является наследником класса Point, но с измененными классами свойств x и y. Разумеется новый класс свойства, должен быть наследником старого класса.

  • Логично, чтобы в классе Point свойства x и y всегда имели один тип.

    class Point x, y !Number // класс свойства x можно менять, а y всегда как x

  • А если свойства приватные?

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

    class Point numberClass #Number private x, y .numberClass method (Point) createPoint(x, y .numberClass) This //This - класс такой же как и у объекта к которому применяется метод result:x = x result:y = y

  • Как сделать так, чтобы любой метод конкретного класса имел только один вариант LLVM IR функции?

    Пример: Использовать в качестве классов аргументов методов указатели на свойства (как выше) или указывать конкретные классы, которые не меняются при наследовании.

    method (Some) foo(a .z, b Point<numberClass .f>, c UInt64)
    //a имеет класс как у свойства z класса к которому применяется метод
    //b имеет класс который наследуется от класса Point но с измененным классом свойства numberClass, новый класс свойства имеет такой же класс как и свойство f класса к которому применяется метод
    //c всегда имеет класс UInt64, изменить его при наследовании нельзя

  • Как реализовать автоматическое управление памяти?

    Если счётчик достиг 0 — освобождать память. При выделении памяти на куче, к этой памяти добавлять счётчик, которые увеличивается, если какой-то объект использует эту память и уменьшается, если какому либо объекту, эта память не нужна.

Концепт выглядит неплохо, приступил к разработке с нуля. Мои три месяца прошли и пришла пора искать работу. Время тяжёлое, работы нигде нет, ходят слухи, что даже работников отдела кадров, заставляют печатать фальшивые вакансии, чтобы создать видимость, что не всё так плохо. И я верю этим слухам, так как, часто звонишь по объявлению, а мне сразу говорят — вакансия не актуальна, хотя вакансия регулярно обновляется и висит на протяжении долгого времени. Поиск хоть какой-нибудь работы занял 5 месяцев. Устроился я в департамент охраны на должность контролёр КПП, на объект находящийся в туберкулёзной больнице. В мои обязанности входило: досмотр больных и посетителей. Зарплата — 170 $/мес. Несмотря на то, что часов работы стало больше, чем на предыдущих работах, времени на разработку тоже стало больше, поскольку работа была посменная. И вот я дописал весь код, создал стандартную библиотеку, написал для библиотеки тесты.

Например некоторые ошибки проявлялись через раз, некоторые проявлялись на уровне оптимизации O0 и O2, но не проявлялись на O1 и O3. Запустив тесты я увидел множество ошибок, в процессе их исправления я понял, что большинство из них связаны с неопределенным поведением, а устранять их крайне тяжело. Во время изучения мною LLVM IR, я смотрел, что генерирует clang из C. Конца и края ошибкам я не видел, а толкового инструментария для поиска ошибок в LLVM IR, я не нашёл. Так зачем я использую LLVM IR? В какой-то момент я понял, что превращение из C в LLVM IR занимает крайне мало времени, а основную часть занимает оптимизация LLVM IR. Ведь его оптимизация все равно будет происходить на компьютере конечного пользователя.

Используя C получаются одни плюсы: нормальные макросы, упрощение написания кода, кучу инструментария, перспектива при необходимости использовать сторонние C библиотеки. Так почему мне не использовать C вместо LLVM IR? Когда я закончил замену, я быстро нашёл и исправил все ошибки найденные в тестах. И я начал переписывать компилятор заменяя LLVM IR на C. Я решил изменить количество уровней мутабельности переменных, this объектов и аргументов методов с 2 (мутабельные и иммутабельные) до 4. Во время написания компилятора, самой распространённой моей ошибкой, было изменение какого либо значения по указателю, думая, что он уникален, а на самом деле на объект по указателю указывало несколько ссылок. Но для того, чтобы понять новые уровни, все таки необходимо уточнить некоторые детали. В этой статья опущено множество деталей, так как объяснение всех нюансов и тонкостей на всех этапах создания языка, заняло бы 20 — 40 таких статей (которая и так получается огромной). Я очень хотел, чтобы программы на моём языке были производительны, а выделять память под каждую переменную — далеко не производительно. Когда я писал про то, что 2 переменные могут содержать одни и те же данные и изменяя одну переменную, вы можете повлиять на вторую, возможно вы подумали, что при создании переменной, в куче создаётся объект, а переменная хранит указатель на этот объект, но это не так. При присвоении переменной какого либо объекта, значения объекта копируются в перемененную и все указатели указывают на одну и туже область памяти. Поэтому при создании переменной значение хранится в переменной, но в значении может быть указатель.

А теперь вернемся к уровням мутабельности, вот два новых уровня:

  • Можно менять значение переменной, но нельзя менять значения по ссылкам.
  • Нельзя менять значение переменной, но можно менять значения по ссылкам.

Возможно у вас возник вопрос — «Зачем учитывать ссылки, если язык их не поддерживает?». Отвечаю, их не нужно было учитывать, просто нужно было придерживаться нескольких правил, нарушая которые в большинстве случаев, была бы ошибка компиляции. Также мною была добавлена возможно указывать, что this объект в методе будет не читаем (по аналогии со свойством). Язык стал выглядеть так:

method (#Some) foo(a String, b `String, c ~String, d :String)
//this нельзя использовать
//a можно только читать
//в b можно заменить любой символ, но нельзя присвоить новую строку или изменить длину строки
//c может присвоить новую строку и изменить длину строки, но нельзя изменить символы в уже переданной строке
//d можно изменить, что угодно

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

method (~Some) veryBadMethod(value This) this = value method (#Some) foo() a := Some b .= Some#randomValue() //подразумевается, что с использованием b не возможно ничего изменить a~veryBadMethod(b) a:someMethod() //а вот здесь, вышеуказанное правило нарушается

В тот момент когда я осознал проблему, я был в бешенстве. Я честно пытался создать безопасный, простой и обязательно создающий производительные программы язык, но всё тщетно. Все, надоело, плевать на производительность, рубисты же как то выживают. Решил — будет только 2 уровня мутабельности, как в начале, а при присвоении какого либо объекта, его значение полностью копируется, в этом случае ещё не надо парится насчёт управления памятью, объект не нужен — значит полностью его освободить.

Кашель был настолько сильным, что я подумал «Все! Во время очередной переписи компилятора, я начал очень сильно кашлять, сопровождалось всё это высокой температурой. Поработал в туберкулёзной больнице.». Молодец! Оказалось — острый бронхит. Я пошёл в поликлинику, где мне сделали снимок лёгких и положили в больницу. Количество таблеток от давления и от бронхита в сумме составляла 11 штук в день. Во время пребывания в больнице, мне постоянно меряли давление, верхняя граница которого частенько была в диапазоне 150-180. В больницу я по этому поводу не пошёл (почему? После выхода из больницы, у меня начались проблемы с когнитивными способностями, особенно это касалось памяти, у меня были такие ситуации когда я о чём-то думал и в процессе думания, забывал о чём и долго не мог вспомнить. Со временем мои способности начали восстанавливаться, но полностью до сих пор не восстановились. в двух словах не рассказать, статью целую написать можно). Вернувшись с больницы, плохо соображающий, почти 30-летний парень, живущий с матерью, на окраине мира, за 200$ в месяц (немного увеличилась зарплата), потративший свою молодость на то, что так и не смог сделать, на меня накатилось крайне депрессивное состояние.

В одной из игр (Disciples 3) был такой юнит — верховный вампир, когда он изрядно ранен, он становится гораздо сильнее и произносит фразу — «Погибну сражаясь!». Я начал прокрастинировать, в основном играя в компьютерные игры. Я закончу свой компилятор, чего бы мне это не стоило! Эта фраза меня вдохновила, ведь какой смысл был в моей жизни, если я не закончу то, что начал? Практически сразу после начала разработки, я подумал «Зачем я полностью копирую объекты? Я продолжил разработку, причём если раньше я писал только по выходным, то теперь я частенько стал писать и после работы. Если хаскелю дать сложить 10 чисел, то он их не сложит, а лишь пометит себе, что их надо сложить, но непосредственно сложение будет произведено в момент когда понадобится результат вычисления, а такое событие может и не произойти. Ведь я могу сделать как в Haskell.». Я могу сделать так-же, не копировать объекты, а пометить сколько объектов владеет областью памяти (тем более у меня есть счетчики, те самые которые обеспечивают автоматическое управление памятью) и если памятью владеет несколько объектов, и значения в этой памяти нужно изменить, то только тогда она будет скопирована.

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

Но теперь уже таких проблем нет. В тот момент когда в языке было множество вариантов мутабельности, я подумывал над ещё одним дизайном языка, но не стал его реализовывать, так как при стольких вариантах мутабельности он был крайне неудобен, а то, что переменные могут быть взаимосвязаны, делало бы язык небезопасным. Новый язык больше не объектно ориентированный, вместо классов теперь типы, с возможностью создавать дженерики. И я начал всё заново. Пример:

type Point(a) x,y a type SimplePoint x,y Double //где-то в коде
a := Point(UInt64)

Есть аналог класса типов из Haskell, в моём языке он называются группой, но в отличии о класса типов, в группе не может быть каких либо ограничений на то, какой тип может быть членном группы. Логически они есть, но из-за особенностей языка их нельзя реализовать. Поэтому требования к группе я пишу в комментариях при создании группы. Пример:

group Equal
//equal(a, a) -> Bool

То есть, если тип является членом группы Equal, то должна быть функция с именем equal, которая должна принимать 2 аргумента данного типа, и возвращать тип Bool.

В этом коде можно добавить тип в группу. В языке появились правила (rules) — это такой мини язык программирования, код которого выполняется во время компиляции. Пример:

rules 1 = type == [_] //если некий тип является слайсом 2 = type[0] > Equal //а его элементы входят в группу Equal 3 = 1 & 2 join 3 Equal //то добавить такой тип в группу Equal

Теперь может быть бесконечное множество функций и процедур с одинаковым именем. Для того, чтобы определить какую функцию из множества доступных вызвать, в функциях так же есть правила, которые оперируя типами передаваемых аргументов, могут определить — подходит ли функция к данным аргументам. У функций есть приоритеты, от 1 до 9 (по умолчанию 5), если при вызове функции, есть несколько подходящих варианта с одинаковым приоритетом, и нет подходящего варианта с приоритетом повыше, то это ошибка компиляции. Пример функции:

func notEqual(a, b) rules final = a > Equal & a == b result = Bool result = !(a == b) //! и == синтаксический сахар, данное выражение эквивалентно result = (neg(equal(a, b)) или a.equal(b).neg()

Закончив разработку нового компилятора, переписав стандартную библиотеку и тесты для неё, я начал перепись компилятора на собственном языке, как я писал ранее, для меня это принципиально. Я очень боялся, что снова обнаружу проблему в языке, которую не смог предусмотреть заранее, но все прошло гладко, а писать на таком языке, было сплошным удовольствием. Для сборки программ, я использовал самописные bash скрипты, но разумеется это не вариант. Поэтому я написал(разумеется на своём языке) сборщик и установщик пакетов.

Вот ссылки на github: И вот наконец, я готов представить вашему взору: язык программирования cine, компилятор с таким же именем, сборщик и установщик проектов fei.

Инсталятор cine и fei.
Исходники компилятора.
Исходники установщика и сборщика пакетов.
Исходники стандартной библиотеке(я использую название модуль).
XML файл для создания подсветки синтаксиса в текстовом редакторе Kate.

Для установки и работы вам необходим Linux x86_64(к сожалению пока только так) и установленный clang.

Поскольку никаких учебников по языку ещё не существует, я открыл блог, где буду писать туториалы по программированию на cine.

Интересные факты:

  • Названия cine и fei произошли от сгенерированно самописным генератором случайных имен n-kine и enfei.
  • В коде на языке cine можно использовать код написанный на C.
  • Компилятор cine автоматически сортирует члены типов по выравниванию (от большего к меньшему).
  • Язык изначально назывался 4ernika, так как в момент когда я решил его создать, я ел чернику.
  • Когда язык был объектно ориентированным, была возможность при наследовании класса от нескольких классов, объединять их свойства. Например size и count объединить в одно свойство, то есть при обращении к свойству size или count, обращение происходило к одному и тому же свойству. Так же была возможность наоборот, создать класс в котором одно свойство имело несколько имен, а при наследовании, этим именам можно было назначить разные свойства.
  • С самого начала и до конца, в языке не было глобальных переменных (оказался от них ещё в лицее).
  • Во время написания одной из версий языка, я придумал новый алгоритм обучения нейронных сетей основанный на новом виде чисел, которые находятся над комплексными (все комплексные числа можно представить в виде новых чисел, но не все новые числа можно представить в виде комплексных). Мне очень хотелось попытаться реализовать этот алгоритм и попробовать его в деле, но поскольку это заняло бы немало времени, я сфокусировался на компиляторе. Но рано или поздно вернусь к этому алгоритму.
  • В языке cine нет специального типа для строк, строка в cine — слайс из элементов типа UInt8, но в каждом слайсе есть специальный флаг, которым помечаются строки.
  • Когда я перешел от использования LLVM IR к C, встал выбор об использовании clang или GCC. Написав несколько примеров, которые скомпилировал двумя компиляторами в ассемблерный код, просмотрев оба файла, я остановился на clang.
  • Хотя изначально язык затевался с целью возможности включения SIMD инструкций в код, в версии 0.1 нет явного использования SIMD (оно обязательно появится), но в одном из проходных вариантов языка, я не выдержал и реализовал поиск элементов слайса с использованием SIMD инструкций.
Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»