Привет, Хабр! Меня зовут SlywerX, я студент 3 курса кафедры Программной инженерии МТУ (Алматы) и fullstack-разработчик. Несколько месяцев назад я задался вопросом: а как вообще работают языки программирования изнутри? Лучший способ разобраться — написать свой. Так появился SWX (Shadow Web eXploit) — скриптовый язык на базе Node.js с собственным синтаксисом, криптографией и даже HTML-рендерингом.
Сейчас SWX на версии 7.0.0. В этой статье расскажу как всё устроено, что было сложно и зачем это вообще нужно было делать.
Зачем писать свой язык?
Честный ответ: из любопытства. Я работаю с JavaScript каждый день, но никогда не понимал что происходит до того как код начинает выполняться. Как интерпретатор понимает что x = 5 это присвоение, а не сравнение? Как работает AST? Что такое лексер?
Все эти вопросы привели меня к проекту decSec и языку SWX. Никакого практического смысла — чистое желание разобраться изнутри.
Архитектура: как устроен SWX
Любой язык программирования проходит через несколько этапов обработки кода. В SWX их три:
Исходный код → [Лексер] → Токены → [Парсер] → AST → [Интерпретатор] → Результат
1. Лексер (lexer.js)
Лексер — это первый этап. Он берёт строку текста и разбивает её на токены — атомарные единицы языка.
Например, строка:
swx x = 42
Превращается в токены:
KEYWORD(swx) IDENT(x) ASSIGN(=) NUMBER(42)
Самое сложное здесь — правильно определить границы токенов. Например, >= это один токен, а не > и = по отдельности. В SWX я использую кастомные операторы типа >~ (больше или равно) и =? (равенство), что потребовало аккуратной обработки многосимвольных последовательностей.
2. Парсер (parser.js)
Парсер берёт токены и строит AST (Abstract Syntax Tree) — дерево которое описывает структуру программы.
Вот как выглядит AST для простого условия:
swx x = 10 sw x >~ 5 { wx "больше пяти" }
json
{ "type": "IfStatement", "condition": { "type": "BinaryExpression", "operator": ">=", "left": { "type": "Identifier", "name": "x" }, "right": { "type": "NumberLiteral", "value": 5 } }, "body": [ { "type": "PrintStatement", "value": { "type": "StringLiteral", "value": "больше пяти" } } ] }
Парсер — самая сложная часть проекта. Особенно рекурсивный спуск для выражений с приоритетами операторов. Например, 2 + 3 4 должно вычисляться как 2 + (3 4), а не (2 + 3) * 4.
3. Интерпретатор (interpreter.js)
Интерпретатор обходит AST и выполняет каждый узел дерева. Это называется tree-walking interpreter.
Для каждого типа узла есть своя функция:
IfStatement→ проверяет условие, выполняет нужную веткуForLoop→ итерирует нужное количество разFunctionDeclaration→ сохраняет функцию в таблице символовBinaryExpression→ вычисляет арифметику/логику
Синтаксис SWX — как это выглядит
Я намеренно сделал синтаксис непохожим на существующие языки. Частично для того чтобы разобраться в парсинге нестандартных конструкций, частично — просто для фана.
Переменные и вывод
swx name = "SlywerX" swx age = 20 wx "Привет, {name}! Тебе {age} лет."
Условия
sw age >~ 18 { wx "совершеннолетний" } dr sw age >~ 13 { wx "подросток" } dr { wx "ребёнок" }
Циклы
xs 5 { wx "итерация {_i}" } // повторить 5 раз swx arr = ["a", "b", "c"] xw item in arr { wx item } // foreach xl x > 0 { x -= 1 } // while
Функции
sx add(a, b = 0) => { ws a + b } swx result = add(3, 4) wx result // 7
Pattern matching (v7.0.0)
match status { case 200 => { wx "OK" } case 404 => { wx "Not Found" } default => { wx "Unknown" } }
Pipe оператор
swx result = "hello" |> upper // HELLO swx n = arr |> len // длина массива
Встроенная криптография (#x)
Одна из фишек SWX — встроенный криптографический модуль на базе Node.js crypto. Без внешних зависимостей.
swx hash = #x.sha256("hello") wx hash // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 swx encrypted = #x.aes_enc("secret data", "mykey") swx decrypted = #x.aes_dec(encrypted, "mykey") swx keys = #x.rsa_keys(2048) swx signature = #x.rsa_sign("data", keys.private) wx #x.rsa_verify("data", signature, keys.public) // true
HTML-рендеринг (sx>)
Ещё одна нестандартная фича — встроенный DSL для генерации HTML прямо из SWX кода:
sx> "output/index.html" { @style { body >> bg(#030810) color(#fff) font(Courier New, monospace) h1 >> color(#00e5ff) size(24px) bold uppercase .btn >> bg(#00e5ff) color(#000) pad(10px 20px) pointer } @body { div.card >> { h1 >> "Привет от SWX" button.btn >> (onclick: "alert('работает')") { "Нажми" } } } }
Это генерирует полноценный HTML файл с встроенными стилями. Своеобразный Tailwind на минималках.
Что было сложно
1. Приоритеты операторов в парсере Реализовать правильный порядок вычислений через рекурсивный спуск — нетривиальная задача. Пришлось несколько раз переписывать логику.
2. Конфликты в лексере Кастомные операторы типа *~ (умножение с присвоением) конфликтовали с CSS-идентификаторами в HTML-рендерере. Решил через контекстный режим лексера.
3. While-цикл (xl) Бесконечные циклы роняли процесс. Добавил лимит в 1 миллион итераций с понятной ошибкой.
4. Стандартная библиотека Писать stdlib на самом SWX когда интерпретатор ещё не стабилен — это как чинить самолёт в полёте. Но именно это лучше всего показывало баги.
Что дальше
#aiмодуль — интеграция с LLM API прямо из SWXКомпилятор в байткод вместо tree-walking интерпретатора (для скорости)
Пакетный менеджер для SWX модулей
Подсветка синтаксиса для VS Code
Выводы
Писать свой язык — это лучший способ понять как работают чужие. После этого проекта я совершенно по-другому смотрю на JavaScript, Python и любой другой язык. Каждый if, каждый for — это узел в AST который кто-то написал руками.
Если вы хотите реально понять программирование изнутри — попробуйте написать хотя бы простой интерпретатор. Даже калькулятор с переменными многому научит.
Буду рад вопросам и критике в комментариях!
SlywerX — Fullstack Developer, студент METU, кафедра Программной инженерии
