Новый язык программирования Mash

в 8:30, , рубрики: Mash lang, виртуализация, Компиляторы, ооп, Совершенный код, транслятор, язык программирования

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

В этой статье я хочу осветить основные этапы своей работы и для начала описать созданный концепт языка и его первую реализацию над которой сейчас работаю.

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

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

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

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

Очевидно, во главе ВМ лежит стек. В моей реализации он работает блоками. По сути это простой массив указателей и переменная для хранения индекса вершины стека.
При его инициализации, создается массив на 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

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

Автор: RoPi0n

Источник


* - обязательные к заполнению поля