Дисклеймер?
Эта серия статей является гайдом, который пишется параллельно с проектом 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'] ]
Думаю, на первую часть этого хватит. Потом будет написание парсера в связке с лексером. в третей части планирую описать транспилятор.
Ссылки:
Теория:
Проект:
-
Гитхаб немного устарел. назначение и синтаксис поменялись. поэтому пока что не советую туда заглядывать. разве что звездануть.
Жажду идей, предложений и конструктивной критики.
Спасибо за прочтение!
ссылка на оригинал статьи https://habr.com/ru/post/677378/
Добавить комментарий