Всем привет! Сразу хочу сказать. Я просто пришел поделиться, как мне кажется, достаточно интересным проектом. Не претендую на то, что данный язык надо тянуть в продакшен и т.д. Более того, я прекрасно понимаю, что данный ЯП не годится для этого.
А теперь к сути :-)
Я достаточно давно мечтал сделать свой язык программирования, но времени на такое обычно мало. Однако, когда я учился в институте, ко мне пришла прекрасная идея: можно сделать язык программирования своим дипломным проектом. Когда все-таки я пришел с этой идеей к научному руководителю - он меня развернул со словами: "зачем ты собрался писать еще один велосипед? Это не интересно."
Но я не сдавался, поэтому, чтобы "продать" эту идею институту, я решил сделать синтаксис этого ЯП полностью на русском языке и, внимание, вообще сделать DSL язык для юристов. Но, с важным нюансом, там будут процедуры для императивной проверки всяких юридических правил. Так родился уникальный гибрид: юридический DSL, под капотом которого живет полноценный императивный язык. Кафедра получила то, что хотела, а я — легальную возможность писать интерпретатор в рабочее время. Win-win!
Важно! Непосредственно код моего языка программирования я частично привожу скриншотами, чтобы была подсветка синтаксиса! Там где это возможно я пользуюсь подсветкой 1С ;-)
Язык делится на две философии:
Декларативная. С простым описанием контрактов:

Императивная. Где уже понятный нам, программистам, код:

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

В начале было слово и слово было у Программиста, и слово было Токен! :) Понятно, что мой язык программирования построен как и все другие: лексер - парсер - компилятор в байт-код и интерпретатор. Но погодите, на схеме нет лексера! К сожалению, это не опечатка. Действительно, лексер перемешан с парсером. Ну а что? Мой велосипед! Как хочу так и пишу :)
Препроцессор
Прежде, чем код дойдет до лексера, а тем более до парсера, он проходит этап препроцессинга. На данном этапе, обрабатываются включения файлов кода друг в друга (импорты) и сохранение мета-информации о строках. Для этого, каждая строка оборачивается в такой класс:
class Info(NamedTuple): num: int file: str raw_line: str class Line(str): def __new__(cls, value: str, num: int = 0, file: str = ""): obj = str.__new__(cls, value) obj.raw_data = value obj.num = num obj.file = file return obj def get_file_info(self) -> Info: return Info( num=self.num, file=self.file, raw_line=self.raw_data )
Да-да, я написал ЯП на python:) Да, мой язык очень медленный.
Сам же препроцессор представляет из себя обычную python-функцию, которая построчно читает файл, пока не встретит команду ВКЛЮЧИТЬ после чего ищет файл по соответствующему пути в той же директории, что и сам скрипт, если не находит его, то выбрасывает исключение.
Файлы бывают 3-х типов:
.raw - это исходый код LawScript
.law - это байт-код всего проекта, собранный в один файл (на самом деле просто сериализованное AST, которое интерпретатор выполняет без повторного парсинга. Назвал байт-кодом для солидности ?)
.pyl - это Python-расширения для языка
Упрощенно, препроцессор выглядит так:
STANDARD_LIB_PATH = Path(__file__).resolve().parent.parent.parent STANDARD_LIB_PATH = f"{STANDARD_LIB_PATH}{settings.standard_lib_path_postfix}" STD_NAME = settings.std_name def _standard_lib_alias(path: str) -> str: if _is_std(path): return path.replace(STD_NAME, STANDARD_LIB_PATH) return path def _is_std(path: str) -> bool: return STD_NAME in path def import_preprocess(path, byte_mode: Optional[bool] = True) -> Union[Compiled, str]: """Обработка импорта файла""" def preprocess(raw_code, path: str) -> list: folder = os.path.dirname(path) prepared_code = [line.strip() for line in raw_code.split("\n")] imports = set() # В реальном коде здесь кэш импортированных модулей code = [] for offset, line in enumerate(prepared_code): code.append(Line(line.strip(), num=offset+1, file=path)) preprocessed = [] for offset, line in enumerate(code): match line.split(" "): case [Tokens.include, package] if package.endswith(Tokens.star): # Обработка импорта формата ВКЛЮЧИТЬ директория.* case [Tokens.include, module] if re.search(r'\.\S+$', module): # Обработка импорта формата ВКЛЮЧИТЬ директория.молуль case [Tokens.include, module]: # Обработка импорта формата ВКЛЮЧИТЬ молуль case _: # Просто строка исходного кода. Сохраняем preprocessed.append(line) imports.add(path) return [line for line in preprocessed if line]
Лексер и Парсер
Предлагаю посмотреть, что там в лексере-парсере, коль уж о них заговорили.
class Parser(ABC): def __init__(self): self.jump: int = -1 @abstractmethod def parse(self, body: list[str], jump: int) -> int: ... @abstractmethod def create_metadata(self, stop_num: int) -> MetaObject: ... @staticmethod def parse_sequence_words_to_str(words: Sequence[str]): return " ".join(words) def execute_parse(self, parser: Type["Parser"], code: list[Line], num: int) -> Union[MetaObject, BaseType]: parser = parser() meta = parse_execute(parser, code, num) self.jump = self.next_num_line(meta.stop_num) return meta @staticmethod def next_num_line(num_line: int) -> int: return num_line + 1 @staticmethod def previous_num_line(num_line: int) -> int: return num_line - 1
Перед вами исходный код парсера. Из него видно, что имплементация собственно парсинга - это дело рук подклассов данного класса. Сделано это для того, чтобы у каждой грамматической конструкции был свой, условно независимый парсер. Это позволяет вкладывать их друг в друга — один парсер просто вызывает другой, когда встречает знакомый синтаксис.
Парсер строит AST, узлами которого являются объекты MetaObject. Которые потом в компиляторе уже валидируются и компилируются в нормальные объекты языка LawScript.
Собственно, лексер - это метод парсера, который вызывается на каждую строку внутри других парсеров в методе parse. Код лексера тоже достаточно большой, поэтому я не буду приводить его полностью:
def separate_line_to_token(self, line: Line) -> list[str]: self._check_quotes(line) raw_line = line.raw_data is_string = False # флаг для отслеживания строковых литералов (в примере опущено) for offset, symbol in enumerate(raw_line): # Убираем комментарии из сырой строки end_symbols = (Tokens.left_bracket, Tokens.right_bracket, Tokens.comma, Tokens.end_expr) for end_symbol in end_symbols: if raw_line.endswith(end_symbol): break else: raise InvalidSyntaxError( f"Некорректная строка: '{line.raw_data}', возможно Вы забыли один из этих знаков в конце: <удалено в примере>" ) separated_line = self.__split(raw_line) tokens = [] for token in separated_line: if token in Tokens: tokens.append(token) continue # Другая специфическая обработка return tokens
Обратная польская нотация
А как дела обстоят с выражениями? В данном языке можно писать что-то сложное? Да! Однозначно! Можно даже писать выражения для аргументов по умолчанию, как в python!
Парочка примеров:


Каждое выражение в процессе парсинга собирается в RPN-стек. Код там не самый красивый и не самый маленький у меня получился, поэтому я его опущу. Отмечу лишь один момент. На этапе парсинга отлавливается много ошибок, и часть из них на этапе построения RPN-стека. Я даже подсказываю позицию, где возникла та или иная ошибка:
if token_ == Tokens.right_bracket: sub_expr = expr[offset:] previous_tok = sub_expr[offset_ - 1] if previous_tok == Tokens.comma: err_expr = ''.join([str(i) for i in sub_expr][:offset_+1]) sub_expr = [str(i) for i in sub_expr] res_expr = ''.join(str(i) for i in expr) target_comma = ( f"{err_expr}\n" f"{" " * (sum(len(t) for o, t in enumerate(sub_expr) if o < offset_ - 1))}^" ) raise InvalidExpression( f"В выражении: '{res_expr}' стоит лишняя запятая '{Tokens.comma}'\n\n" f"{target_comma}\n" )
Вот как это выглядит для пользователя языка:

В конечном итоге, из такого кода:
ОПРЕДЕЛИТЬ ПРОЦЕДУРУ сортировка_массива(массив_чисел) ( ЗАДАТЬ длина = длина_массива(массив_чисел); ЗАДАТЬ минимальный_индекс = 0; ЦИКЛ индекс ОТ 0 ДО длина-1 ( ! Находим минимальный элемент в оставшейся части массива минимальный_индекс = индекс; ЦИКЛ внутренний_индекс ОТ индекс+1 ДО длина-1 ( ЕСЛИ достать_из_массива(массив_чисел, внутренний_индекс) МЕНЬШЕ достать_из_массива(массив_чисел, минимальный_индекс) ТО ( минимальный_индекс = внутренний_индекс; ) ) ! Меняем местами найденный минимальный элемент с текущим ЕСЛИ минимальный_индекс НЕРАВНО индекс ТО ( ЗАДАТЬ временная_переменная = достать_из_массива(массив_чисел, индекс); изменить_в_массиве(массив_чисел, индекс, достать_из_массива(массив_чисел, минимальный_индекс)); изменить_в_массиве(массив_чисел, минимальный_индекс, временная_переменная); ) ) НАПЕЧАТАТЬ массив_чисел; )
Получается такое AST:
[ ["AssignField", "TARGET", "длина", "EXPR", [...]], ["AssignField", "TARGET", "минимальный_индекс", "EXPR", [Число(0)]], ["Loop", "FROM_EXPR", [Число(0)], "TO_EXPR", [Служебное имя: <длина>, Число(1), Служебное имя: <->], [ ["AssignOverrideVariable", "TARGET_EXPR", [...], "OVERRIDE_EXPR", [...]], ["Loop", "FROM_EXPR", [...], "TO_EXPR", [...], [ ["When", "EXPR", [...], [["AssignOverrideVariable", ...]] ] ] ], ["When", "EXPR", [...]], [["AssignField", ...], [...], [...]] ] ], ["Print", "EXPR", [Служебное имя: <массив_чисел>]] ]
Компилятор
После того как парсер построил AST, в дело вступает компилятор. Его задача — пройтись по всем узлам MetaObject, провалидировать типы (насколько это возможно в динамическом языке) и превратить их в исполняемые объекты.
В компиляторе всё довольно стандартно: рекурсивный обход дерева, таблица символов для областей видимости и проверка того, что оператор ПРОПУСТИТЬ или ПРЕРВАТЬ находится в теле цикла, а не где-то в другом месте. Или, например, что конструктор класса не пытается вернуть значение через ВЕРНУТЬ — такие штуки отлавливаются ещё на этапе компиляции.
Перед компиляцией пользовательского кода я регистрирую встроенные исключения языка — чтобы можно было кидать ОшибкаТипа или ДелениеНаНоль прямо из интерпретатора. Исключения хранятся глобально в константе EXCEPTIONS.
Ниже - главный метод компилятора. Сам компилятор занимает 600+ строк кода, поэтому опустим большую часть реализации. Покажу только основную логику:
def compile(self) -> Compiled: compiled_modules = {} # Регистрируем встроенные исключения языка for name, ex in EXCEPTIONS.items(): ex_def = create_define_class_wrap(ex) self.compiled[ex_def.name] = ex_def for idx, meta in enumerate(self.ast): compiled = self.execute_compile(meta) # Если скомпилировался целый модуль — сливаем его содержимое if isinstance(compiled, Compiled): compiled_modules = {**compiled_modules, **compiled.compiled_code} continue printer.logging(f"Команда компиляции №{idx + 1}", level="INFO") if compiled.name in self.compiled: printer.logging(f"Ошибка: {compiled.name} уже существует", level="ERROR") raise NameAlreadyExist(compiled.name, info=compiled.meta_info) self.compiled[compiled.name] = compiled printer.logging(f"Скомпилировано: {compiled.name}", level="INFO") # Собираем итоговый словарь: сначала импорты, потом наш код compiled_without_build_modules = self.compiled self.compiled = {**compiled_modules, **self.compiled} # Компилируем тела процедур и методов for name, compiled in compiled_without_build_modules.items(): if isinstance(compiled, Procedure): self.body_compile(compiled.body) if compiled.default_arguments is not None: self.compile_default_args(compiled.default_arguments) elif isinstance(compiled, ClassDefinition): self.body_compile(compiled.constructor.body) compiled.constructor.name = compiled.name for method in compiled.methods.values(): if method.default_arguments is not None: self.compile_default_args(method.default_arguments) self.body_compile(method.body) # Конструктор не должен возвращать значение for cmd in compiled.constructor.body.commands: if isinstance(cmd, Return): raise InvalidSyntaxError( f"Конструктор класса '{compiled.name}' не может содержать '{Tokens.return_}'", info=cmd.expression.meta_info ) return Compiled(self.compiled)
Обратите внимание на ветку if isinstance(compiled, Compiled). Это случай, когда мы компилируем не отдельную сущность, а целый импортированный модуль — его содержимое просто сливается с общим словарём скомпилированных объектов без проверок имен. Это нужно для того, чтобы в перспективе можно было переопределять процедуры импортируемых библиотек. Своего рода полиморфизим.
Интерпретатор
Интерпретатор делится на несколько исполнителей (Executor). У каждой грамматической конструкции есть своя реализация.
Класс Executor представляет собой по сути интерфейс:
class Executor(ABC): @abstractmethod def execute(self) -> BaseAtomicType: ...
Он не обладает никаким состоянием и поведением. Но выступает в качестве контракта.
Исполнителей в LawScript существует достаточно много, но два основных, которые являются точками входа в императивную и декларативную часть языка, я опишу:
ExecuteBlockExecutor- для входа в императивную частьCheckerSituationExecutor- для входа в декларативную часть
Их представления в коде LawScript:


Блок ВЫПОЛНИТЬ - это точка входа в контракт LawScript. Внутри данного блока разрешены только выражения: вызовы функций, арифметика и пр. Какие-то сложные грамматические конструкции по типу циклов, операторов ветвления или запуска фоновых задач (и такое есть, да), запрещены. ExecuteBlockExecutor просто итерируется по выражениям внутри себя и вызывает на каждое из них исполнитель выражений: ExpressionExecutor
CheckerSituationExecutor отвечает за декларативную часть — он проходит по юридическим проверкам и сопоставляет фактические ситуации с документами. Но, как я и обещал, не буду углубляться в это болото. Вернёмся к императивному коду.
Код ExecuteBlockExecutor достаточно прост, поэтому покажу его полностью:
class ExecuteBlockExecutor(Executor): def __init__(self, execute_block: 'ExecuteBlock', compiled: 'Compiled'): self.execute_block = execute_block self.compiled = compiled def execute(self) -> BaseAtomicType: for expression in self.execute_block.expressions: scope_stack = ScopeStack() for object_name, value in self.compiled.compiled_code.items(): scope_stack.set(Variable(object_name, value)) expression_executor = ExpressionExecutor(expression, scope_stack, self.compiled) expression_executor.execute() return VOID
В языке есть специальное значение VOID — аналог None в Python или void в других языках. Оно возвращается, когда выражение или блок не производит полезного результата.
Как устроен VOID под капотом
class Void(BaseAtomicType): def __init__(self): super().__init__(None) @classmethod def type_name(cls): return "Пустота" def __str__(self) -> str: return Tokens.void VOID: Final[Void] = Void()
Простая обертка :)
Что ещё умеет LawScript?
Думаю, логика работы языка понятна. Чтобы не перегружать и так уже объемную статью отмечу тезисно пару фич языка, которыми я особенно горжусь:
1. Честное ООП с одиночным наследованием
Классы, методы, конструкторы — всё как у взрослых. Можно наследоваться от другого класса и переопределять методы. Множественное наследование не завёз — так как приводит к спагетти-коду.
2. Асинхронность
Да, в моём велосипеде есть своя асинхронность! Под капотом — планировщик с пулом потоков и циклом событий в каждом. Задачи переключаются через механизм, похожий на yield from. Можно запускать фоновые задачи прямо из кода LawScript.
ОПРЕДЕЛИТЬ ПРОЦЕДУРУ фоновая_задача(номер) ( НАПЕЧАТАТЬ форматировать_строку("Задача номер {}, делает запрос в интернет", номер); ЗАДАТЬ результат = запрос_в_интернет("GET", "https://ya.ru", таблица()); НАПЕЧАТАТЬ форматировать_строку("Задача номер {}, выполнена! Статус: {}", номер, извлечь_из_таблицы(результат, "статус_код")); ) ОПРЕДЕЛИТЬ ПРОЦЕДУРУ главная() ( ЗАДАТЬ задачи = Список(); ЦИКЛ номер ОТ 1 ДО 25 ( задачи:добавить(В ФОНЕ фоновая_задача(номер)); ) ждать_всех(задачи:в_массив()); ) ВЫПОЛНИТЬ ( главная(); )
3. FFI — интеграция с Python в обе стороны
Самая полезная фича для реального применения. Через файлы .pyl можно писать расширения на чистом Python и вызывать их из LawScript как обычные функции. Надо просто реализовать класс, который наследуется от PyExtendWrapper и реализовать в нем метод call!
Но самое интересное — интеграция работает в обе стороны. При желании можно передать в PyExtendWrapper ссылку на LawScript-процедуру и вызвать её прямо из Python-кода через метод run_procedure. Это открывает широчайшие возможности: например, библиотека на Python может дёргать callback, написанный на LawScript, когда происходит какое-то событие.
Именно так я прикрутил Telegram Bot API и графику для игры — всё, что нельзя или неудобно писать на самом LawScript, выносится в Python-расширения.
4. Подробная обработка ошибок
Я постарался сделать сообщения об ошибках максимально дружелюбными. Интерпретатор показывает не только строку, но и конкретную позицию, где возникла проблема — со стрелочкой, как в Rust или современном Python. Чтобы было понятно не только что сломалось, но и где именно. В некоторых случаях интерпретатор даже понимает, что вы хотели написать, и подсказывает правильный вариант.
ОПРЕДЕЛИТЬ ПРОЦЕДУРУ главная() ( ЗАДАТЬ тест; тест1; ) ВЫПОЛНИТЬ ( главная(); )

Гвоздь программы...
Наконец-то переходим к тому, ради чего мы собственно собрались! Я действительно настолько увлекся данным пет-проектом, что написал на нем во-первых, игру, а во-вторых, телеграм-бота :)))
Игра
Для работы с графикой я обернул библиотеку Pygame. Дело это было не простое — пришлось прокидывать классы Pygame в рантайм LawScript. Но, слава Богу, всё решилось простыми обёртками.
Суть механизма проста: каждый тип из Pygame наследуется от CustomType — базового класса для всех пользовательских типов в LawScript. В конструкторе мы сохраняем оригинальный объект Pygame, а в словаре fields описываем его свойства, доступные из кода на LawScript. Обратите внимание на русскоязычные названия полей — ширина, высота, клавиша, это_выход. Именно так к ним обращается программист на LawScript.
Обёртки типов Pygame (GameScreen, GameEvent, GameImage и другие)
import pygame from src.core.types.atomic import CustomType, Number, Array, Boolean class GameScreen(CustomType): def __init__(self, screen): super().__init__() self.screen = screen def eq(self, other: 'GameScreen'): if isinstance(other, GameScreen): return self.screen == other.screen return False def __str__(self) -> str: return "ИгровоеОкно" @classmethod def type_name(cls): return "ИгровоеОкно" class GameEventType(CustomType): def __init__(self, type_): super().__init__(type_) self.type = type_ def eq(self, other: 'GameEventType'): if isinstance(other, GameEventType): return self.type == other.type return False def __str__(self) -> str: return str(self.value) @classmethod def type_name(cls): return "ТипСобытия" class GameEvent(CustomType): def __init__(self, event): super().__init__() self.event = event self.fields = { "тип": GameEventType(event.type), "это_выход": Boolean(self.event.type == pygame.QUIT) } if event.type == pygame.KEYDOWN or event.type == pygame.KEYUP: self.fields["клавиша"] = Number(event.key) elif event.type == pygame.MOUSEBUTTONDOWN or event.type == pygame.MOUSEBUTTONUP: self.fields["кнопка"] = Number(event.button) self.fields["позиция"] = Array([Number(event.pos[0]), Number(event.pos[1])]) elif event.type == pygame.MOUSEMOTION: self.fields["позиция"] = Array([Number(event.pos[0]), Number(event.pos[1])]) self.fields["относительно"] = Array([Number(event.rel[0]), Number(event.rel[1])]) def eq(self, other: 'GameEvent'): if isinstance(other, GameEvent): return self.event == other.event return False def __str__(self) -> str: return "Событие" @classmethod def type_name(cls): return "Событие" class GameImage(CustomType): def __init__(self, image): super().__init__() self.image = image self.fields = { "ширина": Number(image.get_width()), "высота": Number(image.get_height()) } def eq(self, other: 'GameImage'): if isinstance(other, GameImage): return self.image == other.image return False def __str__(self) -> str: return "Картинка" @classmethod def type_name(cls): return "Картинка" class GameRect(CustomType): def __init__(self, rect): super().__init__() self.rect = rect self.fields = { "x": Number(rect.x), "y": Number(rect.y), "ширина": Number(rect.width), "высота": Number(rect.height), "центр_x": Number(rect.centerx), "центр_y": Number(rect.centery), "верх": Number(rect.top), "низ": Number(rect.bottom), "лево": Number(rect.left), "право": Number(rect.right) } def eq(self, other: 'GameRect'): if isinstance(other, GameRect): return self.rect == other.rect return False def __str__(self) -> str: return "Прямоугольник" @classmethod def type_name(cls): return "Прямоугольник" class GameText(CustomType): def __init__(self, text_surface): super().__init__() self.text_surface = text_surface self.fields = { "ширина": Number(text_surface.get_width()), "высота": Number(text_surface.get_height()) } def eq(self, other: 'GameText'): if isinstance(other, GameText): return self.text_surface == other.text_surface return False def __str__(self) -> str: return "Текст" @classmethod def type_name(cls): return "Текст"
Точно также я обернул функции Pygame:
Обёртки функций Pygame
from typing import Optional from pathlib import Path from src.core.extend.function_wrap import PyExtendWrapper, PyExtendBuilder from src.core.types.basetype import BaseAtomicType builder = PyExtendBuilder() standard_lib_path = f"{Path(__file__).resolve().parent.parent}/modules/_/" MOD_NAME = "game" @builder.collect(func_name='_инициализация_игрового_движка') class Init(PyExtendWrapper): def __init__(self, func_name: str): super().__init__(func_name) self.empty_args = True self.count_args = 0 def call(self, args: Optional[list[BaseAtomicType]] = None): import pygame from src.core.types.atomic import VOID pygame.init() return VOID @builder.collect(func_name='_создать_окно') class CreateScreen(PyExtendWrapper): def __init__(self, func_name: str): super().__init__(func_name) self.count_args = 2 def call(self, args: Optional[list[BaseAtomicType]] = None): import pygame from src.core.extend.standard_lib.lib_game.util import GameScreen from src.core.types.atomic import Number from src.core.exceptions import ErrorType wight = args[0] height = args[1] if not isinstance(wight, Number): raise ErrorType(f"Первый аргумент должен иметь тип '{Number.type_name()}'!") if not isinstance(height, Number): raise ErrorType(f"Второй аргумент должен иметь тип '{Number.type_name()}'!") screen = pygame.display.set_mode((wight.value, height.value)) real_screen = GameScreen(screen) return real_screen def build_module(): builder.build_python_extend(f"{standard_lib_path}{MOD_NAME}") if __name__ == '__main__': build_module()
Тут только часть
И уже после этого, я сделал отдельный модуль, в котором написал простой класс Игра на LawScript с игровым циклом. Код данного класса я опущу. Зато покажу код простой игрушки) Которую я, к слову, навайбкодил :3
Игра "Поймай еду"
ВКЛЮЧИТЬ стандартная_библиотека.игры ВКЛЮЧИТЬ стандартная_библиотека.рандом ОПРЕДЕЛИТЬ КЛАСС ЛовляЕды ( ОПРЕДЕЛИТЬ КОНСТРУКТОР (ссылка) () ( ссылка:игра = Игра("Поймай еду"); ссылка:окно = ссылка:игра:создать_экран(800, 600); ссылка:генератор = ГенераторСлучайныхЧисел(); ссылка:счет = 0; ссылка:игра_окончена = ЛОЖЬ; ссылка:время_жизни = 100; ссылка:еда_x = ссылка:генератор:получить_целое_в_диапазоне(50, 750); ссылка:еда_y = ссылка:генератор:получить_целое_в_диапазоне(50, 550); ссылка:размер_еды = 30; ЗАДАТЬ обновление = ссылка:обновление; ЗАДАТЬ отрисовка = ссылка:отрисовка; ссылка:игра:игровой_цикл(обновление, отрисовка, 60); ) ОПРЕДЕЛИТЬ МЕТОД (ссылка) обновление(дельта, события) ( ЕСЛИ ссылка:игра_окончена ТО ( ЕСЛИ ссылка:игра:нажата_клавиша_по_имени("ПРОБЕЛ") ТО ( ссылка:перезапустить(); ) ВЕРНУТЬ; ) ссылка:время_жизни = ссылка:время_жизни - 0.7; ЕСЛИ ссылка:время_жизни МЕНЬШЕ 0 ТО ( ссылка:игра_окончена = ИСТИНА; ВЕРНУТЬ; ) ЗАДАТЬ мышь = ссылка:игра:получить_позицию_мыши(); ЗАДАТЬ mx = достать_из_массива(мышь, 0); ЗАДАТЬ my = достать_из_массива(мышь, 1); ЕСЛИ ссылка:игра:нажата_кнопка_мыши(1) ТО ( ЗАДАТЬ dx = mx - ссылка:еда_x; ЗАДАТЬ dy = my - ссылка:еда_y; ЗАДАТЬ dist = корень(dx * dx + dy * dy); ЕСЛИ dist МЕНЬШЕ ссылка:размер_еды ТО ( ссылка:счет = ссылка:счет + 1; ссылка:время_жизни = ссылка:время_жизни + 30; ЕСЛИ ссылка:время_жизни БОЛЬШЕ 100 ТО ( ссылка:время_жизни = 100; ) ссылка:еда_x = ссылка:генератор:получить_целое_в_диапазоне(50, 750); ссылка:еда_y = ссылка:генератор:получить_целое_в_диапазоне(50, 550); ) ) ) ОПРЕДЕЛИТЬ МЕТОД (ссылка) отрисовка() ( ссылка:игра:залить_экран(массив(30, 30, 50)); ЕСЛИ ссылка:игра_окончена ТО ( ЗАДАТЬ текст = ссылка:игра:создать_текст(форматировать_строку("Игра окончена! Счет: {}", ссылка:счет), 48, массив(255, 255, 255)); ссылка:игра:отобразить_текст(текст, 200, 250); ЗАДАТЬ рестарт = ссылка:игра:создать_текст("ПРОБЕЛ - заново", 24, массив(200, 200, 200)); ссылка:игра:отобразить_текст(рестарт, 280, 320); ВЕРНУТЬ; ) ! Шкала времени ЗАДАТЬ ширина_шкалы = 400 * ссылка:время_жизни / 100; ЗАДАТЬ шкала = ссылка:игра:создать_прямоугольник(200, 20, ширина_шкалы, 20); ссылка:игра:нарисовать_прямоугольник(шкала, массив(255, 100, 100), 0); ! Еда ЗАДАТЬ размер = ссылка:размер_еды; ссылка:игра:нарисовать_круг(массив(255, 255, 0), ссылка:еда_x, ссылка:еда_y, размер); ссылка:игра:нарисовать_круг(массив(255, 100, 0), ссылка:еда_x - 5, ссылка:еда_y - 5, размер / 3); ! Счет ЗАДАТЬ текст = ссылка:игра:создать_текст(форматировать_строку("Счет: {}", ссылка:счет), 32, массив(255, 255, 255)); ссылка:игра:отобразить_текст(текст, 10, 10); ! Прицел ЗАДАТЬ мышь = ссылка:игра:получить_позицию_мыши(); ЗАДАТЬ mx = достать_из_массива(мышь, 0); ЗАДАТЬ my = достать_из_массива(мышь, 1); ссылка:игра:нарисовать_линию(массив(255, 255, 255), mx - 10, my, mx + 10, my); ссылка:игра:нарисовать_линию(массив(255, 255, 255), mx, my - 10, mx, my + 10); ) ОПРЕДЕЛИТЬ МЕТОД (ссылка) перезапустить() ( ссылка:счет = 0; ссылка:игра_окончена = ЛОЖЬ; ссылка:время_жизни = 100; ссылка:еда_x = ссылка:генератор:получить_целое_в_диапазоне(50, 750); ссылка:еда_y = ссылка:генератор:получить_целое_в_диапазоне(50, 550); ) ) ОПРЕДЕЛИТЬ ПРОЦЕДУРУ корень(x) ( ВЕРНУТЬ x ^ 0.5; ) ВЫПОЛНИТЬ ( ЛовляЕды(); )

Телеграм бот
А чтобы доказать, что на LawScript можно писать не только игрушки, но и что-то приближенное к реальности, я запилил Telegram-бота.
Бот умеет:
Отвечать на команду
/startПоддерживать "светскую" беседу на темы «приветствия» и «как дела»
Работать в режиме long polling (постоянный опрос сервера Telegram)
Обрабатывать ошибки сети и переподключаться
Под капотом — всё те же .pyl расширения. HTTP-запросы к Telegram API я обернул в функцию запрос_в_интернет, а всю бизнес-логику написал уже на чистом LawScript.
Вот как выглядит основной модуль бота:
Основной модуль Telegram-бота на LawScript
ВКЛЮЧИТЬ стандартная_библиотека.интернет ВКЛЮЧИТЬ стандартная_библиотека.время ВКЛЮЧИТЬ стандартная_библиотека.рандом ВКЛЮЧИТЬ стандартная_библиотека.строки ВКЛЮЧИТЬ стандартная_библиотека.конкурентность ВКЛЮЧИТЬ стандартная_библиотека.ввод_вывод ВКЛЮЧИТЬ стандартная_библиотека.структуры.* ВКЛЮЧИТЬ config.* ВКЛЮЧИТЬ bot.updater ВКЛЮЧИТЬ bot.sender ВКЛЮЧИТЬ handlers ОПРЕДЕЛИТЬ ПРОЦЕДУРУ главная() ( ЗАДАТЬ фоновые_задачи = Список(); ЗАДАТЬ события = Очередь(100); ЗАДАТЬ настройки = Настройки(); ЗАДАТЬ ТОКЕН = настройки:токен; ЗАДАТЬ БАЗОВЫЙ_АДРЕС = форматировать_строку("{}{}", настройки:адрес, ТОКЕН); ЗАДАТЬ актуализатор = Актуализатор(БАЗОВЫЙ_АДРЕС, ТОКЕН); фоновые_задачи:добавить(В ФОНЕ актуализатор:ожидание_новых_событий(события)); фоновые_задачи:добавить(В ФОНЕ обработчик_текста(БАЗОВЫЙ_АДРЕС, события)); НАПЕЧАТАТЬ "Бот запущен! И ждет сообщений..."; ждать_всех(фоновые_задачи:в_массив()); ) ВЫПОЛНИТЬ ( главная(); )
Обработчик сообщений реализует простую, но расширяемую логику:
Обработчик текстовых сообщений
ОПРЕДЕЛИТЬ ПРОЦЕДУРУ _случайный_ответ(ответы, генератор_рандома) ( ВЕРНУТЬ достать_из_массива(ответы, генератор_рандома:получить_целое_в_диапазоне(0, длина_массива(ответы) - 1)); ) ОПРЕДЕЛИТЬ КЛАСС ТекстовыеОтветы ( ОПРЕДЕЛИТЬ КОНСТРУКТОР (ссылка) () ( ссылка:приветствия = массив("И тебе привет!", "Привет!", "Приветик", "Приветос"); ссылка:как_дела = массив("Хорошо", "Отлично", "Прекрасно, а у Вас?", "Оки-доки"); ) ) ОПРЕДЕЛИТЬ ПРОЦЕДУРУ обработчик_текста(адрес, очередь_событий) ( ЗАДАТЬ сообщение; ЗАДАТЬ идентификатор_чата; ЗАДАТЬ текст; ЗАДАТЬ ответ; ЗАДАТЬ ответы = ТекстовыеОтветы(); ЗАДАТЬ генератор_рандома = ГенераторСлучайныхЧисел(); ЗАДАТЬ темы_разговора = массив("приветствия", "как дела"); ПОКА ИСТИНА ( КОНТЕКСТ ( сообщение = очередь_событий:взять(); ) ОБРАБОТЧИК ОчередьПуста КАК _ ( ЖДАТЬ В ФОНЕ асинхронный_сон(0.1); ПРОПУСТИТЬ; ) идентификатор_чата = извлечь_из_таблицы(извлечь_из_таблицы(сообщение, "chat"), "id"); ЕСЛИ НЕ есть_ключ_в_таблице(сообщение, "text") ТО ( В ФОНЕ отправить_сообщение(адрес, идентификатор_чата, "Я понимаю только текст :("); ПРОПУСТИТЬ; ) текст = извлечь_из_таблицы(сообщение, "text"); ЕСЛИ текст РАВНО "/start" ТО ( ответ = форматировать_строку("Привет! Я бот, который умеет говорить на следующие темы: {}", темы_разговора); ) ИНАЧЕ ЕСЛИ входит_в_строку(нижний_регистр(текст), "прив") ИЛИ входит_в_строку(нижний_регистр(текст), "здрав") ТО ( ответ = _случайный_ответ(ответы:приветствия, генератор_рандома); ) ИНАЧЕ ЕСЛИ входит_в_строку(нижний_регистр(текст), "как дел") ТО ( ответ = _случайный_ответ(ответы:как_дела, генератор_рандома); ) ИНАЧЕ ( ответ = форматировать_строку("Я Вас не понял, поэтому отвечу как эхо-бот :) Эхо: '{}'\n\nНапомню, что я могу говорить на следующие темы: {}", текст, темы_разговора); ) В ФОНЕ отправить_сообщение(адрес, идентификатор_чата, ответ); ) )
А вот класс Актуализатор, который отвечает за long polling — постоянный опрос сервера Telegram на предмет новых сообщений:
Long polling на LawScript
ОПРЕДЕЛИТЬ КЛАСС Актуализатор ( ОПРЕДЕЛИТЬ КОНСТРУКТОР (ссылка) (базовый_адрес, токен="ВАШ_ТОКЕН", интервал_опроса=0.5) ( ссылка:_токен = токен; ссылка:_базовый_адрес = базовый_адрес; ссылка:_интервал_опроса = интервал_опроса; ) ОПРЕДЕЛИТЬ МЕТОД (ссылка) ожидание_новых_событий(очередь_событий) ( ЗАДАТЬ сдвиг = 0; ЗАДАТЬ обновления; ЗАДАТЬ номер_обновления; ЗАДАТЬ события; ЗАДАТЬ событие; ПОКА ИСТИНА ( обновления = ссылка:получить_событие(сдвиг); ЕСЛИ НЕ извлечь_из_таблицы(обновления, "ok") ТО ( НАПЕЧАТАТЬ форматировать_строку("Произошла ошибка: {}", извлечь_из_таблицы(обновления, "description")); ПРОПУСТИТЬ; ) события = извлечь_из_таблицы(обновления, "result"); НАПЕЧАТАТЬ "получены события"; ЦИКЛ счет ОТ 0 ДО длина_массива(события) - 1 ( событие = достать_из_массива(события, счет); сдвиг = извлечь_из_таблицы(событие, "update_id") + 1; НАПЕЧАТАТЬ форматировать_строку("Обработка события со сдвигом: {}", сдвиг); ЕСЛИ есть_ключ_в_таблице(событие, "message") ТО ( ЗАДАТЬ сообщение = извлечь_из_таблицы(событие, "message"); ПОКА ИСТИНА ( КОНТЕКСТ ( очередь_событий:положить(сообщение); ПРЕРВАТЬ; ) ОБРАБОТЧИК ОчередьПолна КАК _ ( НАПЕЧАТАТЬ форматировать_строку("Очередь на обработку полна. Повторяю попытку запланировать событие: {}", сдвиг); ЖДАТЬ В ФОНЕ асинхронный_сон(0.5); ) ) ) ) ЖДАТЬ В ФОНЕ асинхронный_сон(ссылка:_интервал_опроса); ) ) ОПРЕДЕЛИТЬ МЕТОД (ссылка) получить_событие(сдвиг=ПУСТОТА) ( ЗАДАТЬ адрес = форматировать_строку("{}/getUpdates?timeout=30", ссылка:_базовый_адрес); ЕСЛИ сдвиг НЕРАВНО ПУСТОТА ТО ( адрес = форматировать_строку("{}&offset={}", адрес, сдвиг); ) ЗАДАТЬ ответ; ПОКА ИСТИНА ( КОНТЕКСТ ( ответ = запрос_в_интернет("GET", адрес, таблица()); ПРЕРВАТЬ; ) ОБРАБОТЧИК ОшибкаПротоколаПередачиГиперТекста КАК _ ( НАПЕЧАТАТЬ "Произошла ошибка, при попытке обновить данные. Инициализация следующей попытки..."; ЖДАТЬ В ФОНЕ асинхронный_сон(1); ) ) ВЕРНУТЬ извлечь_из_таблицы(ответ, "json"); ) )
И, конечно, куда без тестов! Я написал небольшой тестовый модуль с моками, чтобы проверять логику бота, не дёргая реальный Telegram:
Тесты для бота
ВКЛЮЧИТЬ стандартная_библиотека.тесты ВКЛЮЧИТЬ стандартная_библиотека.время ВКЛЮЧИТЬ стандартная_библиотека.рандом ВКЛЮЧИТЬ стандартная_библиотека.структуры.* ВКЛЮЧИТЬ bot.sender ВКЛЮЧИТЬ config.* ВКЛЮЧИТЬ handlers ! Мок ОПРЕДЕЛИТЬ ПРОЦЕДУРУ запрос_в_интернет(метод, адрес, данные) ( ВЕРНУТЬ таблица(); ) ! Мок ОПРЕДЕЛИТЬ ПРОЦЕДУРУ прочитать_файл(путь_до_файла) ( ВЕРНУТЬ массив("ТОКЕН=123", "АДРЕС=321"); ) ОПРЕДЕЛИТЬ ПРОЦЕДУРУ запуск() ( ЗАДАТЬ тестовый_сценарий = ТестовыйСценарий(); тестовый_сценарий:простой_тест(отправить_сообщение("127.0.0.1", 1337, "тест") РАВНО таблица()); ЗАДАТЬ ключи_файла = массив("ТОКЕН", "АДРЕС"); ЗАДАТЬ значения_файла = массив("123", "321"); тестовый_сценарий:простой_тест(парсер() РАВНО таблица(ключи_файла, значения_файла)); ЗАДАТЬ настройки = Настройки(); тестовый_сценарий:простой_тест((настройки:токен РАВНО "123") И (настройки:адрес РАВНО "321")); ) ВЫПОЛНИТЬ ( запуск(); )

Что в итоге?
Я написал язык программирования на Python, с русскоязычным синтаксисом, классами, асинхронностью и FFI. А потом сделал на нём:
Игру «Поймай еду» — с графикой на Pygame, обработкой клавиатуры и игровым циклом.
Telegram-бота — с long polling, очередями сообщений, парсингом конфигов и тестами.
Всё это — работающий код, который можно потрогать и запустить.
Зачем я это сделал? Потому что это прикольно :3
Ссылка на исходники