Skip to content

Commit 74c2a3c

Browse files
committed
refactor(ast): replace string placeholders with Placeholder AST node
Introduce a dedicated Placeholder AST node type to represent variable references and lambda parameters instead of using special string patterns like `$$VAR_...$$`. This provides type safety through symbol-based identification and cleaner separation between variable placeholders (resolved at compile time) and lambda parameter placeholders (resolved at code generation time). - Add Placeholder interface with symbol id to ast-types.ts - Update ArrowFunctionExpr.params to accept Placeholder nodes - Add transformPlaceholders function for compile-time placeholder resolution - Update generate.ts to handle Placeholder nodes with symbol-based mapping - Remove getVariablePlaceholder and parseVariablePlaceholder from variable.ts
1 parent ff79389 commit 74c2a3c

7 files changed

Lines changed: 171 additions & 68 deletions

File tree

src/ast-types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ export interface Identifier {
2828
name: string;
2929
}
3030

31+
/**
32+
* 变量占位符节点
33+
* 用于表示变量引用或 lambda 参数,在编译/代码生成时被替换
34+
*/
35+
export interface Placeholder {
36+
type: "Placeholder";
37+
/** 变量或 lambda 参数的唯一标识符 */
38+
id: symbol;
39+
}
40+
3141
export interface BinaryExpr {
3242
type: "BinaryExpr";
3343
operator: string;
@@ -83,7 +93,7 @@ export interface ObjectProperty {
8393

8494
export interface ArrowFunctionExpr {
8595
type: "ArrowFunctionExpr";
86-
params: Identifier[];
96+
params: (Identifier | Placeholder)[];
8797
body: ASTNode;
8898
}
8999

@@ -94,6 +104,7 @@ export type ASTNode =
94104
| BooleanLiteral
95105
| NullLiteral
96106
| Identifier
107+
| Placeholder
97108
| BinaryExpr
98109
| UnaryExpr
99110
| ConditionalExpr

src/compile.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ASTNode } from "./ast-types";
2-
import { generate, transformIdentifiers } from "./generate";
2+
import { generate, transformIdentifiers, transformPlaceholders } from "./generate";
33
import { serializeArgumentToAST } from "./proxy-variable";
44
import type { BranchNode, CompiledData, CompiledExpression, JumpNode, LambdaBodyResult, PhiNode } from "./types";
55
import { getVariableId } from "./variable";
@@ -83,35 +83,43 @@ export function compile<TResult>(
8383

8484
const ast = serializeArgumentToAST(expression);
8585

86-
// 建立变量名到索引的映射
86+
// 建立变量名到索引的映射,以及 symbol -> 变量名的映射
8787
const variableOrder: string[] = [];
8888
const variableToIndex = new Map<string, number>();
89-
const descToName = new Map<string, string>();
89+
const symbolToName = new Map<symbol, string>();
9090

9191
for (const [name, value] of Object.entries(variables)) {
9292
if (!variableToIndex.has(name)) {
9393
variableToIndex.set(name, variableOrder.length);
9494
variableOrder.push(name);
9595
}
9696
const id = getVariableId(value);
97-
if (id?.description) {
98-
descToName.set(id.description, name);
97+
if (id) {
98+
symbolToName.set(id, name);
9999
}
100100
}
101101

102-
// 转换 AST:将占位符替换为变量名,然后替换为 $N
103-
const undefinedVars: string[] = [];
104-
const transformed = transformIdentifiers(ast, (name) => {
105-
const placeholderMatch = name.match(/^\$\$VAR_(.+)\$\$$/);
106-
const resolvedName = placeholderMatch ? descToName.get(placeholderMatch[1]!) : name;
102+
// 第一步:转换 Placeholder 节点为 $N 格式的 Identifier
103+
// lambda 参数的 Placeholder 保留不转换(返回 null)
104+
const placeholderTransformed = transformPlaceholders(ast, (id) => {
105+
const name = symbolToName.get(id);
106+
if (!name) return null; // 不是变量占位符(可能是 lambda 参数),保留
107+
const index = variableToIndex.get(name);
108+
if (index === undefined) return null;
109+
return `$${index}`;
110+
});
107111

108-
if (placeholderMatch && !resolvedName) throw new Error(`Unknown variable placeholder: ${name}`);
112+
// 第二步:检查是否有未定义的 Identifier(非全局对象)
113+
const undefinedVars: string[] = [];
114+
const transformed = transformIdentifiers(placeholderTransformed, (name) => {
115+
// 已经转换为 $N 的跳过
116+
if (name.startsWith("$") && /^\$\d+$/.test(name)) return name;
109117

110-
const index = variableToIndex.get(resolvedName!);
118+
const index = variableToIndex.get(name);
111119
if (index !== undefined) return `$${index}`;
112120

113-
if (!ALLOWED_GLOBALS.has(resolvedName!)) undefinedVars.push(resolvedName!);
114-
return resolvedName!;
121+
if (!ALLOWED_GLOBALS.has(name)) undefinedVars.push(name);
122+
return name;
115123
});
116124

117125
if (undefinedVars.length > 0) {

src/expr.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { ASTNode } from "./ast-types";
1+
import type { ASTNode, Placeholder } from "./ast-types";
22
import { transformIdentifiers } from "./generate";
33
import { parse } from "./parser";
44
import { getProxyMetadata } from "./proxy-metadata";
55
import { createProxyExpressionWithAST } from "./proxy-variable";
66
import type { InferExpressionResult, ValidateExpression } from "./type-parser";
77
import type { Proxify } from "./types";
8-
import { getVariableId, getVariablePlaceholder } from "./variable";
8+
import { getVariableId } from "./variable";
99

1010
/**
1111
* 创建表达式
@@ -95,12 +95,12 @@ export function expr<TContext extends Record<string, unknown>>(
9595
const ast = parse(source as string);
9696

9797
// 在 AST 级别进行标识符替换
98-
const transformedAst = transformIdentifiers(ast, (name) => {
98+
const transformedAst = transformIdentifiers(ast, (name): string | ASTNode => {
9999
// 检查是否是 context 中的变量
100100
const id = nameToId.get(name);
101101
if (id) {
102-
// 返回占位符标识符名称
103-
return getVariablePlaceholder(id);
102+
// 返回占位符节点
103+
return { type: "Placeholder", id } satisfies Placeholder;
104104
}
105105

106106
// 检查是否是子表达式

src/generate.ts

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { BUILTIN_CONSTRUCTORS, PRECEDENCE, RIGHT_ASSOCIATIVE } from "./ast-types
1212
export interface GenerateContext {
1313
/** lambda 参数计数器,用于生成唯一参数名 */
1414
lambdaParamCounter: number;
15-
/** 占位符到实际参数名的映射 */
16-
paramMapping: Map<string, string>;
15+
/** 占位符 Symbol 到实际参数名的映射 */
16+
paramMapping: Map<symbol, string>;
1717
}
1818

1919
/**
@@ -52,10 +52,15 @@ export function generateWithContext(node: ASTNode, ctx: GenerateContext): string
5252
case "NullLiteral":
5353
return "null";
5454

55-
case "Identifier": {
56-
// 检查是否是 lambda 参数占位符
57-
const mappedName = ctx.paramMapping.get(node.name);
58-
return mappedName ?? node.name;
55+
case "Identifier":
56+
return node.name;
57+
58+
case "Placeholder": {
59+
// 查找 lambda 参数映射
60+
const mappedName = ctx.paramMapping.get(node.id);
61+
if (mappedName) return mappedName;
62+
// 未编译的占位符输出为特殊格式(用于测试/调试)
63+
return `$$${node.id.description}$$`;
5964
}
6065

6166
case "BinaryExpr": {
@@ -118,11 +123,19 @@ export function generateWithContext(node: ASTNode, ctx: GenerateContext): string
118123
case "ArrowFunctionExpr": {
119124
// 为每个参数分配唯一的参数名
120125
const paramNames: string[] = [];
126+
const placeholderIds: symbol[] = [];
127+
121128
for (const param of node.params) {
122-
const uniqueName = `_${ctx.lambdaParamCounter++}`;
123-
paramNames.push(uniqueName);
124-
// 建立占位符到实际参数名的映射
125-
ctx.paramMapping.set(param.name, uniqueName);
129+
if (param.type === "Placeholder") {
130+
// Placeholder 参数:分配唯一名称并建立映射
131+
const uniqueName = `_${ctx.lambdaParamCounter++}`;
132+
paramNames.push(uniqueName);
133+
placeholderIds.push(param.id);
134+
ctx.paramMapping.set(param.id, uniqueName);
135+
} else {
136+
// Identifier 参数:直接使用名称
137+
paramNames.push(param.name);
138+
}
126139
}
127140

128141
const paramsStr = paramNames.length === 1 ? paramNames[0]! : `(${paramNames.join(",")})`;
@@ -131,11 +144,9 @@ export function generateWithContext(node: ASTNode, ctx: GenerateContext): string
131144
? `(${generateWithContext(node.body, ctx)})`
132145
: generateWithContext(node.body, ctx);
133146

134-
// 清理参数映射(可选,防止污染外层作用域)
135-
// 注意:由于我们使用唯一的参数名,不清理也不会冲突
136-
// 但为了语义清晰,在函数体生成完成后移除映射
137-
for (const param of node.params) {
138-
ctx.paramMapping.delete(param.name);
147+
// 清理 Placeholder 参数映射
148+
for (const id of placeholderIds) {
149+
ctx.paramMapping.delete(id);
139150
}
140151

141152
return `${paramsStr}=>${body}`;
@@ -220,6 +231,10 @@ export function transformIdentifiers(node: ASTNode, transform: (name: string) =>
220231
return typeof result === "string" ? { ...node, name: result } : result;
221232
}
222233

234+
case "Placeholder":
235+
// Placeholder 不是 Identifier,保持不变
236+
return node;
237+
223238
case "BinaryExpr":
224239
return {
225240
...node,
@@ -273,8 +288,11 @@ export function transformIdentifiers(node: ASTNode, transform: (name: string) =>
273288
};
274289

275290
case "ArrowFunctionExpr": {
276-
// 箭头函数:参数名不转换,只转换函数体中的非参数标识符
277-
const paramNames = new Set(node.params.map((p) => p.name));
291+
// 箭头函数:只转换 Identifier 参数名,Placeholder 参数保持不变
292+
// 只转换函数体中的非参数标识符
293+
const paramNames = new Set(
294+
node.params.filter((p): p is { type: "Identifier"; name: string } => p.type === "Identifier").map((p) => p.name)
295+
);
278296
return {
279297
...node,
280298
body: transformIdentifiers(node.body, (name) => (paramNames.has(name) ? name : transform(name))),
@@ -286,6 +304,87 @@ export function transformIdentifiers(node: ASTNode, transform: (name: string) =>
286304
}
287305
}
288306

307+
/**
308+
* 转换 AST 中的占位符节点
309+
* 回调函数接收 symbol,返回 Identifier 节点的名称
310+
* 如果返回 null/undefined,则保留原始 Placeholder 节点
311+
*/
312+
export function transformPlaceholders(node: ASTNode, transform: (id: symbol) => string | null | undefined): ASTNode {
313+
switch (node.type) {
314+
case "Placeholder": {
315+
const name = transform(node.id);
316+
return name != null ? { type: "Identifier", name } : node;
317+
}
318+
319+
case "Identifier":
320+
return node;
321+
322+
case "BinaryExpr":
323+
return {
324+
...node,
325+
left: transformPlaceholders(node.left, transform),
326+
right: transformPlaceholders(node.right, transform),
327+
};
328+
329+
case "UnaryExpr":
330+
return {
331+
...node,
332+
argument: transformPlaceholders(node.argument, transform),
333+
};
334+
335+
case "ConditionalExpr":
336+
return {
337+
...node,
338+
test: transformPlaceholders(node.test, transform),
339+
consequent: transformPlaceholders(node.consequent, transform),
340+
alternate: transformPlaceholders(node.alternate, transform),
341+
};
342+
343+
case "MemberExpr":
344+
return {
345+
...node,
346+
object: transformPlaceholders(node.object, transform),
347+
property: node.computed ? transformPlaceholders(node.property, transform) : node.property,
348+
};
349+
350+
case "CallExpr":
351+
return {
352+
...node,
353+
callee: transformPlaceholders(node.callee, transform),
354+
arguments: node.arguments.map((arg) => transformPlaceholders(arg, transform)),
355+
};
356+
357+
case "ArrayExpr":
358+
return {
359+
...node,
360+
elements: node.elements.map((el) => transformPlaceholders(el, transform)),
361+
};
362+
363+
case "ObjectExpr":
364+
return {
365+
...node,
366+
properties: node.properties.map((prop) => ({
367+
...prop,
368+
key: prop.computed ? transformPlaceholders(prop.key, transform) : prop.key,
369+
value: transformPlaceholders(prop.value, transform),
370+
})),
371+
};
372+
373+
case "ArrowFunctionExpr": {
374+
// 箭头函数:参数保持不变(Placeholder 参数在代码生成时处理)
375+
// 函数体中的 Placeholder 需要转换,但要排除参数本身的 symbol
376+
const paramSymbols = new Set(node.params.filter((p) => p.type === "Placeholder").map((p) => p.id));
377+
return {
378+
...node,
379+
body: transformPlaceholders(node.body, (id) => (paramSymbols.has(id) ? null : transform(id))),
380+
};
381+
}
382+
383+
default:
384+
return node;
385+
}
386+
}
387+
289388
/**
290389
* 收集 AST 中所有使用的标识符名称
291390
*/
@@ -341,7 +440,10 @@ export function collectIdentifiers(node: ASTNode): Set<string> {
341440
case "ArrowFunctionExpr": {
342441
// 箭头函数:收集参数名和函数体中的标识符
343442
// 但从闭包角度,只需收集非参数的自由变量
344-
const paramNames = new Set(n.params.map((p) => p.name));
443+
// 只考虑 Identifier 参数,Placeholder 参数在编译时处理
444+
const paramNames = new Set(
445+
n.params.filter((p): p is { type: "Identifier"; name: string } => p.type === "Identifier").map((p) => p.name)
446+
);
345447
const bodyIdentifiers = collectIdentifiers(n.body);
346448
for (const id of bodyIdentifiers) {
347449
if (!paramNames.has(id)) {

src/lambda.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,17 @@ function extractBodyAstAndDeps(bodyExpr: unknown): { bodyAst: ASTNode; bodyDeps:
149149

150150
/**
151151
* 创建箭头函数 AST
152-
* 使用占位符参数名,在代码生成时再分配实际参数名
152+
* 使用 Placeholder 节点作为参数,在代码生成时再分配实际参数名
153153
*/
154154
function createArrowFunctionAst(bodyAst: ASTNode, paramCount: number, paramSymbols: symbol[]): ASTNode {
155-
const paramIdentifiers = Array.from({ length: paramCount }, (_, i) => ({
156-
type: "Identifier" as const,
157-
// 使用占位符,在 generate 时会被替换为唯一参数名
158-
name: `$$VAR_${paramSymbols[i]?.description}$$`,
155+
const paramPlaceholders = Array.from({ length: paramCount }, (_, i) => ({
156+
type: "Placeholder" as const,
157+
id: paramSymbols[i]!,
159158
}));
160159

161160
return {
162161
type: "ArrowFunctionExpr",
163-
params: paramIdentifiers,
162+
params: paramPlaceholders,
164163
body: bodyAst,
165164
};
166165
}

src/proxy-variable.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
NullLiteral,
1010
NumberLiteral,
1111
ObjectExpr,
12+
Placeholder,
1213
StringLiteral,
1314
} from "./ast-types";
1415
import { getProxyMetadata, setProxyMetadata } from "./proxy-metadata";
@@ -45,11 +46,10 @@ const typedArrayConstructors = [
4546
];
4647

4748
/**
48-
* 使用 Symbol.description 生成占位符
49-
* 用于在表达式源码中标识变量
49+
* 创建占位符 AST 节点
5050
*/
51-
function getVariablePlaceholder(id: symbol): string {
52-
return `$$VAR_${id.description}$$`;
51+
function placeholder(id: symbol): Placeholder {
52+
return { type: "Placeholder", id };
5353
}
5454

5555
/**
@@ -123,9 +123,9 @@ export function serializeArgumentToAST(arg: unknown): ASTNode {
123123
if (meta) {
124124
// 如果有 ast,直接返回
125125
if (meta.ast) return meta.ast;
126-
// 否则是根 variable,返回占位符标识符
126+
// 否则是根 variable,返回占位符节点
127127
if (meta.rootVariable) {
128-
return identifier(getVariablePlaceholder(meta.rootVariable));
128+
return placeholder(meta.rootVariable);
129129
}
130130
}
131131
}
@@ -245,7 +245,7 @@ export function collectDepsFromArgs(args: unknown[], deps: Set<symbol>): void {
245245
* 根据路径构建成员表达式 AST
246246
*/
247247
function buildMemberExprAst(rootId: symbol, path: string[]): ASTNode {
248-
let ast: ASTNode = identifier(getVariablePlaceholder(rootId));
248+
let ast: ASTNode = placeholder(rootId);
249249
for (const prop of path) {
250250
ast = memberExpr(ast, identifier(prop));
251251
}

0 commit comments

Comments
 (0)