Вступление
Это не гайд, не туториал и не исследование. Эта статья - просто история о том, как я решил создать свой язык для низкоуровневых штучек, отличного от ассемблера и С. Приятного прочтения!
Предыстория
Как вы знаете, я обожал (и до сих пор обожаю) язык ассемблера, а именно - fasm. Я уже давно его изучил и пользуюсь им, но постепенно стал давать слабину. Я устал писать код на ассемблере, так как это долго и довольно таки тяжело (рано, или поздно, это всё равно произошло бы). Понять мою любовь к этому языку можно прочитав мои предыдущие статьи: раз, два, три, четыре. Почему же я не стал использовать С? Он мне просто не нравится. Неудобно мне. Вот и всё.
Концепт
Концепт заключается в том, что человек указывает все данные для компиляции в главном файле с помощью директивы, например, format
. Так же, есть два типа библиотек: встроенные (стандартные) и кастомные (пользовательские). Иначе говоря, либы и модули. Для использования shared object, dll и пр. будет отдельная директива. Указателей в языке не будет, а для разыменывания будет использоваться макрос OFFSET
. Для работы с железом на все случаи жизни будут стдлибы. Синтаксис будет чем-то средним между Java, TS, fasm (для некоторых выражений) и Python.
Синтаксис
Это, пожалуй, первое, с чего я начал. Для начала, я написал пример кода, который устроит меня своим синтаксисом. Вышло как-то так:
format mbr.x86.16.executable;
use Bios;
use Console;
fn main() -> Void
{
Bios.set_mode(3);
Console.log("Hello, World!");
return 0;
}
Я решил не сильно париться, поэтому использовал библиотеку parglare. Она очень легкая и удобная, всем рекомендую. Для описания синтаксиса парсер принимает строку в соответствующем формате, использует регулярные выражения (не надо осуждать регулярки, они всесильны!). Итак, вот такое описание синтаксиса у меня получилось:
sroot: sany*;
sany: sformat
| suse
| sfn
| sdefine;
sformat: "format" tname ";";
suse: "using" tname ";";
sfn: "fn" tname "(" sargs? ")" "->" tname sbody;
sargs: sarg ("," sarg)*;
sarg: tname ":" tname;
sbody: "{" sline* "}";
sline: (scall | sequal | sdefine | sreturn) ";";
scall: tname "(" scallargs? ")";
scallargs: sexpr ("," sexpr)*;
sequal: tname "=" sexpr ";";
sdefine: sarg ("=" sexpr)* ";";
sreturn: "return" sexpr?;
sexpr:
"(" sexpr ")"
|(ssequence | tname) "[" tinteger "]"
|sexpr "*" "*" sexpr
|"-" sexpr
|sexpr "*" sexpr
|sexpr "/" sexpr
|sexpr "+" sexpr
|sexpr "-" sexpr
|"!" sexpr
|sexpr "&" sexpr
|sexpr "|" sexpr
|sexpr "^" sexpr
|tinteger
|tstring
|tname
|ssequence;
ssequence:
"<" scallargs ">";
terminals
tstring: /"[^"]*"/;
tname: /([A-Za-z_]\w*\.)*[A-Za-z_]\w*/;
tinteger: /00?|[1-9]\d*/;
Вышло кратко. А краткость - сестра таланта. Да и к тому же, так будет проще.
Компиляция в язык ассемблера (fasm)
Я долго ломал голову, как это полаконичней сделать, и никак не мог придумать решение. Через пару дней я наткнулся на статью про pyast64 (на медиум, по-моему). Она раскрыла мне глаза. В коде этого скрипта было все что мне нужно: и работа со стеком, и соглашение о вызовах, и пр и тп. Я очень шустро накидал константы:
SIMPLE_BINOPS = (
"add",
"sub",
"or",
"and",
"xor",
"mul",
"div"
)
SIMPLE_UNOPS = (
"neg",
"not"
)
SIMPLE_TYPES = (
"integer",
"string",
"label"
)
COMPLEX_BINOPS = (
"pow",
"ind"
)
MUTABLE = (
"integer"
)
ASM_BINOP = """
pop qword rdx
pop qword rax
%s qword rax, rdx
push qword rax"""
ASM_DEF = """
%s d_%s %s"""
ASM_EQU = """
pop qword rdx
mov [%s], rdx"""
ASM_LDR = """
push qword [%s]"""
ASM_LEA = """
push qword %s"""
ASM_PTR = """
push qword %s"""
ASM_RET = "\n ret"
ASM_SEQ = """
push %s
"""
Дописать транслятор в ассемблер я ещё не успел, но до этого не долго осталось. Весь исходный код опубликован здесь: группа Telegram, здесь же можно обсудить и/или поддержать проект. Спасибо за внимание!