Skip to content

Commit 9dd4a4d

Browse files
committed
fix(lambda): resolve nested lambda parameter name conflicts
Move parameter name allocation from lambda creation time to code generation time. Use a global counter in the generate function to assign unique parameter names (_0, _1, _2...) across all nested lambdas, preventing inner lambda parameters from shadowing outer ones.
1 parent 77af621 commit 9dd4a4d

3 files changed

Lines changed: 97 additions & 69 deletions

File tree

src/lambda.test.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -600,55 +600,49 @@ describe("嵌套 lambda", () => {
600600
});
601601
});
602602

603-
describe("已知限制", () => {
604-
test("相同参数数量的嵌套 lambda 会发生参数名冲突", () => {
605-
// 当内外层 lambda 参数数量相同时,参数名会冲突(都是 _0, _1 等)
606-
// 这是当前实现的已知限制
603+
describe("相同参数数量的嵌套 lambda", () => {
604+
test("内外层使用相同数量参数时参数名唯一", () => {
605+
// 内外层 lambda 参数数量相同时,代码生成会分配唯一参数名,不会冲突
607606
const matrix = variable<number[][]>();
608607
const processed = matrix.map(
609608
lambda<[number[], number], number[]>((row, rowIdx) =>
610609
row.map(
611610
lambda<[number, number], number>((val, colIdx) =>
612-
// rowIdx 在编译后会变成内层的 _1 (即 colIdx),导致结果不符预期
613611
expr({ val, rowIdx, colIdx })("val + rowIdx * 100 + colIdx")
614612
)
615613
)
616614
)
617615
);
618616

619617
const compiled = compile(processed, { matrix });
620-
// 编译结果: $0.map((_0,_1)=>_0.map((_0,_1)=>_0+_1*100+_1))
621-
// 内层的 _1 覆盖了外层的 _1,所以 rowIdx 实际上是 colIdx
622-
expect(compiled[1]).toContain("(_0,_1)=>_0.map((_0,_1)=>");
618+
// 编译结果: $0.map((_0,_1)=>_0.map((_2,_3)=>_2+_1*100+_3))
619+
// 外层参数 _0, _1,内层参数 _2, _3,不会冲突
620+
expect(compiled[1]).toContain("(_0,_1)=>_0.map((_2,_3)=>");
623621

624622
const result = evaluate(compiled, {
625623
matrix: [
626624
[1, 2, 3],
627625
[4, 5, 6],
628626
],
629627
});
630-
// 实际计算: val + colIdx * 100 + colIdx = val + colIdx * 101
631-
// 而非预期的: val + rowIdx * 100 + colIdx
628+
// 正确计算: val + rowIdx * 100 + colIdx
632629
expect(result).toEqual([
633-
[1, 103, 205], // 1+0*101, 2+1*101, 3+2*101
634-
[4, 106, 208], // 4+0*101, 5+1*101, 6+2*101
630+
[1, 3, 5], // row 0: 1+0+0, 2+0+1, 3+0+2
631+
[104, 106, 108], // row 1: 4+100+0, 5+100+1, 6+100+2
635632
]);
636633
});
637634

638-
test("workaround: 使用不同参数数量避免冲突", () => {
639-
// 解决方案:确保内外层 lambda 使用不同数量的参数
635+
test("内层只用单参数捕获外层参数", () => {
640636
const matrix = variable<number[][]>();
641637
const processed = matrix.map(
642638
lambda<[number[], number], number[]>((row, rowIdx) =>
643-
// 内层只用一个参数,外层用两个参数,避免 _1 冲突
644639
row.map(lambda<[number], number>((val) => expr({ val, rowIdx })("val + rowIdx * 100")))
645640
)
646641
);
647642

648643
const compiled = compile(processed, { matrix });
649-
// 编译结果: $0.map((_0,_1)=>_0.map(_0=>_0+_1*100))
650-
// 内层只有 _0,外层的 _1 不会被覆盖
651-
expect(compiled[1]).toContain("(_0,_1)=>_0.map(_0=>");
644+
// 编译结果: $0.map((_0,_1)=>_0.map(_2=>_2+_1*100))
645+
expect(compiled[1]).toContain("(_0,_1)=>_0.map(_2=>");
652646

653647
const result = evaluate(compiled, {
654648
matrix: [

src/lambda.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// lambda.ts
2-
import { transformIdentifiers, type ASTNode } from "./parser";
2+
import type { ASTNode } from "./parser";
33
import { getProxyMetadata, setProxyMetadata } from "./proxy-metadata";
44
import {
55
collectDepsFromArgs,
@@ -77,13 +77,11 @@ export function lambda<Args extends unknown[], R>(builder: LambdaBuilder<Args, R
7777
// 3. 从 bodyExpr 中提取 AST 和依赖
7878
const { bodyAst, bodyDeps } = extractBodyAstAndDeps(bodyExpr);
7979

80-
// 4. 将参数占位符标识符转换为实际参数名 (_0, _1, _2...)
81-
const transformedBodyAst = transformParamPlaceholders(bodyAst, paramSymbols);
80+
// 4. 构造完整的箭头函数 AST(参数名使用占位符,在代码生成时分配)
81+
// 注意:不再在此处转换参数占位符,而是保持占位符到代码生成时统一分配唯一参数名
82+
const arrowFunctionAst = createArrowFunctionAst(bodyAst, paramCount, paramSymbols);
8283

83-
// 5. 构造完整的箭头函数 AST
84-
const arrowFunctionAst = createArrowFunctionAst(transformedBodyAst, paramCount);
85-
86-
// 6. 过滤掉 lambda 参数依赖,只保留外部闭包变量
84+
// 5. 过滤掉 lambda 参数依赖,只保留外部闭包变量
8785
const closureDeps = filterClosureDeps(bodyDeps, paramSymbols);
8886

8987
// 7. 返回包含 lambda AST 的 Proxy
@@ -149,31 +147,15 @@ function extractBodyAstAndDeps(bodyExpr: unknown): { bodyAst: ASTNode; bodyDeps:
149147
}
150148
}
151149

152-
/**
153-
* 将参数占位符标识符转换为实际参数名
154-
*/
155-
function transformParamPlaceholders(bodyAst: ASTNode, paramSymbols: symbol[]): ASTNode {
156-
return transformIdentifiers(bodyAst, (name) => {
157-
for (let i = 0; i < paramSymbols.length; i++) {
158-
const sym = paramSymbols[i];
159-
if (!sym) continue;
160-
// 占位符格式:$$VAR_lambda_param_N_INDEX$$
161-
const placeholder = `$$VAR_${sym.description}$$`;
162-
if (name === placeholder) {
163-
return `_${i}`;
164-
}
165-
}
166-
return name;
167-
});
168-
}
169-
170150
/**
171151
* 创建箭头函数 AST
152+
* 使用占位符参数名,在代码生成时再分配实际参数名
172153
*/
173-
function createArrowFunctionAst(bodyAst: ASTNode, paramCount: number): ASTNode {
154+
function createArrowFunctionAst(bodyAst: ASTNode, paramCount: number, paramSymbols: symbol[]): ASTNode {
174155
const paramIdentifiers = Array.from({ length: paramCount }, (_, i) => ({
175156
type: "Identifier" as const,
176-
name: `_${i}`,
157+
// 使用占位符,在 generate 时会被替换为唯一参数名
158+
name: `$$VAR_${paramSymbols[i]?.description}$$`,
177159
}));
178160

179161
return {

src/parser.ts

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -804,10 +804,39 @@ export function parse(source: string): ASTNode {
804804
return new Parser(source).parse();
805805
}
806806

807+
/**
808+
* 代码生成上下文
809+
* 用于在嵌套 lambda 中分配唯一参数名
810+
*/
811+
interface GenerateContext {
812+
/** lambda 参数计数器,用于生成唯一参数名 */
813+
lambdaParamCounter: number;
814+
/** 占位符到实际参数名的映射 */
815+
paramMapping: Map<string, string>;
816+
}
817+
818+
/**
819+
* 创建新的生成上下文
820+
*/
821+
function createGenerateContext(): GenerateContext {
822+
return {
823+
lambdaParamCounter: 0,
824+
paramMapping: new Map(),
825+
};
826+
}
827+
807828
/**
808829
* 从 AST 生成规范化的代码
809830
*/
810831
export function generate(node: ASTNode): string {
832+
const ctx = createGenerateContext();
833+
return generateWithContext(node, ctx);
834+
}
835+
836+
/**
837+
* 带上下文的代码生成
838+
*/
839+
function generateWithContext(node: ASTNode, ctx: GenerateContext): string {
811840
switch (node.type) {
812841
case "NumberLiteral":
813842
return node.raw;
@@ -822,12 +851,15 @@ export function generate(node: ASTNode): string {
822851
case "NullLiteral":
823852
return "null";
824853

825-
case "Identifier":
826-
return node.name;
854+
case "Identifier": {
855+
// 检查是否是 lambda 参数占位符
856+
const mappedName = ctx.paramMapping.get(node.name);
857+
return mappedName ?? node.name;
858+
}
827859

828860
case "BinaryExpr": {
829-
const left = wrapIfNeeded(node.left, node, "left");
830-
const right = wrapIfNeeded(node.right, node, "right");
861+
const left = wrapIfNeededWithContext(node.left, node, "left", ctx);
862+
const right = wrapIfNeededWithContext(node.right, node, "right", ctx);
831863
// 关键字运算符需要空格
832864
if (node.operator === "in" || node.operator === "instanceof") {
833865
return `${left} ${node.operator} ${right}`;
@@ -837,55 +869,74 @@ export function generate(node: ASTNode): string {
837869

838870
case "UnaryExpr":
839871
if (node.prefix) {
840-
const arg = wrapIfNeeded(node.argument, node, "argument");
872+
const arg = wrapIfNeededWithContext(node.argument, node, "argument", ctx);
841873
// 对于关键字运算符(typeof, void)需要空格
842874
if (node.operator === "typeof" || node.operator === "void") {
843875
return `${node.operator} ${arg}`;
844876
}
845877
return `${node.operator}${arg}`;
846878
}
847-
return generate(node.argument) + node.operator;
879+
return generateWithContext(node.argument, ctx) + node.operator;
848880

849881
case "ConditionalExpr": {
850-
const test = wrapIfNeeded(node.test, node, "test");
851-
const consequent = wrapIfNeeded(node.consequent, node, "consequent");
852-
const alternate = wrapIfNeeded(node.alternate, node, "alternate");
882+
const test = wrapIfNeededWithContext(node.test, node, "test", ctx);
883+
const consequent = wrapIfNeededWithContext(node.consequent, node, "consequent", ctx);
884+
const alternate = wrapIfNeededWithContext(node.alternate, node, "alternate", ctx);
853885
return `${test}?${consequent}:${alternate}`;
854886
}
855887

856888
case "MemberExpr": {
857-
const object = wrapIfNeeded(node.object, node, "object");
858-
const property = generate(node.property);
889+
const object = wrapIfNeededWithContext(node.object, node, "object", ctx);
890+
const property = generateWithContext(node.property, ctx);
859891
return node.computed
860892
? `${object}${node.optional ? "?." : ""}[${property}]`
861893
: `${object}${node.optional ? "?." : "."}${property}`;
862894
}
863895

864896
case "CallExpr": {
865-
const callee = wrapIfNeeded(node.callee, node, "callee");
866-
const args = node.arguments.map(generate).join(",");
897+
const callee = wrapIfNeededWithContext(node.callee, node, "callee", ctx);
898+
const args = node.arguments.map((arg) => generateWithContext(arg, ctx)).join(",");
867899
const isNew = node.callee.type === "Identifier" && BUILTIN_CONSTRUCTORS.has(node.callee.name);
868900
return `${isNew ? "new " : ""}${callee}${node.optional ? "?." : ""}(${args})`;
869901
}
870902

871903
case "ArrayExpr":
872-
return `[${node.elements.map(generate).join(",")}]`;
904+
return `[${node.elements.map((el) => generateWithContext(el, ctx)).join(",")}]`;
873905

874906
case "ObjectExpr": {
875907
const props = node.properties.map((prop) => {
876908
if (prop.shorthand) {
877-
return generate(prop.key);
909+
return generateWithContext(prop.key, ctx);
878910
}
879-
const key = prop.computed ? `[${generate(prop.key)}]` : generate(prop.key);
880-
return `${key}:${generate(prop.value)}`;
911+
const key = prop.computed ? `[${generateWithContext(prop.key, ctx)}]` : generateWithContext(prop.key, ctx);
912+
return `${key}:${generateWithContext(prop.value, ctx)}`;
881913
});
882914
return `{${props.join(",")}}`;
883915
}
884916

885917
case "ArrowFunctionExpr": {
886-
const params = node.params.map((p) => p.name).join(",");
887-
const body = node.body.type === "ObjectExpr" ? `(${generate(node.body)})` : generate(node.body);
888-
const paramsStr = node.params.length === 1 ? params : `(${params})`;
918+
// 为每个参数分配唯一的参数名
919+
const paramNames: string[] = [];
920+
for (const param of node.params) {
921+
const uniqueName = `_${ctx.lambdaParamCounter++}`;
922+
paramNames.push(uniqueName);
923+
// 建立占位符到实际参数名的映射
924+
ctx.paramMapping.set(param.name, uniqueName);
925+
}
926+
927+
const paramsStr = paramNames.length === 1 ? paramNames[0]! : `(${paramNames.join(",")})`;
928+
const body =
929+
node.body.type === "ObjectExpr"
930+
? `(${generateWithContext(node.body, ctx)})`
931+
: generateWithContext(node.body, ctx);
932+
933+
// 清理参数映射(可选,防止污染外层作用域)
934+
// 注意:由于我们使用唯一的参数名,不清理也不会冲突
935+
// 但为了语义清晰,在函数体生成完成后移除映射
936+
for (const param of node.params) {
937+
ctx.paramMapping.delete(param.name);
938+
}
939+
889940
return `${paramsStr}=>${body}`;
890941
}
891942

@@ -898,14 +949,15 @@ export function generate(node: ASTNode): string {
898949
}
899950

900951
/**
901-
* 判断是否需要括号包裹,并生成代码
952+
* 判断是否需要括号包裹,并生成代码(带上下文版本)
902953
*/
903-
function wrapIfNeeded(
954+
function wrapIfNeededWithContext(
904955
child: ASTNode,
905956
parent: ASTNode,
906-
position: "left" | "right" | "argument" | "object" | "callee" | "test" | "consequent" | "alternate"
957+
position: "left" | "right" | "argument" | "object" | "callee" | "test" | "consequent" | "alternate",
958+
ctx: GenerateContext
907959
): string {
908-
const code = generate(child);
960+
const code = generateWithContext(child, ctx);
909961

910962
if (needsParens(child, parent, position)) {
911963
return `(${code})`;

0 commit comments

Comments
 (0)