Дисклеймер?
Эта серия статей является гайдом, который пишется параллельно с проектом pHoney. Этот гайд не несет какой-либо цели, просто чтобы новички впитывали мой опыт, а олды закутались в одеяло и приготовили себе чай или какао. Спасибо
Введение
Я считаю, что создать свой ЯП должен каждый программист, не важно какой сферы деятельности (ну у frontend'а не знаю как с этим дела обстоят). И поэтому я поделюсь здесь своим опытом.
Немного теории
И так, сначала просто скажу, что наш язык будет компилируемым. Но что это значит? Это значит, что прежде чем быть запущенной, программа на таком языке должна быть превращена в машинный код (приложение, если под ОС). Например, mingw по умолчанию превратит сишный файл в exe, а gcc - в elf, bin и прочую лабуду, там свобода в настройках гораздо больше. Так же, мы сделаем бутстрапинг компилятора - то есть, перепишем его на самом себе. Это очень частая (fasm, gcc), и очень практичная тактика. И ещё немного терминов: лексер - компонент языка, отвечающий за разделение ключевых слов и символов друг от друга. такие разделенные кусочки кода - токены - потом используются парсером - компонентом языка, который строит из линейного списка АСД - абстрактное синтаксическое дерево. АСД в свою очередь использует транспилятор(олдскуллы не бейте), переводя его в код на другом языке(чаще всего С или асм), или же в машинные коды напрямую. Думаю, теперь можно по-настоящему начинать.
Каркас программы
Не думаю, что это кому-то интересно разжевывать и/или читать, так что просто дам листинг.
листинг 0 - каркас
# импорты
import os
from sys import path
# это нужно из-за особенностей ФС проекта
path+=['.\\src', '.\\..']
# печатаем на экран "эмблемку". MS win можете поменять на свое
print(f"""::: ::: :::: :::
:+: :+: :+:+: :+:
+:+ +:+ :+:+:+ +:+
+#++:++#++ +#+ +:+ +#+
+#+ +#+ +#+ +#+#+#
#+# #+# #+# #+#+#
### ### ### ####
pHoney ver 1.9.8a for Microsoft Windows\n""")
# удаляем это потому, что этот код могут запустить
# из визуалки или на ПК, где файлы *.ру не сопоставлены с питоном
if "python" in sys.argv:sys.argv.remove("python")
# вычисляем путь к файлу вывода
outpath = os.path.splitext(os.path.normpath(os.path.abspath(parse_path_args(sys.argv)[0])))[0]+".exe"
# меняем этот путь, если юзер скажет
for i in range(len(sys.argv)):
if sys.argv[i] == '-o':
outpath = sys.argv[i + 1]
break
pass
# здесь проверяем, можем ли мы получить к входным данным доступ
if len(sys.argv) < 2:
print('Error: file expected')
raise SystemExit
try: # вот-так. криво, но я делал на скорую руку.
with open(os.path.normpath(os.path.abspath(parse_path_args(sys.argv)[0])), 'rt', encoding="utf-8") as _:pass
pass
except FileNotFoundError:
print(prf + 'Error: file not found' + psf)
raise SystemExit
except PermissionError:
print(prf + 'Error: permission denied' + psf)
raise SystemExit
except KeyboardInterrupt:raise SystemExit
except SystemExit as E:raise SystemExit
except BaseException as E:
print(f"Error: internal error. additional info: {E}")
raise SystemExit
# открываем собственно файл, поданный на вход
with open(os.path.normpath(os.path.abspath(parse_path_args(sys.argv)[0])), 'rt', encoding="utf-8") as f:lines = f.read()
print(len(lines), 'bytes source, ', end='')
print(len(lines.splitlines()), 'lines.\n')
# функция, которая пригодится, но позже.
def fold(data: str):
return data.replace('\\t', ' ').\
replace('\\b', ' <backspace> ').\
replace('\\r', ' <carriage-return> ').\
replace(' ', ' ').\
replace('\\n', ' <new-line> ')
Лексер
Сейчас напишем лексер. Использовать будем регулярные выражения,это уже почти традиция для DIY-языков. Нашему лексеру понадобится список regex'ов и сопоставненных типов токенов.
Лексер будет брать строку, находить первый токен, сохранять его, сдвигать курсор вправо на кол-во символов, соответствующее размеру токена, и так в цикле пока не встретится проблемное место и/или конец строки.
листинг 1 - лексер
# импорты
import re
import sys
# класс для токенов
class LexToken(object):
# нужны: тип, значение и позиция
def __init__(self, typ, val, pos):
self.typ = typ
self.val = val
self.ps = pos
return
def __str__(self):
return\
'{\n\ttype: '+self.typ+',\n\tvalue: \"'+\
self.val+'\",\n\tpos: '+str(self.ps)+'\n}'
def __repr__(self):
return str('LexToken{'+self.type()+\
':\"'+self.value()+'\":'+\
str(self.ps)+'}').replace('\n', '\\n')
def __getitem__(self, i):
return [self.type, self.val][i]
def __setitem__(self, i, value):
if i == 0:
self.typ = value
pass
elif i == 1:
self.val = value
pass
else:
raise IndexError('Sequence index out of range: ' + str(i))
# возвращает тип
def type(self):
return self.typ
# возвращает значение
def value(self):
return self.val
# возвращает позицию
def pos(self):
return self.ps
pass
# ошибка для лексера, хранит всю важную инфу
class LexError(BaseException):
def __init__(self, token, pos, typ='unexpected'):
self.token = token
self.pos = pos
self.type = typ
pass
pass
# собственно, лексер
class Lex(object):
# опять-таки, ему нужны правила
def __init__(s, rules):
s.pos = None
s.buf = None
idx = 1
regex_parts = []
s.group_type = {}
for typ, regex in rules:
groupname = 'TOKEN%s' % idx
regex_parts.append('(?P<%s>%s)' % (groupname, regex))
s.group_type[groupname] = typ
idx += 1
s.regex = re.compile('|'.join(regex_parts))
# а ещё нужна строка, которую надо разбирать
def input(s, buf):
s.buf = buf
s.pos = 0
# генерирует 1 отдельный токен
def token(s):
if s.pos >= len(s.buf):
return None
else:
mtch = s.regex.match(s.buf, s.pos)
if mtch:
groupname = mtch.lastgroup
tok_type = s.group_type[groupname]
tok = LexToken(tok_type, mtch.group(groupname), s.pos)
s.pos = mtch.end()
return tok
raise LexError(LexToken('<unexpected>',\
s.buf[s.pos], s.pos), s.pos)
# генератор последовательности токенов
def tokens(s):
while 1:
tok = s.token()
if tok is None: break
yield tok
return -1
pass
да, этот листинг очень похож на один известный и обсосанный со всех сторон, но я написал его до того, как начать гуглить.
А вот для него правила(ws я решил не опускать, на всякий случай):
RULES = [
['str', r'(\'[.^\']*\'|\"[.^\"]*\")'],
['comment', r'//.*|/\*.*/'],
['name', r'[A-Za-zА-Яа-яЁё_]\w*'],
['int', r'[+-]?(0[Xx][A-Za-z0-9][A-Za-z0-9_]*|\d+|1[01]*[Bb]|0[Bb])'],
['float', r'[+-]?\d*\.\d+'],
['plus', r'\+'],
['minus', r'\-'],
['star', r'\*'],
['slash', r'/'],
['back', r'\\'],
['colon', r'\:'],
['semi', r'\;'],
['dot', r'\.'],
['comma', r'\,'],
['amper', r'\&'],
['sharp', r'\#'],
['expl', r'\!'],
['dog', r'\@'],
['bux', r'\$'],
['percent', r'\%'],
['flex', r'\^'],
['lpar', r'\('],
['rpar', r'\)'],
['lblc', r'\['],
['rblc', r'\]'],
['lfig', r'\{'],
['rfig', r'\}'],
['lang', r'\<'],
['rang', r'\>'],
['equ', r'='],
['apos', r'\`'],
['tilde', r'\~'],
['ignore', ' |\r|\f|\t'],
['newline', '\n']
]
Думаю, на первую часть этого хватит. Потом будет написание парсера в связке с лексером. в третей части планирую описать транспилятор.
Ссылки:
Теория:
Проект:
Гитхаб немного устарел. назначение и синтаксис поменялись. поэтому пока что не советую туда заглядывать. разве что звездануть.
Жажду идей, предложений и конструктивной критики.
Спасибо за прочтение!
Комментарии (14)
GeniyZ
17.07.2022 18:17+2Я вообще ни чего не понял (
TalismanChet Автор
17.07.2022 22:07-2что же вы не поняли? я объясню вам в комментариях, а также доработаю статью.
VaalKIA
17.07.2022 19:20+12Какой-то бред! Любой, кто пытался написать свой язык, тут же погружается в невозможность использования регулярок, в виду их недостаточной выразительности, потом следует исследование «а чего им не хватает», потом контекстная зависимость размывает понятие лексер и такие тривиальные вещи, как вложенные комментарии, становятся не такими тривиальными, зато вы приходите к осознанию, того, что сообщить об ошибке компиляции и показать, что-то вразумительное — это не тривиальная задача. В итоге, ваш пост выглядит как инструкция для лохов.
TalismanChet Автор
17.07.2022 22:02-1это только первая часть! это - то, с чего я начал. сейчас, само собой, я действую иначе, но об этом в следующих частях.
ktod
17.07.2022 21:20-5Зачем вы в статье используете жаргонизмы "лексер" и "парсер"? Они скрывают суть вещей. Почему бы не использовать классические академические термины "лексический анализатор" и "синтаксический анализатор"? Вам так жаль лишние буквы потратить?
TalismanChet Автор
17.07.2022 22:03это не жаргонизмы, а оригинальные названия. и суть вещей они не скрывают. я раскрыл в статье всю терминологию и оставил ссылки на вики.
GeniyZ
18.07.2022 07:08+1Вот: https://habr.com/ru/post/119850/
Хороший пример серии статей о создании ЯП.Тут у вас не мало текста, но так и не понятно, какой у вашего языка синтаксис, на чем вы его пишете, какие у языка возможности планируются в целом. Это самые основы, для понимания вашей темы. А вы это бережно скрываете.
И да, написано небрежно, читаешь — и складывается ощущение, что автор тебе дерзит. Но это дело опыта, это наживное.
А может и действительно автор — щегол)
alexshipin
18.07.2022 08:20+1Эта серия статей является гайдом, который пишется параллельно с проектом pHoney. Этот гайд не несет какой-либо цели, просто чтобы новички впитывали мой опыт, а олды закутались в одеяло и приготовили себе чай или какао. Спасибо
После прочтения слов "Я считаю, что создать свой ЯП должен каждый программист" и статьи далее, новички ничего не поймут.
Извините, но какой опыт они должны впитать, если этого опыта тут и нет, а есть какие-то обрывочные высказывания с примерами хорошего ничего?
Первое общее впечатление - это авторская заметка, но никак не "гайд".
К тому же, вам уже не один раз указывали на то, что у вас ничего не раскрыто: ни цели, ни языка, ни каких-либо развернутых теорий и их решений. Подойдите к вопросу ответственно.
Я считаю, что создать свой ЯП должен каждый программист,
Можете спокойно считать так и дальше, только данное лучше держать при себе, а не накидывать на "вентилятор".
Итоговое общее заключение: Где фабула? Где сюжет? Где хоть что-нибудь?..
PS: Полностью сами себе противоречите в тех же комментариях, опираясь на статью, написанную вами же (или нет?).
webhamster
18.07.2022 10:33Подытожим. Автор не составил план, откладывая определение формы всего повествования на потом. И к тому же не показал ни синтаксис создаваемого языка, ни его парадигму, не обосновал необходимость его создания. Зато сразу показал лексический анализатор к своему неизвестному языку, что сразу вызывает вопрос: где последовательность повествования?
playermet
18.07.2022 12:56В статье о создании языка хотелось бы видеть более развернутое описание "зачем" и "почему". Например "язык предполагается для X", "поэтому в нем должны быть фичи Y", "у них будет синтаксис Z потому что A", "такой синтаксис удобно парсить с помощью B потому что C, и у него будут плюсы D и минусы F", и т.д.. Чтобы человек который сам изучает тему и наткнется на вашу статью смог понять мотивацию выбора, и на основе этого сделать собственные заметки.
agratoth
Так о чем, собственно, статья? "Смотри, как я умею"? В чем образовательная идея? Как написать лексер неизвестного языка? Язык, как и любая технология, создаётся не просто так, а с какой то целью. Пусть если цель даже и эзотерическая
TalismanChet Автор
ну, во-первых это первая часть, здесь я показываю как Я это делал, с чего начал. А насчет цели - для меня цель этого проекта(на данный момент) - развлечение.
agratoth
Нет, вы не показываете, как сделали. Вы просто говорите: "Я сделяль" и дальше листинги какого-то кода. Вы бы хоть вводную часть дали, зачем вы решили свой язык запилить. Какие задачи он должен решить (изучить создание компилятор - тоже задача, но её надо явно указать), почему он сделан так, а не иначе. Каким вы видели дизайн языка на стадии идеи, и как идея потом эволюционировала.
PS. Пассаж про "каждый должен" - в этом месте было обидно. Я вот не писал своего языка. Все, я недостаточно профессиональный разработчик?