С 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)

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


  1. dyadyaSerezha
    16.04.2024 22:07
    +1

    1) первые две строки в ChangeValue:

    if value != str: value = str(value)

    Это как? Может isinstance?

    2) в ChangeValue три очень похожих куска. Хорошо бы их зарефакторить.


    1. AlmazCode Автор
      16.04.2024 22:07

      Да, вы правы. Стоит отрефакторить данный метод


  1. ALexKud
    16.04.2024 22:07
    +1

    Если уж идти до конца, то нужен ещё режим отладки, пошаговое выполнение, просмотр переменных и тп.


    1. dyadyaSerezha
      16.04.2024 22:07

      Ага-ага. Подсвечивание синтаксиса, подсказки программисту и оптимизация для современных процессоров. Что-то упустил? GUI-библиотека и асинхронное программирование?


  1. domix32
    16.04.2024 22:07

    копипаста-копипасточка. Вы бы хоть в линтер какой-нибудь свой код скормили (ruff например), он бы подсказал как писать понятнее. И тестов нет.

    Немного непонятно чего вам текстом команды не писалось, сделали б как у wasm - тектовое и бинарное представления. Вот например тот же код из начала статьи

    #begin 1
    
    cell -1 1
    mov -1 "Hello, World!"
    print -1
    
    #end


    1. AlmazCode Автор
      16.04.2024 22:07
      +1

      Моя цель была в том, чтобы специально сделать язык нечитаемым


      1. domix32
        16.04.2024 22:07

        Как-то вы в статье не освятили ни этот момент, ни что же в нём эзотеричного.


  1. AlibekBa
    16.04.2024 22:07
    +3

    Ого в 7 классе, мне 13 я знаю Python и Html css js