Здравствуйте, Sinclair, Вы писали:
S>Кстати, есть отдельный набор вопросов, не относящихся к теме топика, про ваш язык и опыт его разработки.
S>1. Есть ли где-то публичное описание этого языка?
S>2. Поверх чего он исполняется (JVM, WASM, натив)?
S>3. Компилятор или интерпретатор?
S>4. Каким тулчейном вы пользовались при его разработке? Языки программирования, библиотеки/фреймворки для парсинга/семантического разбора?
Попробую ответить на всё сразу. Нормального описания при мне не было, язык мог внезапно измениться на уровне работы базовых бильтинов, при этом мы всей командой тратили день на адаптацию кодовой базы.
При разработке использовали компилятор, написанный на нём же самом. Вернее это был транслятор в текстовый LLVM-IR. Исходники старой версии тут:
https://github.com/Matway/mpl-c/tree/master
Язык стековый как Форт. То есть обратная польская запись, стек сущностей, ну и все функции описываются как операции над стеком. Поэтому у него очень маленький синтаксис и минимальный набор конструкций. Очень важно что стек есть лишь при компиляции.
Базовые сущности языка — это числа, строки, блоки кода, анонимные кортежи, именнованные кортежи, и все они могут быть положены в виртуальный стек, который существует лишь на этапе компиляции. Ещё есть слова. Всё остальное строится из них. Любое слово производит какие-то манипуляции над "стеком времени компиляции". Например, слово if со стека снимает булевую переменную и два блока кода, а кладёт результат выполнения блоков кода. При этом если компилятор не может вычислить условиe if то он считает его динамическим и требует чтобы блоки кода одинаково действовали на стек — ну иначе стек станет частью рантайма. Для циклов тоже есть аналогичное требование, что если цикл "динамический" то он должен сохранять стек.
Можно заводить переменные, само собой. Причём переменная может быть блоком кода и передана куда-то в другую функцию. И это на самом деле основной способ задания функций в языке.
Язык позволяет делать адскую метушню, например написать функцию switch, которая принимает анонимный кортеж, в котором по очереди идут блоки кода и константы (например
(1 [doSmth1] 2 [doSmth2]) condVar switch
), и прямо в процессе компиляции превращает его в лесенку ифов
condVar 1 = [
doSmth1
] [
condVar 2 = [
doSmth2
] [
] if
]
которая оптимизаторов шланга превращается в честный свич
В языке вообще сделан жёсткий упор на то, чтоб всё нахрен вычислить во время компиляции. И то, о чём я говорил про то, что сигнатура функции определяется по аргументам:
foo: [ a: b: ;; // снять со стека две сущности и завести локальные переменные
a 1 = [
c:; // снять со стека ещё сущность
] [] if
];
Тут мы видим if у которого ветки по-разному действуют на стек, поэтому он может скомпилироваться лишь при статически известном условии. При этом содержимое "ложной" ветки игнорируется и может содержать вообще неизвестные имена и невалидные конструкции.
Так вот, если вызвать код так:
1 2 3 foo // первая переменная 1, поэтому создастся инстанс foo который жрёт 3 аргумента
2 3 foo // первая переменная на 1, поэтому создастся инстанс foo который жрёт 2 аргумента
i: 42 dynamic; // создаём переменную i в которой лежит 42 и просим компилятор не делать с ней никаких вычислений времени компиляции
i 2 3 foo // а тут компилятор пытается создать инстанс для foo у которого первый аргумент неизвестен при компиляции. Правда в данном случае это не получится, потому что внутри находится иф, у которого разные ветки по-разному действуют на стек. Так что будет ошибка компиляции
Ну и это вот слово dynamic на самом деле приходилось пихать везде, кроме того 1% кода где реально нужна метушня. Потому что мне похрен на то, что статически известное значение аргумента позволит развернуть один из внешних циклов, даже близко не влияющих на производительность. Зато компилятор будет дольше страдать компилируя нахрен не нужный инстанс, это раздует бинарник, зачем оно.
В целом язык прикольно поиграться, но для больших проектов не подходит ну ваще никак.
Причём часть проблем являются принципиальными и более лучший умный компилятор их не решит. И часть из них я называл ранее. А ещё из-за шаблонности всего-всего ни о каком нормальном автодополнении не может быть и речи. То есть после вызова другой функции ты не имеешь никакого представления что у тебя лежит на стеке, и куда дальше пойдёт выполнение и какие ветки проигнорятся. И чтобы это узнать, надо полностью проанализировать вызываемую функцию. Из-за этого крайне сложно делается рекурсия для функций у которых непонятно какой тип возвращаемого результата, потому что если функция вызывает саму себя, то для того чтобы понять, что дальше вообще лежит на стеке после вызова, надо проанализировать полностью всё тело этой функции, но мы же как раз находимся в процессе. Приходилось делать "полускомпилированные функции" у которых есть "предположительный тип результата" и на основе этого предположения компилировать функцию ещё раз и доходить до "неподвижной точки".
А с автодополнением в лучшем случае студия выдаст просто список ВСЕХ методов всех объектов видимых из этого файла.
Ну и раздельная компиляция идёт нафиг, потому что изменение тела одной функции может внезапно повлиять на компиляцию чего угодно в любом другом файле.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте