Как я написал свой первый эзотерический язык программирования

578a5aa9917e743db0718ba72feaf9d5

С 7 класса я стал очень сильно интересоваться программированием, после того как на одном из уроков информатики мы начали изучать такой язык, как Python. Для меня это было что-то новое и страшное, ведь до этого я программировал лишь на блочных языках (конструкторах по типу Scratch). Однако, я очень быстро углубился в изучение этого прекрасного языка и уже три года создаю на нем различные программы. Я многое на нем писал, от «Hello, World!» до собственного игрового движка. Это был невероятный опыт.

Но больше всего я хотел написать свой язык программирования, который был бы необычным. Было много попыток, но все они были тщетны, так как мне не хватало опыта. И неделю назад я увидел на YouTube видео, где автор рассказывает об 10 эзотерических языках — языках, которые были специально созданы так, чтобы программы на них было сложно или почти невозможно писать. И я словил сильное вдохновение написать такой же язык. Так на свет появился язык C42.

О языке

C42 — это язык, похожий на Assembler, где есть всего 42 команды для написания программы, а данные можно хранить в специальных ячейках (подобные переменным), каждая из которых может хранить только определенный тип данных (int, string, float). Вот пример кода, который выводит всеми любимую фразу в консоль:

#1 1

41 -1 1
04 -1 "Hello, World!"
02 -1

#0

В данном языке я решил сделать так, чтобы код хранился в определенном блоке (аналог функции), который можно вызывать. В данном коде есть один блок с именем 1, и не спроста, потому что блок с индификатором 1 является точкой входа (как в том же C функция main), где должен храниться основной код программы. Начало блока обозначается #1 blockID., а конец — #0. Стоит подметить, что имя блока может быть только числом и не более. А дальше в блоке пишутся команды. Комментарии в данном языке есть только в виде однострочных, начиная с символа $ все дальше будет считаться комментарием.

Каждая команда в C42 принимает в себя определенное количество аргументов (или не принимает вовсе). Например, команда 41 (создание новой ячейки) принимает в себя 2 аргумента: имя новой ячейки, которое может быть только отрицательным числом от -1 и тип данных, которое сможет хранить эта ячейка: 0 — int, 1 — string, 2 — float

Как я писал интерпретатор

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

  • main.py — как главный файл чтобы запускать интерпретатор.

  • interpreter.py — сам интерпретатор.

  • exception.py — файл для вывода ошибок в коде.

  • constants.py — где есть все необходимые константы.

Главный файл у меня вышел достаточно короткий:

from interpreter import Interpreter


code: str = ""

with open("code.cft", "r", encoding = "utf-8") as file:
    code = file.read()

C42 = Interpreter(code)
C42.Interpret()

Здесь я просто импортирую класс интерпретатора, читаю файл с кодом, создаю новый экземпляр и вызываю главный метод класса.

Файл для обработки ошибок также получился довольно компактным, всего 49 строк кода:

class Error:
    def __init__(self, message, line, command):
        print()
        if command != None:
            print(f"> {command}")
            print(f"Ошибка : на строке {line} : {message}")
        else:
            print(f"Ошибка : {message}")
        exit(1)

class BlockNotFound(Error):
    def __init__(self, name):
        super().__init__(f"Не удалось найти блок под номером {name}", None, None)
...

Здесь я создал базовый класс, который выводит команду, вызвавшую ошибку, номер строки ошибки и само сообщение об ошибке. Затем на основе этого класса я создал дочерние классы. По файлу с константами особо проходиться не стоит, я просто для каждой команды создал переменную с ее идентификатором. Файл с интерпретатором у меня получился на 576 строк, поскольку реализацию всех команд я включил прямо в класс. Возможно, это не самый лучший подход, но, думаю, что для моего случая это подойдет. В файле есть два класса: Cell, для удобной работы с ячейками:

class Cell:

    CELLS: list['Cell'] = []

    def __init__(self, name: str, defaultValue: int | float | str, dataType: str):

        cell = Cell.GetCellByName(name)
        if cell != None:
            Cell.CELLS.remove(cell)
        Cell.CELLS.append(self)

        self.name = name
        self.defaultValue = defaultValue
        self.value = defaultValue
        self.dataType = dataType
    
    @staticmethod
    def GetCellByName(name: str) -> 'Cell':
        return next((cell for cell in Cell.CELLS if cell.name == name), None)

    @staticmethod
    def isFloat(s):
        try: 
            float(s)
            return True
        except:
            return False

    @staticmethod
    def isInt(s):
        try: 
            int(s)
            return True
        except:
            return False

    @staticmethod
    def isString(s):
        return s[0] == "\"" and s[-1] == "\"" if s != "" else True
    
    @staticmethod
    def isCorrectName(name: str):
        return bool(re.match(r"^-[1-9][0-9]*$", name))
    
    @staticmethod
    def isCorrectDataType(char: str):
        return char in [INT, STRING, FLOAT]

И сам интерпретатор:

class Interpreter:
    def __init__(self, code: str):

        self.cells: list[Cell] = []

        self.code = code
        self.blocks: dict[list[list[str]]] = {}
        self.currentLine = 0
        self.currentCommand = ""
        self.skipNextCommand = False
        self.executionStack: list[list[str, bool]] = []
        self.returnCalled = False

        self.Parse()
    
    def Interpret(self, blockId: str = "1"):
        if blockId not in self.blocks:
            BlockNotFound(blockId)
        
        self.executionStack.append([blockId, False])
        idx = {block: -1 for block in self.blocks}

        while self.executionStack:
            currentBlock = self.executionStack.pop()
            commands = self.blocks[currentBlock[0]]
            blockName = currentBlock[0]
            blockIsLoop = currentBlock[1]

            while 1:
                idx[blockName] += 1 if not idx[blockName] + 1 > len(commands) - 1 else 0
                self.currentLine = commands[idx[blockName]][0]
                self.currentCommand = " ".join(commands[idx[blockName]][1])

                if self.skipNextCommand:
                    self.skipNextCommand = False
                    continue

                nextCommand = None if idx[blockName] + 1 > len(commands) - 1 else commands[idx[blockName] + 1]
                forceExit = self.ExecuteCommand(commands[idx[blockName]][1], nextCommand)

                if self.returnCalled:
                    break
                elif forceExit:
                    break
                elif idx[blockName] >= len(commands) - 1:
                    idx[blockName] = -1
                    break

            if not self.returnCalled and blockIsLoop:
                self.executionStack.append(currentBlock)
                self.returnCalled = False
            elif forceExit and not idx[blockName] >= len(commands) - 1:
                self.executionStack.append(currentBlock)
                self.executionStack[-1], self.executionStack[-2] = self.executionStack[-2], self.executionStack[-1]

    def ExecuteCommand(self, command, nextCommand):
        CMD = command[0]

        if CMD == EXIT: exit(1)
        
        elif CMD == PRINT:
            cell = self.GetCell(self.GetArgument(1, command))
            print(str(cell.value).replace("\\n", "\n"), end = "", flush = True)
    
        elif CMD == INPUT:
            cell = self.GetCell(self.GetArgument(1, command))
            value = input()
            self.ChangeValue(cell, value, False)

        elif CMD == ASSIGN_VALUE:
            value = self.GetArgument(2, command)
            cell = self.GetCell(self.GetArgument(1, command))
            self.ChangeValue(cell, value)
            
        ... # дальше тут реализация остальных команд

    def Parse(self):
        lines = self.code.split('\n')
        result = {}
        block = None
        line_number = 1

        for line in lines:
            if line.startswith(START_BLOCK):
                if block != None:
                    del result[block]
                words = line.split()
                if len(words) <= 1 or len(words) > 2:
                    block = None
                    continue
                block = words[1]
                result[block] = []
            elif line.startswith(END_BLOCK):
                block = None
            elif block is not None and line.strip():
                parsed_line = (line_number, re.findall(r'(?:"[^"]*"|[^"\s]+)', line))
                if '$' in parsed_line[1]:
                    parsed_line = (parsed_line[0], parsed_line[1][:parsed_line[1].index('$')])
                if parsed_line[1]:
                    result[block].append(parsed_line)

            line_number += 1

        self.blocks = result
    
    def GetCell(self, name: str) -> Cell:
        cell = Cell.GetCellByName(name)
        if cell != None:
            return cell
        CellNotFound(self.currentLine, self.currentCommand, name)
    
    def GetArgument(self, index: int, command: list[str]) -> str:
        if index <= len(command) - 1:
            return command[index]
        InvalidSyntax(self.currentLine, self.currentCommand)
    
    def ChangeValue(self, cell: Cell, value: str, lookAtQuotes = True, mode = "set") -> bool:
        if value != str:
            value = str(value)

        if cell.dataType == INT:
            if Cell.isInt(value):
                if mode == "set":
                    cell.value = int(value)
                elif mode == "add":
                    cell.value += int(value)
            else:
                IncorrectValue(self.currentLine, self.currentCommand, "int")
        
        elif cell.dataType == FLOAT:
            if Cell.isFloat(value):
                if mode == "set":
                    cell.value = float(value)
                elif mode == "add":
                    cell.value += float(value)
            else:
                IncorrectValue(self.currentLine, self.currentCommand, "float")
            
        elif cell.dataType == STRING:
            if Cell.isString(value) or not lookAtQuotes:
                if mode == "set":
                    cell.value = value[1:-1] if lookAtQuotes else value
                elif mode == "add":
                    cell.value += value[1:-1]
            else:
                IncorrectValue(self.currentLine, self.currentCommand, "string")

В init я создаю все необходимое и вызываю метод Parse. Этот метод преобразует входной код в словарь блоков, каждый из которых содержит полный список команд из блока. В методе Interpret программа проходит по последнему блоку в self.executionStack, который хранит блоки, которые программа должна выполнить. Если данный блок был вызван как цикл, то программа будет выполнять его до тех пор, пока не будет вызвана команда 42 (return) внутри блока. Если же блок был вызван для обычного выполнения, то после прохождения по всем его командам, программа удалит блок из списка.

Заключение

В общем и целом, я рад, что смог написать свой собственный язык. Хотя он и не принес мне много опыта, но я смог осуществить свою давнюю мечту. Спасибо, что прочитали мою первую статью. Желаю всем хорошего дня!

Исходник языка, если кому надо — AlmazCode/C42: C42: Эзотерический язык программирования вдохновленный ASM. (github.com)

Habrahabr.ru прочитано 2716 раз