Привет, Хабр! В этой статье я хотел бы поделиться опытом создания своего языка программирования.

Предыстория

Мне 14. Обучаясь на втором году Яндекс Лицея, нужно было написать несколько проектов. Первым из них стал проект на PyQT5. Я долго думал над идеей и вспомнил, что летом я хотел создать свой язык, но у меня этого не получилось (Тогда я не понимал как работает парсер и абстрактное синтаксическое дерево, поэтому забросил). И вот, мне пришла идея - сделать свой язык программирования и написать для него IDLE (т.к. тема проекта все-таки QT). Ещё полгода назад я изучал асинхронность и многопоточность, поэтому именно одну из этих идей я хотел воплотить в своём языке. В данной статье я хотел рассказать устройство интерпретируемых языков и как их создать.

Чем отличается интерпретируемый язык от компилируемого

Вся разница в исполнении кода. Интерпретируемые языки работают по следующей схеме:

Lexer.py ---> Parser.py ---> AbstractSyntaxTree.py ---> result

Ниже я более подробно рассказываю про работу всех компонентов кроме AST, потому что на момент написания языка я не понимал как это работает.

Компилируемые языки программирования исполняют код не построчно (как интерпретируемые). Процесс компиляции включает препроцессинг исходного кода, где удаляются комментарии и вставляются содержимое файлов, компиляцию кода в ассемблерный или промежуточный код, а затем ассемблирование этого кода в машинный код, который линкуется в исполняемый файл с разрешением внешних ссылок. Проще говоря, написанный код переводится либо на ассемблер, либо на другие компилируемые языки, которые его исполняют.

Устройство языка

Я решил, что мой язык будет состоять только из Лексера и Парсера (окончательное понимание синтаксического дерева пришло ко мне под конец). Я сразу отказался использовать такие библиотеки, как PLY и RPLY, поэтому мой проект написан на чистом Python. Про компоненты языка я буду объяснять неформально, то-есть так, как понял это я.

Лексер

В каждом языке программирования есть Лексер и Парсер. Лексер - класс, который принимает строку кода и разбивает её на токены. Токены нужны для проверки грамматики на этапе парсинга.

Заранее извиняюсь за мой кривой монтаж
Заранее извиняюсь за мой кривой монтаж

Токены, которые представлены на картинке, являются объектами класса Token, Данный класс принимает в __init__: тип токена и его значение.

NUMBER = "NUMBER"
STRING = "STRING"

class Token:
    def __init__(self, type, value):
        self.type = type
        self.value = value

    def __str__(self):
        return f'Token({self.type}, {self.value})'

    def __repr__(self):
        return self.__str__()

Помимо класса Token, есть вышеупомянутый класс Lexer, который посимвольно проходит строку циклом и возвращает объекты класса Token:

class Lexer:
      def __init__(self, text):
          self.text = text
          self.pos = 0
          self.current_char = self.text[self.pos]

      def advance(self):
          self.pos += 1
          if self.pos < len(self.text):
              self.current_char = self.text[self.pos]
          else:
              self.current_char = None

      def return_pos(self, pos):
          self.pos = pos
          if self.pos < len(self.text):
              self.current_char = self.text[self.pos]
          else:
              self.current_char = None

      def skip_whitespace(self):
          while self.current_char is not None and self.current_char.isspace():
              self.advance()

      def get_next_token(self):
          while self.current_char is not None:
              if self.current_char.isspace():
                  self.skip_whitespace()
                  continue
 
              if self.current_char.isdigit() or self.current_char == "-":
                  token_value = ""
                  while self.current_char is not None and (self.current_char.isdigit() or self.current_char == "." or self.current_char == "-"):
                      token_value += self.current_char
                      self.advance()
                  try:
                      return Token(NUMBER, int(token_value))
                  except:
                      try:
                          return Token(NUMBER, float(token_value))
                      except:
                          return Token(MINUS, "-")
 
              if self.current_char == '"' or self.current_char == "'":
                  self.advance()
                  string_value = ""
                  while self.current_char is not None and self.current_char != '"' and self.current_char != "'":
                      string_value += self.current_char
                      self.advance()
                  if self.current_char == '"' or self.current_char == "'":
                      self.advance()
                      return Token(STRING, '"' + string_value + '"')
                  else:
                      print(f"Некорректный символ: {self.current_char}")
                      sys.exit()
 
              if self.current_char == '=':
                  self.advance()
                  return Token(ASSIGN, '=')
 
              if self.current_char == '+':
                  self.advance()
                  return Token(PLUS, '+')
 
              if self.current_char == '-':
                  self.advance()
                  return Token(MINUS, '-')
 
              if self.current_char == '*':
                  self.advance()
                  return Token(MULTIPLY, '*')
 
              if self.current_char == '/':
                  self.advance()
                  return Token(DIVIDE, '/')
 
              if self.current_char == '%':
                  self.advance()
                  return Token(PR, '%')
 
              if self.current_char == '(':
                  self.advance()
                  return Token(LPAREN, '(')
 
              if self.current_char == ')':
                  self.advance()
                  return Token(RPAREN, ')')
 
              if self.current_char == '^':
                  self.advance()
                  return Token(DEG, '^')
 
              if self.current_char == ',':
                  self.advance()
                  return Token(COMMA, ',')
 
              if self.current_char.isalpha():
                  token_value = ""
                  while self.current_char is not None and (self.current_char.isalpha() or self.current_char.isdigit() or self.current_char == "_"):
                      token_value += self.current_char
                      self.advance()
                  if token_value == "int":
                      return Token(INT, "int")
                  elif token_value == "float":
                      return Token(FLOAT, "float")
                  elif token_value == "str":
                      return Token(STR, "str")
                  elif token_value == "None":
                      return Token(NONE, "None")
                  elif token_value == "print":
                      return Token(PRINT, "print")
                  elif token_value == "input":
                      return Token(INPUT, "input")
                  elif token_value == "True" or token_value == "False":
                      return Token(BOOL, token_value)
                  else:
                      return Token(VAR, token_value)

              print(f"Неверный синтаксис: {self.text}")
              sys.exit()

          return Token(EOF, None)

Это лишь половина класса Лексера. Он проходится по каждому символу и проверяет его существование. Если Лексер нашёл символ, то возвращает объект класса Token с переданными типом символа и самим символом. Помимо этого существуют ещё три класса, которые хранят значения локальных переменных и код функций:

Парсер

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

import math
from lexer import *


# Определение парсера для вычисления выражений
class Parser():
    def __init__(self, lexer, symbol_table):
        """
        Конструктор класса Parser.

        Параметры:
        - lexer: экземпляр класса Lexer для токенизации входного текста.
        - symbol_table: экземпляр класса SymbolTable для отслеживания переменных.

        symbol_table - объект класса SymbolTable, используется для 
        хранения и управления переменными.
        """
        self.lexer = lexer
        self.current_token = self.lexer.get_next_token()
        self.symbol_table = symbol_table

    def eat(self, token_type):
        """
        Функция проверяет текущий токен и токен, с которого
        мы хотим перейти на другой токен.
        То есть у нас не получиться будучи на Token(VAR, "x") перейти на 
        следущий токен с того, которого мы передаём в функцию:
        Текущий токен: VAR, мы передали в функцию токен PRINT - 
        получаем ошибку, потому что мы находимся на токене VAR, а хотим перейти
        на следующий токен с токена PRINT.
        """
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            raise Exception(f"Error eating {self.current_token.type} token!")

    def factor(self):
        """
        Функция отвечает за использование встроенных функций языка или других 
        токенов которые возвращают значения:
        ... = input()
        ... = int(x)
        ... = True
        """
        token = self.current_token
        if token.type == NUMBER:
            self.eat(NUMBER)
            return token.value
        elif token.type == STRING:
            string_value = token.value
            self.eat(STRING)
            return string_value
        elif token.type == NONE:
            self.eat(NONE)
            return None
        elif token.type == BOOL:
            result = token.value
            if result == "True":
                result = True
            else:
                result = False
            self.eat(BOOL)
            return result
        elif token.type == VAR:
            var_name = token.value
            self.eat(VAR)
            return self.symbol_table.lookup(var_name)
        elif token.type == INT:
            self.eat(INT)
            self.eat(LPAREN)
            result = int(self.expr())
            self.eat(RPAREN)
            return result
        elif token.type == FLOAT:
            self.eat(FLOAT)
            self.eat(LPAREN)
            result = float(self.expr())
            self.eat(RPAREN)
            return result
        elif token.type == STR:
            self.eat(STR)
            self.eat(LPAREN)
            result = str(self.expr())
            self.eat(RPAREN)
            return result
        elif token.type == LPAREN:
            self.eat(LPAREN)
            result = self.expr()
            self.eat(RPAREN)
            return result

    def term(self):
        """
        Обработка терма (произведения или частного).

        Возвращает:
        - Результат вычисления терма.
        """
        result = self.factor()

        while self.current_token.type in (MULTIPLY, DIVIDE, PR, DEG):
            token = self.current_token
            if token.type == MULTIPLY:
                self.eat(MULTIPLY)
                result *= self.factor()
            elif token.type == DIVIDE:
                self.eat(DIVIDE)
                result /= self.factor()
            elif token.type == PR:
                self.eat(PR)
                result %= self.factor()
            elif token.type == DEG:
                self.eat(DEG)
                num = self.factor()
                result = math.pow(result, num)

        return result

    def expr(self):
        """
        Обработка выражения (суммы или разности).

        Возвращает:
        - Результат вычисления выражения.
        """
        result = self.term()

        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
                result += self.term()
            elif token.type == MINUS:
                self.eat(MINUS)
                result -= self.term()

        return result

    def statement(self):
        """
        Функция отвечает за встроенные функции и конструкции,
        которые не возвращают значения:
          print()
          var = ...
          if ...

        """
        if self.current_token.type == VAR:
            var_name = self.current_token.value
            self.eat(VAR)
            if self.current_token.type == ASSIGN:
                self.eat(ASSIGN)
                value = self.expr()
                self.symbol_table.define(var_name, value)
            else:
                sys.exit()

        elif self.current_token.type == PRINT:
            self.eat(PRINT)
            self.eat(LPAREN)
            values = []
            while self.current_token.type != EOF and self.current_token.type != RPAREN:
                values.append(self.expr())
                if self.current_token.type == COMMA:
                    self.eat(COMMA)
            self.eat(RPAREN)
            print(*values)
        else:
            self.current_token = self.lexer.get_next_token()

    def parse(self):
        """
        Парсинг входного текста и выполнение выражений.

        Эта функция запускает парсинг входного текста и последовательно выполняет
        выражения, включая присваивание переменных и вывод значений.
        """
        while self.current_token.type != EOF:
            self.statement()

Теперь рассмотрим на примере, как работает класс. На вход подаётся строка:

x = 5

Она проходит через лексер:

Token(VAR, "x")
Token(ASSIGN, "=")
Token(NUMBER, 5)
Token(EOF, None)

После токены используются в парсере:

Token(VAR, "x") ---> self.parse() ---> self.statement() ---> self.expr() ---> self.term() ---> self.factor()
                                               |                                                     |
                                              x =       <--- self.expr() <--- self.term() <---       5 
                                               |
                                  self.symbol_table.define("x", 5)

На вход поступает первый токен — Token(VAR, «x»). Класс запускается при использовании функции self.parse(). Функция self.parse() вызывает функцию self.statement(), которая вызывает функцию self.expr() и ждет от неё возвращаемое значение. Функция self.expr() вызывает self.term() и тоже ждёт возвращаемое значение и т.д.

Возникшие проблемы

Основными проблемами стали GIL и низкая скорость Python. Язык работал слишком медленно (Был вариант переписать на C++, но приближалась сдача проекта), а параллельные циклы, реализованные с помощью asyncio, не работали вовсе (т.к. в моём случае корутины не работали из-за блокируемых операций). Asyncio пришлось заменить библиотекой multiprocessing, которая запускает несколько Python интерпретаторов. Проблему с низкой скоростью я решил с помощью Cython. Он помог разогнать язык в 90 раз! Поэтому, если вы захотите написать что-нибудь на моём языке, Вам придётся установить Cython и выполнить компиляцию файлов .pyx (делается это просто, расписал инструкцию в репозитории).

Выводы

В этой статье я постарался рассказать, как работают интерпретируемые языки программирования. Язык я завершил после добавления функций и многопоточных циклов. документацию, примеры и код к языку вы можете найти в моём репозитории. На написание данной статьи меня подтолкнуло малое количество информации на подобную тему. Буду рад комментариям и Вашим замечаниям. До скорых встреч!

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


  1. SpiderEkb
    26.11.2023 09:15
    +6

    Поздравляю с изобретением велосипеда :-)

    Вообще-то есть LEX и YACC (или аналоги их - FLEX и BISON) Там достаточно описать грамматику языка чтобы получить исходный код интерпретатора на С/С++ Дальше уже можно играться с компиляцией в байт-код (например, для ускорения выполнения программы).

    Когда-то давно занимался этим - нужно было написать скриптовый язык для работы со специфической платой расширения для компа (фактически там были ЦАП/АЦП - что-то к ней цеплялось, нужна была возможность писать скрипты для работы с тем что к ней прицепили). Делал интерпретатор скрипта (для тестов) и компилятор скрипта в байткод + интерпретатор байткода (когда скрипт уже протестирован).

    Получился вполне себе типизированный процедурный язык с вистом и профурсетками переменными, условиями, циклами + набором встроенных специфических функций для работа с платой расширения.

    Так вот делал как раз на С++ + FLEX + BISON. Даже работало :-)


    1. Alexander554 Автор
      26.11.2023 09:15
      +8

      Добрый день! Я рассматривал использование Flex и Bison. Основная цель проекта - самому написать Лексер и Парсер и приблизительно понять, как это работает изнутри. Также возникли проблемы с изучением Bison. Когда я начинал, я не понимал как описывать грамматику.


      1. sshikov
        26.11.2023 09:15
        +2

        Потому что и FLex и Bison, опять же - это очень старые технологии. Причем ориентированные на один целевой язык. Посмотрите на ANTLR, для начала. Он вам построит парсер на питоне, если вам так удобнее.

        В принципе, написать самому что-то типа LL(1) на языке высокого уровня вообще не сложно, это будет примерно столько же кода, что вы тут показали. Все из чего состоит генератор парсеров - это по сути, снова парсер метаязыка описания грамматики (EBNF обычно), а дальше некоторый объем возни с таблицами символов конкретной грамматики (для LL(1), опять же - это например построение транзитивных замыканий множеств символов, с которых может начинаться нетерминал). Ничего принципиально сложного там нет. Главное начать с хорошей книжки по теории.


        1. Alexander554 Автор
          26.11.2023 09:15
          +1

          Спасибо за совет! Я рассмотрю использование LL(1)


          1. auddu_k
            26.11.2023 09:15

            К своим, если есть интерес, очень рекомендую после такого упражнения попробовать ANTLR4 или какие ещё генераторы умеют в Python, просто для сравнения.



    1. Viacheslav01
      26.11.2023 09:15
      +2

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

      Но в свое время когда надо было свой "язык" в "прод", то в бой пошел ANTLR )


  1. sshikov
    26.11.2023 09:15
    +7

    Я сразу отказался использовать такие библиотеки, как PLY и RPLY, поэтому мой проект написан на чистом Python. 

    То что вы PLY не взяли - наверное даже правильно, потому что по описанию это порт LEXX/YACC, а это ну очень старый подход к построению интерпретаторов. Можно и что-то получше найти взамен.

    А вот то что вы про грамматику своего языка ничего не написали, а сразу код стали приводить - это зря. По-хорошему, если уж вы делаете язык - опишите грамматику. А потом уже из нее можно было бы построить (автоматически) таблицы для разбора одним из проверенных методов, типа LR(n) или даже самым простым LL(1). А для лексера напрашивается конечный автомат.

    В вашем же коде, не порывшись достаточно глубоко, не понять, что за метод вы применили. И кстати, именно поэтому у вас парсер сложный, в том числе для вас. В случае же например LL(1), парсер это строк 20 кода, примерно. Остальное таблицы, которые автоматически строятся из грамматики. В этом случае вам зачастую вообще не нужно код вашего парсера понимать, потому что он строится автоматически, а вы для него дописываете функции, выполняемые например при распознавании в тексте какой-то части грамматики.


    1. SpiderEkb
      26.11.2023 09:15
      +2

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

      LEX/YACC это не то чтобы старый подход. Это потоковые (насколько помню) интерпретаторы. Они позволяют (в том числе) строить и командные процессоры, а не только интерпретаторы скриптовых языков.

      Но описание грамматики нужно, да.


      1. sshikov
        26.11.2023 09:15

        LEX/YACC это не то чтобы старый подход. 

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

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

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


        1. SpiderEkb
          26.11.2023 09:15

          Ну я этой темы не касался очень давно - то что делал, делал лет 15 назад. И тогда какого-то серьезного выбора не было.

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


          1. sshikov
            26.11.2023 09:15

            ANTLR это вообще-то на минутку, 1992 год. Так что выбор был.


            1. SpiderEkb
              26.11.2023 09:15

              Возможно, но тогда (2005-й год или раньше даже) что-то найти было сложнее, особенно когда не знаешь точно что ищешь :-)

              Так или иначе, поставленная задача тогда была решена в установленные сроки и с потребным качеством. С тех пор не приходилось заниматься подобными вещами.


          1. vkni
            26.11.2023 09:15

            то что делал, делал лет 15 назад.

            OCaml, ocamllex/ocamlyacc — они хотя бы типы не стирают.


      1. vkni
        26.11.2023 09:15

        LEX/YACC это не то чтобы старый подход.

        Если под этим подходом вы имеете ввиду генераторы парсеров, то, конечно, не старый. А если конкретно пару Lex/Yacc — то это из времён, когда динозавры бегали по болотам. Мало того, что вместо них flex/bison, так и те стирают типы, передавая в основную программу.

        Из современного antlr4, BNFC, ocamllex/menhir.


        1. KvanTTT
          26.11.2023 09:15

          antlr4

          Едва ли его можно назвать современным, но с учетом отсутствия альтернатив сгодится.


    1. slonopotamus
      26.11.2023 09:15

      ну очень старый подход к построению интерпретаторов. Можно и что-то получше найти взамен

      Подходы кагбэ не портятся со временем. Вы предлагаете LL вместо LR. Но LL уступает LR в некоторых аспектах.


      1. sshikov
        26.11.2023 09:15

        Я не предлагаю LL и даже LR. Вам показалось. Я предлагаю взять любой инструмент, который умеет генерировать код по любой типовой грамматике. Как правило это будет LR, кстати.


  1. x86128
    26.11.2023 09:15
    +1

    Направление верное взяли. Фишка ручного разбора в том (а не готового генератора типа PLY/FLEX/YACC), что можно выдавать человекопонятные сообщения об ошибках (забытая скобка, или перменная) и за один проход находить не только первую ошибку, а несколько, например в разных функциях вашего языка.


  1. raspberry-porridge
    26.11.2023 09:15
    +5

    Каждая такая статья кидает камень в четырнадцатилетнего меня, который по видеокурсам пытался свой первый сайт на php сделать (получилось очень плохо).


    1. m1n7
      26.11.2023 09:15
      +21

      получилось


    1. vkni
      26.11.2023 09:15

      Каждая такая статья кидает камень в четырнадцатилетнего меня, который по видеокурсам пытался свой первый сайт на php сделать (получилось очень плохо).

      Пфуй, у нас в 14 лет учителя говорили, что не надо заниматься всякой низкоинтеллектуальной чепухой вроде програзма! :-)


  1. MountainGoat
    26.11.2023 09:15

    Я никогда не интересовался созданием языков программирования, но регулярно натыкался на утверждения что в Rust макросы настолько разнообразные, что на них можно делать новые и новые языки.


    1. rutexd
      26.11.2023 09:15

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


  1. Myclass
    26.11.2023 09:15
    +3

    Даже, если и соглашусь со всеми комментаторами, что подход у автора и не самый оптимальный и маловато инфы. Но принимая во внимание, возраст и неопытность - скажу - неплохо и желаю удачи в будущем.


  1. domix32
    26.11.2023 09:15
    -1

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


  1. MegaMANGO
    26.11.2023 09:15

    .


  1. KvanTTT
    26.11.2023 09:15
    +4

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

    А для меня парсер - одна из простейших частей языка. Говорю как программист, работающий над компилятором языка Kotlin.


    1. D7ILeucoH
      26.11.2023 09:15
      +3

      Ну так ты сравнил божественный Котлин и чё-попало Питон) точнейшая типизация делает парсинг сильно проще. Байт-код ART выглядит отлично, он намного понятнее даже того же Ассемблера (который вообще "обёртка над байт-кодом") - именно такое сравнение мне пришло на ум...

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

      Хотя если честно я не очень понял в чём смысл этой работы, в то время как основная её фишка "многопоточный цикл" работает неправильно xD. Во избежание неадекватных минусов поясню для тех кто не допёр: посмотрите на output цикла от 0 до 4 в три потока, который выдаёт значение 0 четвертым. Подсказка: потоки работают последовательно, зачем ноль в конце?

      Коммент про парсинг говорю как энтузиаст, изнедавна ковыряющий SMALI (продукты жизнедеятельности Котлина)


      1. Viacheslav01
        26.11.2023 09:15

        Потоки работают паралельно, на то они и потоки.
        Ну а если дело в том, что 3 потока при обработке 4 выдали 0 в конце, ну так то же ничего сверестественного. Планируем на выполнение 4 задачи, 0 забирает первый поток и тут у него отбирают управление и передают второму, дальше дела, дела, потом управление возвращают первому, после всех остальных. Бинго )


    1. vkni
      26.11.2023 09:15

      А для меня парсер - одна из простейших частей языка.

      Это немножечко напоминает анекдот про то, как мальчики в классе мерялись писюнами. :-)


  1. StreamThread
    26.11.2023 09:15
    +1

    Очень хорошо. Можно только код рефакторить с применением подхода Don't Repeat Yourself (DRY). А именно, использовать новый оператор Match вместо elif'ов и try-except блоков, и словари.

    Ничего больше усложнять не стоит, чисто лишь код в порядок привести.


    1. Alexander554 Автор
      26.11.2023 09:15
      +1

      Спасибо за совет! Использовать словари действительно хорошая идея.


  1. vkni
    26.11.2023 09:15
    +2

    Очень хорошо!

    Одно замечание — вы смешиваете понятия «языка» и «интерпретатора». Язык — это соглашения, это не программа, он не может работать быстро или медленно. :-) Но это сейчас не важно.

    Вы сделали tree-walking interpreter. А дальше вы можете воспользоваться техниками из Crafting interpreters, чтобы сделать компилятор в байт-код, и интерпретировать уже байт-код. См https://craftinginterpreters.com/

    См. также https://github.com/true-grue/Compiler-Development — «Что читать о разработке компиляторов». И можно задавать народу вопросы в телеге «Compiler Development».

    Успехов!


    1. Alexander554 Автор
      26.11.2023 09:15

      Спасибо! Я уже думал, сделать язык компилируемым. Сейчас только изучаю теорию.


      1. vkni
        26.11.2023 09:15

        На всякий случай — вы смотрели курс SICP, который старый?

        https://bmstu-iu9.github.io/scheme-labs/sicp.pdf


  1. Zeusina
    26.11.2023 09:15

    А у меня в мои 15 лет не найдется проекта, который я был бы готов показать людям.


    1. TemaAE
      26.11.2023 09:15

      Не торопитесь, можно до 25 (50-ти) изучать базу и штуки интересные очень узкому кругу людей.
      А потом на основе этого сделать продукт всей своей жизни )