Архитектура 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,MemberExpression—ast/nodes.ts - Парсинг строк:
parseExpression—parser/parse.ts - Обход AST:
traverse—utils/traverse.ts - Сбор идентификаторов/путей:
collectIdentifiers,collectMemberPaths—utils/collect.ts - Печать AST:
expressionToString—utils/print.ts - Типизация:
ExpressionContext,ExpressionPrimitiveType—analyzer/types.ts - Вывод типов:
inferExpressionType—analyzer/inferType.ts - Валидация:
validateExpressionType—analyzer/validate.ts
1. Роль Expression Parser в платформе
@lowcode/expression-parser — это общий «движок выражений» для всей Lowcode Platform. Он решает сразу несколько задач:
- парсит строковые выражения DSL в нормализованное AST;
- предоставляет утилиты обхода и обработки дерева выражения;
- умеет выводить примитивный тип результата выражения с учётом контекста (state, props, dataSources);
- проверяет корректность выражений и возвращает диагностические сообщения;
- служит базой для редактора (
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;
Алгоритм:
- вызывает
jsep(source)и получает jsep-дерево; - рекурсивно конвертирует его в
ExpressionNode(см.convert(...)вparse.ts); - переносит
start/endиз узлов jsep в Expression AST; - при любой синтаксической ошибке возвращает
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;+с участиемstring→string;- сравнения (
==,>=и т.п.) →boolean;
-
логические операции:
&&,||→boolean(консервативная оценка);??→ объединение типов (left/right), с упрощением до одного типа или"any";
-
унарные операции:
!→boolean;+/-→number | any(при несовпадении);typeof→string;
-
массивы и объекты → пока
"any"(возможное направление развития).
Алгоритм специально сделан консервативным: при сомнениях возвращает "unknown" или "any", чтобы не блокировать работу редактора.
7. Валидация выражений (analyzer/validate.ts)
Валидатор проверяет две группы условий:
- корректность используемых символов (идентификаторы и member-пути);
- соответствие итогового типа выражения ожидаемому типу.
7.1. Интерфейс ValidationIssue
interface ValidationIssue {
message: string;
path?: string;
severity?: 'error' | 'warning';
}
7.2. validateExpressionType
function validateExpressionType(
root: ExpressionNode,
context: ExpressionContext,
expected: ExpressionPrimitiveType,
): ValidationIssue[];
Шаги:
-
Проверка символов:
- обход дерева через
traverse; - для
Identifier— проверкаcontext.variables[name]; - для
MemberExpression— сборpathчерезbuildMemberPathи проверкаcontext.variables[path]; - для неизвестных идентификаторов/путей создаются
ValidationIssueсseverity: "error".
- обход дерева через
-
Проверка конечного типа:
- вычисление
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) модуль используется так:
- пользователь меняет строку выражения в инпуте;
- builder-web вызывает
parseExpressionиvalidateExpressionTypeс актуальнымExpressionContext; - ошибки из
ValidationIssue[]отображаются рядом с полем; - по
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.