Главная / Сериалы / Новый язык программирования Mash

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

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

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

Заранее скажу, что писал весь проект на 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.

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

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

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

    Источник

    Смотрите также

    Прекрасная вампирша / Прекрасный вампир (2018)

    Добавить комментарий

    Ваш e-mail не будет опубликован. Обязательные поля помечены *