Главная » Хабрахабр » [Из песочницы] Новый язык программирования Mash

[Из песочницы] Новый язык программирования Mash

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

проги на нем можно собрать под огромное число платформ, да и сам компилятор выдает очень даже оптимизированные бинарники (собираю все составляющие проекта с O2 флагом). В этой статье я хочу осветить основные этапы своей работы и для начала описать созданный концепт языка и его первую реализацию над которой сейчас работаю.
Заранее скажу, что писал весь проект на Free Pascal, т.к.

Среда выполнения языка

Первым делом стоит рассказать о виртуальной машине, которую мне пришлось писать для выполнения будущих приложений на моём языке. Решил я реализовывать стековую архитектуру, пожалуй, потому что так было проще всего. Ни одной нормальной статьи как мне это сделать на русском я не нашел, так что после ознакомления с англоязычным материалом я засел за проектирование и написание своего велосипеда. Далее буду приводить свои «передовые» идеи и разработки в этом деле.

Реализация стека

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

В ВМ используется несколько стеков:

  1. Основной стек.
  2. Стек для хранения точек возврата.
  3. Стек сборщика мусора.
  4. Стек обработчика try/catch/finally блоков.

Константы и переменные

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

Сборщик мусора

В моей ВМ он полуавтоматический. Т.е. разработчик сам решает когда нужно вызвать сборщик мусора. Работает он не по обычному счетчику указателей, как в тех же Python, Perl, Ruby, Lua и т.д. Он реализован через систему маркеров. Т.е. когда подразумевается, что переменной присваивается временное значение — указатель на это значение добавляется в стек сборщика мусора. В дальнейшем сборщик быстро пробегается по уже готовому списку указателей.

Обработка try/catch/finally блоков

Как и в любом современном языке, обработка исключений — важная его составляющая. Ядро ВМ обернуто в try..catch блок, который может вернуться к исполнению кода, после поимки исключения, поместив в стек немного информации о нем. В коде приложений можно задавать try/catch/finally блоки кода, указывая точки входа на catch (обработчик исключения) и на finally/end (конец блока).

Многопоточность

Она поддерживается на уровне ВМ. Это просто и удобно для использования. Работает без системы прерываний, так что код должен выполняться в нескольких потоках в несколько раз быстрее соответственно.

Внешние библиотеки для ВМ

Без этого никак не обойтись. ВМ поддерживает импорты, подобно тому, как это реализовано и в других языках. Можно написать часть кода на Mash и часть кода на нативных языках, затем связав их в одно целое.

Транслятор с высокоуровневого языка Mash в байткод для ВМ

Промежуточный язык

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

Архитектура транслятора

Выбрал я не самую хорошую архитектуру для реализации. Транслятор не строит дерево кода, как подобает прочим трансляторам. Он смотрит на начало конструкции. Т.е. если разбираемый кусок кода имеет вид «while :», то очевидно, что это конструкция while цикла и обрабатывать её нужно как конструкцию while цикла. Что-то вроде сложного switch-case.

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

Оптимизация кода

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

Язык Mash

Основная концепция языка

Основной идеей было разработать максимально функциональный и простой язык. Считаю, что со своей задачей разработка справляется на ура.

Блоки кода, процедуры и функции

Все конструкции в языке открываются двоеточием : и закрываются оператором end.

В скобках перечисляются аргументы. Процедуры и функции объявляются как proc и func соответственно. Все как у большинства других языков.

Оператором return можно вернуть из функции значение, оператор break позволяет выйти из процедуры/функции (если он стоит вне циклов).

Пример кода:

... func summ(a, b): return a + b
end proc main(): println(summ(inputln(), inputln()))
end

Поддерживаемые конструкции

  • Циклы: for..end, while..end, until..end
  • Условия: if..[else..]end, switch..[case..end..][else..]end
  • Методы: proc <имя>():… end, func <имя>():… end
  • Label & goto: <имя>:, jump <имя>
  • Enum перечисления и константные массивы.

Переменные

Транслятор их может определять автоматически, либо если разработчик пишет var перед их определением.

Примеры кода:

a ?= 10
b ?= a + 20

var a = 10, b = a + 20

Поддерживаются глобальные и локальные переменные.

ООП

Ну вот и подобрались мы к самой вкусной теме. В языке Mash поддерживаются все парадигмы объектно-ориентированного программирования. Т.е. классы, наследования, полиморфизм (в т.ч. динамический), динамические автоматические рефлексия и интроспекция (полная).

Без лишних слов, лучше просто приведу примеры кода.

Простой класс и работа с ним:

uses <bf>
uses <crt> class MyClass: var a, b proc Create, Free func Summ
end proc MyClass::Create(a, b): $a = new(a) $b = new(b)
end proc MyClass::Free(): Free($a, $b) $rem()
end func MyClass::Summ(): return $a + $b
end proc main(): x ?= new MyClass(10, 20) println(x->Summ()) x->Free()
end

Выведет: 30.

Наследование и полиморфизм:

uses <bf>
uses <crt> class MyClass: var a, b proc Create, Free func Summ
end proc MyClass::Create(a, b): $a = new(a) $b = new(b)
end proc MyClass::Free(): Free($a, $b) $rem()
end func MyClass::Summ(): return $a + $b
end class MyNewClass(MyClass): func Summ
end func MyNewClass::Summ(): return ($a + $b) * 2
end proc main(): x ?= new MyNewClass(10, 20) println(x->Summ()) x->Free()
end

Выведет: 60.

Да это же рефлексия!: Что на счет динамического полиморфизма?

uses <bf>
uses <crt> class MyClass: var a, b proc Create, Free func Summ
end proc MyClass::Create(a, b): $a = new(a) $b = new(b)
end proc MyClass::Free(): Free($a, $b) $rem()
end func MyClass::Summ(): return $a + $b
end class MyNewClass(MyClass): func Summ
end func MyNewClass::Summ(): return ($a + $b) * 2
end proc main(): x ?= new MyClass(10, 20) x->Summ ?= MyNewClass::Summ println(x->Summ()) x->Free()
end

Выведет: 60.

Теперь уделим минутку интроспекции для простых значений и классов:

uses <bf>
uses <crt> class MyClass: var a, b
end proc main(): x ?= new MyClass println(BoolToStr(x->type == MyClass)) x->rem() println(BoolToStr(typeof(3.14) == typeReal))
end

Выведет: true, true.

Об операторах присваивания и явных указателях

Оператор ?= служит для присвоения переменной указателя на значение в памяти.
Оператор = изменяет значение в памяти по указателю из переменной.
И теперь немного о явных указателях. Добавил я их в язык чтобы они были.
@ — взять явный указатель на переменную.
? — получить переменную по указателю.
@= — присвоить значение переменной по явному указателю на неё.

Пример кода:

uses <bf>
uses <crt> proc main(): var a = 10, b b ?= @a PrintLn(b) b ?= ?b PrintLn(b) b++ PrintLn(a) InputLn()
end

Выведет: какое-то число, 10, 11.

Try..[catch..][finally..]end

Пример кода:

uses <bf>
uses <crt> proc main(): println("Start") try: println("Trying to do something...") a ?= 10 / 0 catch: println(getError()) finally: println("Finally") end println("End") inputln()
end

Планы на будущее

Все присматриваюсь да присматриваюсь к GraalVM & Truffle. У моей среды выполнения отсутствует JIT компилятор, так что в плане производительности он пока что может составлять конкуренцию разве что питону. Надеюсь, что мне окажется под силу реализовать JIT компиляцию на базе GraalVM или LLVM.

Репозиторий

Вы можете поиграться с наработками и проследить за проектом сами.

Сайт
Репозиторий на GitHub

Спасибо, что дочитали до конца, если вы это сделали.


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] Haiku β1 — сделаем /b/ OS великой снова

Совсем недавно (почти 4 месяца назад) вышла новая Haiku (далее — просто BeOS, ибо проект гораздо удачнее ReactOS — настолько, что разница между Haiku и BeOS уже пренебрежимо мала). Да и недавно прочитанный киберпанк-роман Александра Чубарьяна давал понять, что BeOS ...

Минкомсвязи одобрило законопроект об изоляции рунета

Министерство цифрового развития, связи и массовых коммуникаций РФ поддержало законопроект №608767-7 об автономной работе рунета, внесённый в Госдуму 14 декабря 2018 года. Об этом сегодня сообщил замглавы Минкомсвязи Олег Иванов в ходе расширенного заседания комитета Госдумы по информационной политике, информационным технологиям ...