M3(1/5): @sfeel —— S-FEEL 子集的解析与求值,前后端同源
- 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
This commit is contained in:
co-authored by
Claude Opus 5
parent
f0444f1417
commit
146e9bbfad
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 静态可检 —— 编排画布「画图即校验」与安装静态审的依据(编码规范 §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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user