Skip to content

Commit a2e764f

Browse files
committed
feat(compiler): add short-circuit evaluation with control flow nodes
Implement SSA-style control flow for short-circuit operators (&&, ||, ??) and ternary expressions. When shortCircuit option is enabled, the compiler generates br/jmp/phi nodes instead of inlined expressions, allowing the evaluator to skip unnecessary computations. - Add BranchNode, JumpNode, PhiNode types for control flow - Add shortCircuit compile option (default: false) - Implement V2 evaluator with program counter and switch-case execution - Maintain backward compatibility with V1 format
1 parent faece04 commit a2e764f

5 files changed

Lines changed: 476 additions & 13 deletions

File tree

src/compile.ts

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { z } from "zod";
22
import { collectIdentifiers, generate, parse, type ASTNode } from "./parser";
3-
import type { CompileContext, CompiledData, Expression, ExprNode, Variable } from "./types";
3+
import type {
4+
BranchNode,
5+
CompileContext,
6+
CompiledData,
7+
CompiledExpression,
8+
Expression,
9+
ExprNode,
10+
JumpNode,
11+
PhiNode,
12+
Variable,
13+
} from "./types";
414

515
/**
616
* 编译选项
@@ -12,6 +22,13 @@ export interface CompileOptions {
1222
* @default true
1323
*/
1424
inline?: boolean;
25+
26+
/**
27+
* 是否启用短路求值
28+
* 为 &&, ||, ??, 和三元表达式生成控制流节点
29+
* @default false
30+
*/
31+
shortCircuit?: boolean;
1532
}
1633

1734
/**
@@ -45,7 +62,7 @@ export function compile<TResult>(
4562
variables: Record<string, Variable<z.ZodType>>,
4663
options: CompileOptions = {}
4764
): CompiledData {
48-
const { inline = true } = options;
65+
const { inline = true, shortCircuit = false } = options;
4966
// 创建编译上下文
5067
const context: CompileContext = {
5168
variableOrder: [],
@@ -247,11 +264,127 @@ export function compile<TResult>(
247264
nodeAstMap.set(exprNode.id, transformed);
248265
}
249266

250-
// 第八步:生成最终表达式列表(只包含不能内联的表达式)
251-
for (const exprNode of sortedExprNodes) {
252-
if (!canInline(exprNode)) {
253-
const ast = nodeAstMap.get(exprNode.id)!;
254-
context.expressions.push(generate(ast));
267+
// 第八步:生成最终表达式列表
268+
if (shortCircuit) {
269+
// 短路求值模式:为每个不能内联的表达式生成控制流指令
270+
const expressions: CompiledExpression[] = [];
271+
let nextIndex = context.variableOrder.length;
272+
273+
// 用于追踪已经编译过的节点
274+
const compiledNodeIndices = new Map<symbol, number>();
275+
276+
// 编译单个 AST 节点到指令序列,返回结果所在的索引
277+
const compileAst = (ast: ASTNode): number => {
278+
// 检查是否需要短路处理
279+
if (ast.type === "BinaryExpr" && (ast.operator === "||" || ast.operator === "&&" || ast.operator === "??")) {
280+
return compileShortCircuit(ast);
281+
}
282+
283+
if (ast.type === "ConditionalExpr") {
284+
return compileConditional(ast);
285+
}
286+
287+
// 普通表达式:直接生成
288+
const exprStr = generate(ast);
289+
const idx = nextIndex++;
290+
expressions.push(exprStr);
291+
return idx;
292+
};
293+
294+
// 编译短路运算符 (&&, ||, ??)
295+
const compileShortCircuit = (node: ASTNode & { type: "BinaryExpr" }): number => {
296+
// 递归编译左操作数
297+
const leftIdx = compileAst(node.left);
298+
299+
// 生成跳转条件
300+
let branchCondition: string;
301+
if (node.operator === "||") {
302+
// || : 如果左边为 true,跳过右边
303+
branchCondition = `$${leftIdx}`;
304+
} else if (node.operator === "&&") {
305+
// && : 如果左边为 false,跳过右边
306+
branchCondition = `!$${leftIdx}`;
307+
} else {
308+
// ?? : 如果左边非 null/undefined,跳过右边
309+
branchCondition = `$${leftIdx}!=null`;
310+
}
311+
312+
// 记录 br 指令的位置,稍后填入正确的 offset
313+
const branchIdx = expressions.length;
314+
expressions.push(["br", branchCondition, 0] as BranchNode); // 占位,offset 稍后修复
315+
nextIndex++;
316+
317+
// 递归编译右操作数
318+
const rightIdx = compileAst(node.right);
319+
320+
// 修复 br 的 offset:跳过右操作数的所有指令
321+
const skipCount = expressions.length - branchIdx - 1;
322+
(expressions[branchIdx] as BranchNode)[2] = skipCount;
323+
324+
// 生成 phi 节点
325+
const phiIdx = nextIndex++;
326+
expressions.push(["phi"] as PhiNode);
327+
328+
return phiIdx;
329+
};
330+
331+
// 编译三元表达式
332+
const compileConditional = (node: ASTNode & { type: "ConditionalExpr" }): number => {
333+
// 编译条件
334+
const testIdx = compileAst(node.test);
335+
336+
// 生成条件跳转:如果条件为 true,跳到 then 分支
337+
// 但在我们的布局中,else 分支在前,then 分支在后
338+
// 所以:如果条件为 true,跳过 else 分支
339+
const branchIdx = expressions.length;
340+
expressions.push(["br", `$${testIdx}`, 0] as BranchNode); // 占位
341+
nextIndex++;
342+
343+
// 编译 else 分支(alternate)
344+
const elseStartIdx = expressions.length;
345+
compileAst(node.alternate);
346+
const elseEndIdx = expressions.length;
347+
348+
// 生成 jmp 跳过 then 分支
349+
const jmpIdx = expressions.length;
350+
expressions.push(["jmp", 0] as JumpNode); // 占位
351+
nextIndex++;
352+
353+
// 编译 then 分支(consequent)
354+
const thenStartIdx = expressions.length;
355+
compileAst(node.consequent);
356+
const thenEndIdx = expressions.length;
357+
358+
// 修复 br 的 offset:跳过 else 分支和 jmp 指令
359+
(expressions[branchIdx] as BranchNode)[2] = jmpIdx - branchIdx;
360+
361+
// 修复 jmp 的 offset:跳过 then 分支
362+
(expressions[jmpIdx] as JumpNode)[1] = thenEndIdx - jmpIdx - 1;
363+
364+
// 生成 phi 节点
365+
const phiIdx = nextIndex++;
366+
expressions.push(["phi"] as PhiNode);
367+
368+
return phiIdx;
369+
};
370+
371+
// 为每个不能内联的表达式生成指令
372+
for (const exprNode of sortedExprNodes) {
373+
if (!canInline(exprNode)) {
374+
const ast = nodeAstMap.get(exprNode.id)!;
375+
const resultIdx = compileAst(ast);
376+
compiledNodeIndices.set(exprNode.id, resultIdx);
377+
}
378+
}
379+
380+
context.expressions = expressions;
381+
} else {
382+
// 原始模式:直接生成表达式字符串
383+
for (const exprNode of sortedExprNodes) {
384+
if (!canInline(exprNode)) {
385+
const ast = nodeAstMap.get(exprNode.id)!;
386+
context.expressions.push(generate(ast));
387+
}
255388
}
256389
}
257390

src/evaluate.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import type { CompiledData } from "./types";
1+
import type { CompiledData, CompiledExpression } from "./types";
22

33
/**
44
* 缓存已构造的求值函数,以提升重复执行性能
55
*/
66
const evaluatorCache = new Map<string, (values: unknown[]) => unknown>();
77

8+
/**
9+
* 检测编译数据是否包含控制流节点(V2 格式)
10+
*/
11+
function isV2Format(expressions: CompiledExpression[]): boolean {
12+
return expressions.some((expr) => Array.isArray(expr));
13+
}
14+
815
/**
916
* 执行编译后的表达式
1017
*
@@ -54,8 +61,10 @@ export function evaluate<TResult>(data: CompiledData, values: Record<string, unk
5461
let evaluator = evaluatorCache.get(cacheKey);
5562

5663
if (!evaluator) {
57-
// 构造求值函数
58-
const functionBody = buildEvaluatorFunctionBody(expressions, variableNames.length);
64+
// 根据格式选择合适的函数体构造器
65+
const functionBody = isV2Format(expressions)
66+
? buildEvaluatorFunctionBodyV2(expressions, variableNames.length)
67+
: buildEvaluatorFunctionBody(expressions as string[], variableNames.length);
5968
// eslint-disable-next-line @typescript-eslint/no-implied-eval
6069
evaluator = new Function("$values", functionBody) as (values: unknown[]) => unknown;
6170
evaluatorCache.set(cacheKey, evaluator);
@@ -110,3 +119,72 @@ function buildEvaluatorFunctionBody(expressions: string[], variableCount: number
110119

111120
return lines.join("\n");
112121
}
122+
123+
/**
124+
* 构造带控制流支持的求值函数体(V2 格式)
125+
*
126+
* @param expressions - 表达式列表(可包含控制流节点)
127+
* @param variableCount - 变量数量
128+
* @returns 函数体字符串
129+
*/
130+
function buildEvaluatorFunctionBodyV2(expressions: CompiledExpression[], variableCount: number): string {
131+
if (expressions.length === 0) {
132+
throw new Error("No expressions to evaluate");
133+
}
134+
135+
const lines: string[] = [];
136+
137+
// 初始化变量
138+
for (let i = 0; i < variableCount; i++) {
139+
lines.push(`const $${i} = $values[${i}];`);
140+
}
141+
142+
// 程序计数器和最近值寄存器
143+
lines.push(`let $pc = 0;`);
144+
lines.push(`let $lastValue;`);
145+
146+
// 预先声明所有中间变量
147+
for (let i = 0; i < expressions.length; i++) {
148+
lines.push(`let $${variableCount + i};`);
149+
}
150+
151+
lines.push(`while ($pc < ${expressions.length}) {`);
152+
lines.push(` switch ($pc) {`);
153+
154+
for (let i = 0; i < expressions.length; i++) {
155+
const expr = expressions[i];
156+
const idx = variableCount + i;
157+
158+
lines.push(` case ${i}: {`);
159+
160+
if (typeof expr === "string") {
161+
// 普通表达式:求值并存储
162+
lines.push(` $${idx} = ${expr};`);
163+
lines.push(` $values[${idx}] = $${idx};`);
164+
lines.push(` $lastValue = $${idx};`);
165+
lines.push(` $pc++; break;`);
166+
} else if (expr[0] === "br") {
167+
// 条件跳转:条件为任意表达式
168+
const [, condExpr, offset] = expr;
169+
lines.push(` if (${condExpr}) { $pc += ${offset + 1}; } else { $pc++; }`);
170+
lines.push(` break;`);
171+
} else if (expr[0] === "jmp") {
172+
// 无条件跳转
173+
const [, offset] = expr;
174+
lines.push(` $pc += ${offset + 1}; break;`);
175+
} else if (expr[0] === "phi") {
176+
// phi 节点:取最近值
177+
lines.push(` $${idx} = $lastValue;`);
178+
lines.push(` $values[${idx}] = $lastValue;`);
179+
lines.push(` $pc++; break;`);
180+
}
181+
182+
lines.push(` }`);
183+
}
184+
185+
lines.push(` }`);
186+
lines.push(`}`);
187+
lines.push(`return $values[$values.length - 1];`);
188+
189+
return lines.join("\n");
190+
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ export { expr } from "./expr";
55
export { variable } from "./variable";
66

77
export type {
8+
BranchNode,
89
CompileContext,
910
CompiledData,
11+
CompiledExpression,
12+
ControlFlowNode,
1013
ExprNode,
1114
Expression,
1215
InferContextType,
1316
InferExpressionType,
1417
InferVariableType,
18+
JumpNode,
19+
PhiNode,
1520
Variable,
1621
} from "./types";
1722

0 commit comments

Comments
 (0)