На протяжении нескольких лет я пробовал свои силы в разработке своего языка программирования. Мне хотелось создать на мой взгляд максимально простой, функциональный и удобный язык.
В этой статье я хочу осветить основные этапы своей работы и для начала описать созданный концепт языка и его первую реализацию над которой сейчас работаю.
Заранее скажу, что писал весь проект на Free Pascal, т.к. проги на нем можно собрать под огромное число платформ, да и сам компилятор выдает очень даже оптимизированные бинарники (собираю все составляющие проекта с O2 флагом).
Среда выполнения языка
Первым делом стоит рассказать о виртуальной машине, которую мне пришлось писать для выполнения будущих приложений на моём языке. Решил я реализовывать стековую архитектуру, пожалуй, потому что так было проще всего. Ни одной нормальной статьи как мне это сделать на русском я не нашел, так что после ознакомления с англоязычным материалом я засел за проектирование и написание своего велосипеда. Далее буду приводить свои «передовые» идеи и разработки в этом деле.
Реализация стека
Очевидно, во главе ВМ лежит стек. В моей реализации он работает блоками. По сути это простой массив указателей и переменная для хранения индекса вершины стека.
При его инициализации, создается массив на 256 элементов. Если в стек закидывается большее число указателей, то его размер увеличивается на следующие 256 элементов. Соответственно, при удалении элементов из стека — его размер регулируется.
В ВМ используется несколько стеков:
- Основной стек.
- Стек для хранения точек возврата.
- Стек сборщика мусора.
- Стек обработчика 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.
Репозиторий
Вы можете поиграться с наработками и проследить за проектом сами.
Спасибо, что дочитали до конца, если вы это сделали.
Автор: RoPi0n