Archived
- lexer / parser(递归下降)/ evaluate / static-check,四文件纯函数,零 IO 零外部调用 - 子集封顶(ADR-006):比较 · 逻辑 · 算术 · 区间(开闭四态)· 列表与 in · 封闭内建函数集(字符串 / 数字 / 日期);未知函数一律拦下 - 单元格谓词(DMN 一格):`-` 任意 / 比较 / 区间 / 逗号析取 - staticCheck 给「画图即校验」用:语法 + 未知函数 + 引用路径清单 - app 侧以它替下 passthroughEvaluator:监听内容条件真求值,求值异常按不命中处置并留痕 32 个 sfeel 测试;全仓 182 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
/**
|
|
* 静态可检 —— 编排画布「画图即校验」与安装静态审的依据(编码规范 §8.3;OS 03)。
|
|
*
|
|
* 检三样:①语法能不能过 ②引用的函数在不在封闭集合里 ③引用了哪些路径(供必填字段核对)。
|
|
*/
|
|
import type { Node, UnaryTest } from './ast.ts';
|
|
import { SfeelSyntaxError } from './lexer.ts';
|
|
import { parse, parseUnaryTest } from './parser.ts';
|
|
import { BUILTIN_FUNCTIONS } from './evaluate.ts';
|
|
|
|
export type StaticIssue = {
|
|
readonly kind: 'syntax' | 'unknown-function';
|
|
readonly message: string;
|
|
};
|
|
|
|
export type StaticCheckResult = {
|
|
readonly ok: boolean;
|
|
readonly issues: readonly StaticIssue[];
|
|
/** 表达式引用的全部根路径 —— 编排器据此核对上游载荷是否供得上 */
|
|
readonly references: readonly string[];
|
|
};
|
|
|
|
export function staticCheck(expression: string, mode: 'expression' | 'unary-test' = 'expression'): StaticCheckResult {
|
|
let node: Node | undefined;
|
|
const issues: StaticIssue[] = [];
|
|
|
|
try {
|
|
if (mode === 'unary-test') {
|
|
const test = parseUnaryTest(expression);
|
|
const nodes = collectTestNodes(test);
|
|
const references = new Set<string>();
|
|
for (const n of nodes) walk(n, issues, references);
|
|
return { ok: issues.length === 0, issues, references: [...references] };
|
|
}
|
|
node = parse(expression);
|
|
} catch (error) {
|
|
if (error instanceof SfeelSyntaxError) return { ok: false, issues: [{ kind: 'syntax', message: error.message }], references: [] };
|
|
throw error;
|
|
}
|
|
|
|
const references = new Set<string>();
|
|
walk(node, issues, references);
|
|
return { ok: issues.length === 0, issues, references: [...references] };
|
|
}
|
|
|
|
function collectTestNodes(test: UnaryTest): Node[] {
|
|
switch (test.kind) {
|
|
case 'any':
|
|
return [];
|
|
case 'compare':
|
|
return [test.operand];
|
|
case 'range':
|
|
return [test.node];
|
|
case 'disjunction':
|
|
return test.tests.flatMap(collectTestNodes);
|
|
}
|
|
}
|
|
|
|
function walk(node: Node, issues: StaticIssue[], references: Set<string>): void {
|
|
switch (node.kind) {
|
|
case 'literal':
|
|
return;
|
|
case 'path':
|
|
references.add(node.segments.join('.'));
|
|
return;
|
|
case 'unary':
|
|
walk(node.operand, issues, references);
|
|
return;
|
|
case 'binary':
|
|
walk(node.left, issues, references);
|
|
walk(node.right, issues, references);
|
|
return;
|
|
case 'list':
|
|
for (const item of node.items) walk(item, issues, references);
|
|
return;
|
|
case 'range':
|
|
walk(node.start, issues, references);
|
|
walk(node.end, issues, references);
|
|
return;
|
|
case 'in':
|
|
walk(node.value, issues, references);
|
|
walk(node.candidates, issues, references);
|
|
return;
|
|
case 'call':
|
|
if (!BUILTIN_FUNCTIONS.includes(node.name)) {
|
|
issues.push({ kind: 'unknown-function', message: `未知函数 ${node.name}(S-FEEL 子集封顶,无外部调用)` });
|
|
}
|
|
for (const arg of node.args) walk(arg, issues, references);
|
|
return;
|
|
}
|
|
}
|