image

Предисловие


Данный язык был разработан мной в учебных целях. Я не считаю его (на данный момент) идеально проработанным языком, но возможно в будущем он сможет потягаться с конкурентами.

Если у вас есть желание попробовать его в действии самому — скачивайте репозиторий проекта, в нем вы сможете найти собранную версию проекта или же собрать её самостоятельно, для своей ОС.

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

Переменные и неявные указатели


В Mash каждая переменная хранит указатель на объект в памяти. При передаче переменных в методы, получении результата или же при простых манипуляциях с ними — работа идет с объектами в памяти по указателям на них.

Т.е. в следующем коде в метод p(arr) будет передан массив в виде указателя на уже созданный ранее объект.

proc p(arr):
  ...
end

proc main():
  arr ?= [1, 2, 3, 4, 5]
  p(arr)
end

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

Временные переменные и сборщик мусора


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

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

Сборка мусора вызывается с помощью gc() и освобождает память из под всех временных объектов.

Многие простые действия с переменными сопровождаются созданием временных объектов в памяти.

Разработчик может явно объявлять выделение памяти, для дальнейшей работы с ней, т.е. объявлять переменные для длительного использования.

Примеры создания временной переменной:

x ?= 10

И переменных, не помеченных для сборщика мусора:

x ?= new(10)
var x2 = 20

Пример использования gc():

while a > b:
  doSomething(a, b)
  gc()
end

Память также можно освобождать вручную:

var a = 10, b = 20, c = 30
...
Free(a, b, c)

Зоны видимости переменных


Переменные в Mash могут быть объявлены как локально, так и глобально.

Глобальные переменные объявляются между методов, через оператор var. Локальные — внутри методов любым способом.

Типы данных


В Mash динамическая типизация. Поддерживается автоматическое определение/переопределение типов данных для чисел без знака и со знаком, дробных чисел и строк. Помимо этого из простых типов данных поддерживаются массивы (в т.ч. многоуровневые), enum типы (почти тоже самое, что и массивы), методы, присутствует логический тип данных и пожалуй все.

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

x ?= 10
x += 3.14
x *= "3"
x /= "2,45"

x ?= [1, 2, 3.33, func1(1, 2, 3), "String", ["Enum type", 1, 2, 3], "3,14"]

Интроспекция позволяет определять тип нужной переменной:

if typeof(x) == TypeInt:
  ...
end

Массивы & перечисления


Редкие задачи не заставляют разработчика объявлять очередной массив в коде.
Mash поддерживает массивы, состоящие из произвольного количества уровней, + массивы могут быть чем-то вроде деревьев, т.е. размеры подуровней могут различаться на одном подуровне.

Примеры объявлений массивов и перечислений:

a ?= new[10][20]
var b = new[10][20]
c ?= [1, [], 3, 3.14, "Test", [1, 2, 3, 4, 5], 7.7, a, b, ["77", func1()]]
var d = [1, 2, 3]

Любые объекты, объявляемые через оператор new не помечаются для сборщика мусора.

Массивы также как и обычные объекты в памяти освобождаются вызовом Free()
Такая работа с перечислениями довольно удобна.

Например, можно передавать в методы или возвращать из них множество значений одной переменной:
func doSomething(a, b, c):
  return [a+c, b+c] 
end

Операторы присваивания


В Mash целых 3 оператора присваивания.

  • ?=
    Присваивает переменной указатель на какой-либо объект (если переменная объявлена но не хранит указатель на объект в памяти, то нужно использовать именно этот оператор, для присваивания ей значения).
  • =
    Обычное присваивание. Присваивает объекту по указателю в переменной значение другого объекта.
  • @=
    Присваивает значение объекту по явному указателю на этот объект (об этом будет сказано позже).

Математические и логические операции


Список поддерживаемых на данный момент математических и логических операций:

  • +, -, *, /
    В комментариях не нуждаются.
  • \
    Деление нацело.
  • %
    Остаток от деления.
  • &
    Логическое «и».
  • |
    Логическое «или».
  • ^
    Логическое «исключающее или».
  • ~
    Логическое «не».
  • ++, --
    Инкремент и декремент.
  • <<, >>
    Побитовые сдвиги влево и вправо.
  • ==, <>, >=, <=
    Логические операторы сравнения.
  • in
    Проверяет принадлежность объекта к перечислению или к массиву.
    Пример:
    if Flag in [1, 3, 5, 8]: ...

Явные указатели


Казалось бы, зачем они нужны, если есть не явные указатели? Например для проверки, хранят ли переменные A и B указатели на один и тот же объект в памяти.

  • @ — получить указатель на объект и положить его в переменную, как объект.
  • ? — получить объект по указателю из объекта в переменной.
    Пример:
    a ?= ?b

Процедуры и функции


Я решил сделать в языке разделение методов по возврату значений на процедуры и функции (как в Pascal).

Объявления методов производятся подобно следующим примерам:

proc SayHello(arg1, arg2, argN):
  println("Hello, ", arg1, arg2, argN)
end

func SummIt(a, b):
  return a + b
end

Языковые конструкции


Пример конструкции if..else..end.

if <условие>:
...
else:
...
end

Цикл for.

for([инициализация]; <условие>; [операции с переменными]):
...
end

While. Условие проверяется перед итерацией.

while <условие>:
...
end

Until. Цикл «пока не» с проверкой условия после итерации. Весьма странная конструкция, делал её по образу и подобию repeat..until из Pascal'я.

until <условие>:
...
end

switch..case..end..else..end… — всем знакомая конструкция для создания логических ветвлений.

switch <переменная>:
  case <значение 1>:
    ...
    break  
  end

  case <значение 2>:
    ...
    break
  end

  else:
   ...
end

Классы и элементы ООП языка


В Mash реализована поддержка классов, наследования, динамической интроспекции и рефлексии, полиморфизм. Т.е. стандартный набор скриптового языка поддерживается.

Рассмотрим объявление простого класса:

class MyClass:
  var a, b
  proc Create, Free
  func SomeFunction
end

Объявление класса не содержит реализаций методов, которые в нем объявлены.
В качестве конструктора класса выступает метод Create. В качестве деструктора — Free.

После объявления класса можно описать реализацию его методов:

proc MyClass::Create(a, b):
  $a ?= new(a)
  $b ?= new(b)
end

proc MyClass::Free():
  Free($a, $b, $)
end

func MyClass::SomeFunction(x):
  return ($a + $b) / x
end

Вы могли заметить символ $ в некоторых местах кода — этим символом я просто сократил длинное this->. Т.е. код:

return ($a + $b) / x
...
Free($a, $b, $)

Эквивалентен этому коду:
return (this->a + this->b) / x
...
Free(this->a, this->b, this)

В this находится указатель на экземпляр класса, от лица которого вызывается метод этого класса.

Для того, чтобы унаследовать функционал какого-либо класса, нужно описать объявление нового класса таким вот образом:

class MySecondClass(MyClass):
  func SomeFunction
end

func MySecondClass::SomeFunction(x):
  return ($a - $b) / x
end

MySecondClass — будет иметь конструктор и деструктор от своего предка + функция SomeFunction, которая имеется у класса-предка перезаписывается функцией из нового класса.

Для создания экземпляров классов существует оператор new.

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

a ?= new MyClass  //выделение памяти под структуру класса

b ?= new MyClass(10, 20)  //выделение памяти под структуру класса и последующий вызов конструктора

Тип экземпляра класса можно определить при создании этого экземпляра, соответственно в языке отсутствует приведение типов.

Интроспекция позволяет определить тип экземпляра класса, пример кода:

x ?= new MyClass(10, 20)
...
if x->type == MyClass:
  //делаем что-нибудь...
end

Иногда нужно обратиться к классовой функции и перезаписать её новой. Пример кода:

func class::NewSomeFunction(x):
  return $x * $y * x
end
...
x ?= new MyClass(10, 20)
x->SomeFunction ?= class::NewSomeFunction
x->SomeFunction(33) //Вызывается NewSomeFunction, будто это оригинальная функция класса.

Заключение


В данной статье я постарался познакомить возможных заинтересованных людей с моим творением.

Спасибо за прочтение. Жду комментариев.

Комментарии (21)


  1. nikbond
    01.03.2019 18:51
    +4

    В чем смысл нового языка, который не вносит никаких новых концепций и абстракций? А уж тем более тащить что-то вроде сишных костылей с switch-case-break 50летней давности?

    Если статья о том, как писать велосипедный ЯП в учебных целях, то нужно больше информации именно про внутреннее устройство языка и компилятора, это было бы интересней.


    1. RoPi0n Автор
      01.03.2019 20:09

      Про внутреннее устройство языка я написал до этого целый ряд статей :\


      1. dolovar
        02.03.2019 14:20

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


    1. Griboks
      01.03.2019 20:22
      +3

      В чём смысл новой машины, если старая шестёрка ещё катается? Наверное, новый язык решает фатальный недостаток других языков. Скорее всего, суп удобнее кушать ложкой, а автору писать
      class MyClass:
      var a, b
      proc Create, Free
      func SomeFunction
      end

      вместо
      interface MyClass{int a,b {get;set;}void Create();void Free();int SomeFunction();}.


      1. nikbond
        02.03.2019 19:46

        Но это не про покупку новой машины, это про смену синей шестерки на красную — по факту выходит одно и то же.


  1. yizraor
    01.03.2019 20:30
    +1

    Забавненько!


    Циклы "while" и "until" отличаются только способом обработки условия. И, по всей видимости, оба являются циклом с предусловием (хотя в упомянутом Паскале — цикл "repeat...until" — это цикл с постусловием).
    Цикл "for" явно Сишного вида, и значит — тоже цикл с предусловием...


    А где же в "новейшем" языке цикл с постусловием?


    1. RoPi0n Автор
      01.03.2019 20:44

      until… end, условие проверяется после итерации


      1. yizraor
        01.03.2019 20:47
        +3

        Т.е., условие мы пишем в начале цикла, а по факту проверяется оно в конце?


        ИМХО: это будет путать программиста, привыкшего ставить условие там (в начале или конце), где оно должно проверяться.


        Раз уж Вы решили дифференцировать циклы по истинности/ложности условия (while/until), то стоит добавить ещё 2 разновидности:
        1) loop...while ( условие );
        2) loop...until ( условие );
        %-)


        P.S.:
        Замена "this->" на знак $ мне понравилась — приятный сахарок :)
        Но выглядит лишним слово "end" после каждого "case" в операторе "switch"...


        1. RussDragon
          02.03.2019 06:19

          Ой, мы ему это еще в статьях об устройстве языка говорили :)
          Хозяин – барин.


          1. RoPi0n Автор
            02.03.2019 12:34

            Да все будет, как руки дойдут… :)


          1. RoPi0n Автор
            02.03.2019 19:36
            -1

            Чтож, спасибо за минус тебе, неизвестный хабро-user, под моим можно сказать обещанием что-то допилить в моём хобби проекте.

            Теперь об изменениях:
            — Переделал until цикл, теперь вместо него цикл whilst — можно сказать, что это копия while, только с пост-условием.

            Изменения залил на github.


  1. voidptr0
    01.03.2019 21:33

    Эра DSL языков на основе llvm объявляется открытой. Это прекрасно, но я думаю излишне говорить каждый раз об «открытии Америки».


  1. Inobelar
    01.03.2019 21:53

    Как автор ЯП, который посвятил этой теме много времени, можете прокомментировать, почему выбрали концепцию-со-сборщиком-мусора?


    Меня (как системного программиста на С++ и С, внимательно наблюдающего за Rust), удивляет это направление )) Почему (особенно, учитывая эту великолепную статю), всё меньше людей задаются вопросами, как создать лучшуюю-RAII, или ещё-лучшую модель типов, или ещё-лучшие-zero-cost-abstractions, или ещё-лучший-хаскелл? ))


    1. RoPi0n Автор
      01.03.2019 22:16

      В отличии от других ЯП, таких как Python, Perl, JS, Ruby и т.д., сборщик мусора у моей ВМ реализован через метки, т.е. переменная помечается, как мусор сразу при объявлении. Сборщик мусора имеет на момент вызова готовый массив указателей на мусор в памяти.


      Основная идея заключалась в том, что даже в нетипизированном языке сборка мусора должна лежать на плечах программиста и быть полностью ему подконтрольна. Используя gc() программист на 150% уверен где, когда и сколько памяти будет освобождено сборщиком мусора.
      Также изначально затеивал такую архитектуру, как небольшую оптимизацию. Ведь сборщику мусора вообще не нужно напрягаться, чтобы исправно выполнять свою работу.


      1. Zibx
        02.03.2019 05:37

        Для меня работа этого GC осталась загадкой. Мне интересно узнать взорвётся ли оно и если взорвётся то как именно если написать вот так:
        for(x?=0; x<10; x++):
        gc()
        end


        Или вот вызвали мы функцию, а в ней позвали gc — соберёт ли он весь мусор в проекте или только мусор внутри функции? Если первое — gc будет запрещено звать в любой библиотеке. Если второе, то можно и автоматически его звать при каждом выходе из скоупа.


        1. RoPi0n Автор
          02.03.2019 12:39

          В данном примере взорвется, т.к. в x?=0 — в х запихивается временная переменная.
          Если написать var x = 0, либо x ?= new(0), то нет. Но затем для х нужно будет вызвать free().


          gc() собирает весь мусор, который в т.ч. лежит в стеке.


      1. Bhudh
        03.03.2019 18:58
        +1

        А на этом языке можно будет написать AI, который бы проверял и исправлял расстановку «в отличии от» и «в отличие от», «также» и «так же», а заодно и запятых?
        Вот тогда он был бы реально полезен автору.


  1. rumkin
    01.03.2019 21:58
    +2

    1. ИМХО. Я бы поменял местами $ и @. Например в Ruby и Crystal используются именно @ как замена конструкции this., а '$' чаще встречается именно в именах переменных.
    2. Вызвав Free, я удаляю значения из памяти даже если они используются в другом месте?
    3. Почему gc() пишется со строчной буквой, а Free() с прописной?
    4. Соглашусь с автором выше, ничего принципиально нового в языке не вижу. Как учебный язык ок, но, на мой взгляд, стоит добавить какую-нибудь изюминку.


    1. RoPi0n Автор
      01.03.2019 22:23
      -2

      1. Лично мне больше нравится $ на месте this-> :)
      2. Ну да. На то это и Free, чтобы память освобождать.
      3. Можно писать с каких угодно букв. Язык не придирчив к этому.
      4. Как по мне, некоторые вещи, которые я реализовал являются весьма необычными, например поддержка многопоточности и всего необходимого для неё функционала прямо из коробки и без использования костылей… Но об этом я наглядно расскажу чуть позже, как найду время для написания очередной статьи.
        Нельзя же рассматривать плюшки языка, не описав перед этим синтаксис языка и базовые вещи. Так что эту статью можно считать вводной… Наверное...


      1. rumkin
        02.03.2019 22:26

        2) С тем, что в последнее время получил развитие отказ от ручного управления памятью, выглядит очень опасно. Нужно как-то ограничить. Например разрешать вызов free только из контекста функции main, чтобы управление всегда происходило явно. Возможно, через контроллер, определяемый программистом на уровне приложения, а не библиотеки.


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


  1. impwx
    02.03.2019 22:47

    Горшочек, не вари!