Дисклеймер?

Эта серия статей является гайдом, который пишется параллельно с проектом 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']
]

Думаю, на первую часть этого хватит. Потом будет написание парсера в связке с лексером. в третей части планирую описать транспилятор.

Ссылки:

Теория:

  1. Компилируемые языки это

  2. Машинный код

  3. Бутстраппинг компилятора

  4. Лексер

  5. Tокен

  6. Парсер

  7. АСД(AST)

  8. Транспилятор(транслятор)

Проект:

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

Жажду идей, предложений и конструктивной критики.

Спасибо за прочтение!

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


  1. agratoth
    17.07.2022 17:37
    +11

    Так о чем, собственно, статья? "Смотри, как я умею"? В чем образовательная идея? Как написать лексер неизвестного языка? Язык, как и любая технология, создаётся не просто так, а с какой то целью. Пусть если цель даже и эзотерическая


    1. TalismanChet Автор
      17.07.2022 22:05
      -4

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


      1. agratoth
        17.07.2022 23:28
        +6

        Нет, вы не показываете, как сделали. Вы просто говорите: "Я сделяль" и дальше листинги какого-то кода. Вы бы хоть вводную часть дали, зачем вы решили свой язык запилить. Какие задачи он должен решить (изучить создание компилятор - тоже задача, но её надо явно указать), почему он сделан так, а не иначе. Каким вы видели дизайн языка на стадии идеи, и как идея потом эволюционировала.

        PS. Пассаж про "каждый должен" - в этом месте было обидно. Я вот не писал своего языка. Все, я недостаточно профессиональный разработчик?


  1. GeniyZ
    17.07.2022 18:17
    +2

    Я вообще ни чего не понял (


    1. TalismanChet Автор
      17.07.2022 22:07
      -2

      что же вы не поняли? я объясню вам в комментариях, а также доработаю статью.


  1. VaalKIA
    17.07.2022 19:20
    +12

    Какой-то бред! Любой, кто пытался написать свой язык, тут же погружается в невозможность использования регулярок, в виду их недостаточной выразительности, потом следует исследование «а чего им не хватает», потом контекстная зависимость размывает понятие лексер и такие тривиальные вещи, как вложенные комментарии, становятся не такими тривиальными, зато вы приходите к осознанию, того, что сообщить об ошибке компиляции и показать, что-то вразумительное — это не тривиальная задача. В итоге, ваш пост выглядит как инструкция для лохов.


    1. TalismanChet Автор
      17.07.2022 22:02
      -1

      это только первая часть! это - то, с чего я начал. сейчас, само собой, я действую иначе, но об этом в следующих частях.


  1. ktod
    17.07.2022 21:20
    -5

    Зачем вы в статье используете жаргонизмы "лексер" и "парсер"? Они скрывают суть вещей. Почему бы не использовать классические академические термины "лексический анализатор" и "синтаксический анализатор"? Вам так жаль лишние буквы потратить?


    1. TalismanChet Автор
      17.07.2022 22:03

      это не жаргонизмы, а оригинальные названия. и суть вещей они не скрывают. я раскрыл в статье всю терминологию и оставил ссылки на вики.


  1. gochaorg
    17.07.2022 23:02

    Чем ваш язык будет отличаться ?


  1. GeniyZ
    18.07.2022 07:08
    +1

    Вот: https://habr.com/ru/post/119850/
    Хороший пример серии статей о создании ЯП.

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

    И да, написано небрежно, читаешь — и складывается ощущение, что автор тебе дерзит. Но это дело опыта, это наживное.
    А может и действительно автор — щегол)


  1. alexshipin
    18.07.2022 08:20
    +1

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

    После прочтения слов "Я считаю, что создать свой ЯП должен каждый программист" и статьи далее, новички ничего не поймут.

    Извините, но какой опыт они должны впитать, если этого опыта тут и нет, а есть какие-то обрывочные высказывания с примерами хорошего ничего?

    Первое общее впечатление - это авторская заметка, но никак не "гайд".

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

    Я считаю, что создать свой ЯП должен каждый программист,

    Можете спокойно считать так и дальше, только данное лучше держать при себе, а не накидывать на "вентилятор".

    Итоговое общее заключение: Где фабула? Где сюжет? Где хоть что-нибудь?..

    PS: Полностью сами себе противоречите в тех же комментариях, опираясь на статью, написанную вами же (или нет?).


  1. webhamster
    18.07.2022 10:33

    Подытожим. Автор не составил план, откладывая определение формы всего повествования на потом. И к тому же не показал ни синтаксис создаваемого языка, ни его парадигму, не обосновал необходимость его создания. Зато сразу показал лексический анализатор к своему неизвестному языку, что сразу вызывает вопрос: где последовательность повествования?


  1. playermet
    18.07.2022 12:56

    В статье о создании языка хотелось бы видеть более развернутое описание "зачем" и "почему". Например "язык предполагается для X", "поэтому в нем должны быть фичи Y", "у них будет синтаксис Z потому что A", "такой синтаксис удобно парсить с помощью B потому что C, и у него будут плюсы D и минусы F", и т.д.. Чтобы человек который сам изучает тему и наткнется на вашу статью смог понять мотивацию выбора, и на основе этого сделать собственные заметки.