Нова мова програмування Mash

Протягом кількох років я пробував свої сили у розробці своєї мови програмування. Мені хотілося створити на мій погляд максимально просту, повнофункціональну та зручну мову.

У цій статті я хочу висвітлити основні етапи своєї роботи і для початку описати створений концепт мови та її першу реалізацію над якою зараз працюю.

Заздалегідь скажу, що писав проект на 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

Основна концепція мови

Основною ідеєю було розробити максимально функціональну та просту мову. Вважаю, що зі своїм завданням розробка справляється на ура.

Блоки коду, процедури та функції

Всі конструкції в мові відкриваються двокрапкою : та закриваються оператором кінець.

Процедури та функції оголошуються як proc та func відповідно. У дужках перераховуються аргументи. Усі як у більшості інших мов.

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

Приклад коду:

...

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

Дякую, що дочитали до кінця, якщо ви це зробили.

Джерело: habr.com

Додати коментар або відгук