Skip to main content

Архитектура Expression Parser


Архитектура Expression Parser

Как устроен модуль @lowcode/expression-parser: парсинг выражений, AST, типизация, валидация и интеграция с DSL/AST.

🎯 Цель страницы

Эта страница описывает внутреннее устройство пакета @lowcode/expression-parser:

  • формат Expression AST и поддерживаемый синтаксис;
  • работу парсера на базе jsep и плагинов;
  • утилиты обхода, сбора идентификаторов и печати выражений;
  • систему типизации (ExpressionContext, ExpressionPrimitiveType);
  • валидатор выражений и типовые сценарии его использования;
  • взаимодействие с @lowcode/dsl и @lowcode/dsl-compiler.

Справочник по модулю:

  • Общий обзор: @lowcode/expression-parser
  • AST-типы: ExpressionNode, LiteralExpression, MemberExpressionast/nodes.ts
  • Парсинг строк: parseExpressionparser/parse.ts
  • Обход AST: traverseutils/traverse.ts
  • Сбор идентификаторов/путей: collectIdentifiers, collectMemberPathsutils/collect.ts
  • Печать AST: expressionToStringutils/print.ts
  • Типизация: ExpressionContext, ExpressionPrimitiveTypeanalyzer/types.ts
  • Вывод типов: inferExpressionTypeanalyzer/inferType.ts
  • Валидация: validateExpressionTypeanalyzer/validate.ts

1. Роль Expression Parser в платформе

@lowcode/expression-parser — это общий «движок выражений» для всей Lowcode Platform. Он решает сразу несколько задач:

  1. парсит строковые выражения DSL в нормализованное AST;
  2. предоставляет утилиты обхода и обработки дерева выражения;
  3. умеет выводить примитивный тип результата выражения с учётом контекста (state, props, dataSources);
  4. проверяет корректность выражений и возвращает диагностические сообщения;
  5. служит базой для редактора (builder-web), компилятора (dsl-compiler) и runtime-хостинга.

Модуль не привязан к конкретной реализации DSL, он оперирует только выражениями и абстрактным контекстом. Благодаря этому его можно использовать во всех слоях:

  • builder-web — для подсветки ошибок и подсказок в PropertiesPanel;
  • dsl-compiler — для нормализации ExpressionValue и статического анализа;
  • backend/runtime — для безопасной проверки логики перед исполнением.

Высокоуровневая схема использования:

graph TD
SRC[Строка выражения<br/>"state.count + 1"] --> PARSE[parseExpression]
PARSE --> AST[Expression AST]
AST --> TYPE[inferExpressionType<br/>ExpressionContext]
AST --> VALIDATE[validateExpressionType]
AST --> UTILS[collectMemberPaths<br/>expressionToString]

2. Структура каталогов @lowcode/expression-parser

Базовая структура в packages/expression-parser/src/:

ast/                 ← описание узлов Expression AST
nodes.ts

parser/ ← парсер строковых выражений
parse.ts ← jsep → Expression AST + ошибки
jsep-config.ts ← конфигурация jsep, подключение плагинов

analyzer/ ← типизация и валидация
types.ts ← ExpressionPrimitiveType, ExpressionContext, ValidationIssue
inferType.ts ← inferExpressionType(ast, context)
validate.ts ← validateExpressionType(ast, context, expectedType)

utils/ ← утилиты обхода и манипуляции AST
traverse.ts ← обход дерева (enter/leave)
collect.ts ← collectIdentifiers, collectMemberPaths, buildMemberPath
print.ts ← expressionToString

errors/
SyntaxError.ts ← ExpressionSyntaxError

index.ts ← публичный API пакета

Структура tests/ зеркалирует src/ и содержит отдельные тесты для парсера, AST, анализатора и утилит.


3. Expression AST

AST выражений — это подмножество JavaScript-выражений, нормализованное для задач DSL.

3.1. Базовый интерфейс узлов

Все узлы наследуют базовый интерфейс:

interface BaseExpression {
type: string;
start?: number;
end?: number;
}

Поля start/end соответствуют позициям в исходной строке и используются builder-web для подсветки ошибок и навигации курсора.

3.2. Основные типы узлов

Поддерживаются следующие узлы:

  • LiteralExpression — литералы: строки, числа, boolean, null;
  • IdentifierExpression — идентификаторы: state, props, user;
  • ThisExpression — ключевое слово this;
  • MemberExpression — доступ к свойству: obj.prop, obj[expr];
  • CallExpression — вызов функции: fn(a, b);
  • BinaryExpression — бинарные операции: +, -, *, /, %, ==, >= и т.д.;
  • LogicalExpression — логические операции: &&, ||, ??;
  • UnaryExpression — унарные операции: !a, -n, typeof value;
  • ConditionalExpression — тернарный оператор: cond ? a : b;
  • ArrayExpression — массивы: [1, state.count];
  • ObjectExpression / ObjectProperty — объекты: { a: 1, b: state.count }.

Полный тип объединения:

export type ExpressionNode =
| LiteralExpression
| IdentifierExpression
| ThisExpression
| MemberExpression
| CallExpression
| BinaryExpression
| LogicalExpression
| ConditionalExpression
| UnaryExpression
| ArrayExpression
| ObjectExpression
| ObjectProperty;

AST устроено так, чтобы быть:

  • достаточно выразительным для большинства UI-логики;
  • достаточно простым для безопасного анализа и генерации кода;
  • независимым от конкретного runtime и DSL.

4. Парсер (parser/)

4.1. jsep и плагин Object

В качестве основы используется библиотека jsep — небольшой парсер JS-выражений. Дополнительно подключается плагин @jsep-plugin/object, который добавляет поддержку объектных литералов { a: 1, b: 2 }.

Конфигурация выполняется в jsep-config.ts:

  • регистрация плагина object;
  • добавление дополнительных операторов (например, ??);
  • при необходимости — подключение других плагинов.

4.2. Функция parseExpression

Главная точка входа парсера:

interface ParseResult {
ast: ExpressionNode | null;
errors: string[];
}

function parseExpression(source: string): ParseResult;

Алгоритм:

  1. вызывает jsep(source) и получает jsep-дерево;
  2. рекурсивно конвертирует его в ExpressionNode (см. convert(...) в parse.ts);
  3. переносит start/end из узлов jsep в Expression AST;
  4. при любой синтаксической ошибке возвращает ast = null и массив errors.

Парсер ничего не знает о DSL, state, props и других сущностях — он работает только со строкой и синтаксисом выражений.


5. Утилиты над AST (utils/)

5.1. traverse

traverse(node, visitor) — универсальный обход дерева:

traverse(ast, {
enter(node, parent) {
// действия при входе в узел
},
leave(node, parent) {
// действия при выходе из узла
},
});

Поддерживаются все типы узлов AST, включая массивы и объектные литералы. Эта утилита используется во всех остальных модулях (collect, validate), а также может применяться снаружи для анализа выражений.

5.2. collectIdentifiers и collectMemberPaths

  • collectIdentifiers(ast) — собирает все идентификаторы (IdentifierExpression) в дереве выражения;
  • collectMemberPaths(ast) — собирает все пути вида obj.prop, data.users[0].name:
    • возвращает полные пути и промежуточные цепочки: data.users, data.users[0], data.users[0].name;
    • использует вспомогательную функцию buildMemberPath(node: MemberExpression).

Эти утилиты используются:

  • при построении ExpressionContext и связывании выражений с DSL;
  • в builder-web — для подсветки доступных идентификаторов и автодополнения;
  • в аналитике и диагностике.

5.3. expressionToString

expressionToString(ast) — преобразует Expression AST обратно в JS‑подобную строку. Реализован как корректный, но «много скобок» printer:

  • расставляет скобки вокруг бинарных и логических выражений;
  • корректно печатает массивы и объекты;
  • поддерживает унарные операторы и typeof.

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


6. Система типизации (analyzer/)

Модуль analyzer/ реализует простой, DSL-независимый механизм вывода типов.

6.1. Примитивные типы и контекст

ExpressionPrimitiveType

export type ExpressionPrimitiveType =
| 'string'
| 'number'
| 'boolean'
| 'null'
| 'undefined'
| 'any'
| 'unknown';

ExpressionContext

Контекст типов определяется вызывающей стороной (например, dsl-compiler):

interface ExpressionContextVariable {
type: ExpressionPrimitiveType;
}

interface ExpressionContext {
variables: Record<string, ExpressionContextVariable>;
}

Примеры ключей:

  • "state.count"{ type: "number" };
  • "props.title"{ type: "string" };
  • "data.users"{ type: "any" }.

Ключи никак не нормализуются самим модулем — формирование имён полностью на стороне DSL и компилятора.

6.2. Вывод типов: inferExpressionType

function inferExpressionType(
node: ExpressionNode,
context: ExpressionContext,
): ExpressionPrimitiveType;

Основные правила:

  • литералы → string, number, boolean, null;

  • идентификаторы → тип из context.variables[name] или "unknown";

  • member-пути → тип из context.variables[path] (path строится через buildMemberPath);

  • бинарные операции:

    • +, -, *, /, %, **number, если оба аргумента number;
    • + с участием stringstring;
    • сравнения (==, >= и т.п.) → boolean;
  • логические операции:

    • &&, ||boolean (консервативная оценка);
    • ?? → объединение типов (left/right), с упрощением до одного типа или "any";
  • унарные операции:

    • !boolean;
    • +/-number | any (при несовпадении);
    • typeofstring;
  • массивы и объекты → пока "any" (возможное направление развития).

Алгоритм специально сделан консервативным: при сомнениях возвращает "unknown" или "any", чтобы не блокировать работу редактора.


7. Валидация выражений (analyzer/validate.ts)

Валидатор проверяет две группы условий:

  1. корректность используемых символов (идентификаторы и member-пути);
  2. соответствие итогового типа выражения ожидаемому типу.

7.1. Интерфейс ValidationIssue

interface ValidationIssue {
message: string;
path?: string;
severity?: 'error' | 'warning';
}

7.2. validateExpressionType

function validateExpressionType(
root: ExpressionNode,
context: ExpressionContext,
expected: ExpressionPrimitiveType,
): ValidationIssue[];

Шаги:

  1. Проверка символов:

    • обход дерева через traverse;
    • для Identifier — проверка context.variables[name];
    • для MemberExpression — сбор path через buildMemberPath и проверка context.variables[path];
    • для неизвестных идентификаторов/путей создаются ValidationIssue с severity: "error".
  2. Проверка конечного типа:

    • вычисление actual через inferExpressionType;
    • если actual"unknown" или "any", а expected"any", добавляется warning;
    • если actual и expected оба конкретные и отличаются — добавляется error.

Типичные сценарии использования:

  • в builder-web — немедленная подсветка ошибок в поле ввода выражения;
  • в dsl-compiler — жёсткая проверка типов пропсов и значений состояния;
  • в backend — валидация логики перед сохранением версии проекта.

8. Интеграция с DSL и DSL Compiler

@lowcode/expression-parser сам по себе не знает ничего о DSL, но его API специально спроектирован так, чтобы легко интегрироваться с @lowcode/dsl и @lowcode/dsl-compiler.

8.1. DSL: формирование контекста

Пакет @lowcode/dsl описывает схему приложения (AppSchema, PageSchema, StateVariable и т.п.). На основе этой схемы можно построить ExpressionContext:

  • для каждой переменной состояния приложения → "state.<name>";
  • для каждой переменной состояния страницы → "state.<pageId>.<name>" или другое соглашение;
  • для пропсов компонента → "props.<propName>";
  • для источников данных → "data.<dataSourceId>".

Контекст может быть разным для разных страниц и компонент.

8.2. DSL Compiler: нормализация ExpressionValue

В @lowcode/dsl-compiler каждое значение типа ExpressionValue может быть нормализовано в расширенную структуру, в которую входят:

  • исходная строка выражения;
  • разобранный ExpressionNode;
  • ExpressionPrimitiveType, полученный через inferExpressionType;
  • список используемых символов и путей.

Это позволяет компилятору:

  • на этапе сборки AST проверять корректность выражений;
  • рантайм-генератору понимать, какие части state/props влияют на выражение;
  • потенциално оптимизировать перерендеринг компонентов.

8.3. Builder-web: интерактивное редактирование

В редакторе свойств (PropertiesPanel v2) модуль используется так:

  1. пользователь меняет строку выражения в инпуте;
  2. builder-web вызывает parseExpression и validateExpressionType с актуальным ExpressionContext;
  3. ошибки из ValidationIssue[] отображаются рядом с полем;
  4. по collectMemberPaths и контексту могут строиться подсказки и автодополнение.

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


9. Расширение модуля

Архитектура @lowcode/expression-parser рассчитана на постепенное развитие.

Возможные направления:

  • добавление новых операторов и плагинов в jsep-конфигурацию;
  • поддержка более богатой типизации массивов и объектов (generic-типы);
  • встроенный реестр «безопасных» функций (len, now, sum, avg) с описанием их типов;
  • кэширование AST и результатов анализа по строке выражения;
  • расширенный формат диагностик (коды ошибок, быстрые фиксы для редактора).

При этом базовый контракт — parseExpression, inferExpressionType, validateExpressionType и структура AST — остаются стабильными, чтобы не ломать потребителей модуля.


10. Резюме

@lowcode/expression-parser предоставляет:

Единый формат AST выражений
Единый механизм парсинга строк
Единую систему типизации и валидации
Утилиты для обхода и анализа выражений

Он служит общим фундаментом для работы с выражениями во всех частях Lowcode Platform и позволяет развивать язык выражений независимо от конкретных реализаций DSL, редактора и runtime.