Новая мова праграмавання 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

Дадаць каментар