Skip to content

Commit b8e3990

Browse files
kubeclaude
andcommitted
FE-514: Fix .map() compilation to use proper variable names
Destructured .map(({ x, y }) => ...) now emits propertyAccess nodes (token.x, token.y) instead of prefixed symbols (_iter_x, _iter_y). The iteration variable is derived from the collection name by singularizing it (tokens → token). Simple .map((token) => ...) uses the user's parameter name directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9ad7e48 commit b8e3990

2 files changed

Lines changed: 52 additions & 9 deletions

File tree

libs/@hashintel/petrinaut/src/expression/ts-to-ir/compile-to-ir.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ describe("compileToIR", () => {
361361
});
362362

363363
describe(".map() list comprehension", () => {
364-
it("should compile .map with destructured params", () => {
364+
it("should compile .map with destructured params as propertyAccess", () => {
365365
const result = compileToIR(
366366
`export default Lambda((tokens, parameters) => tokens.map(({ x }) => x * parameters.infection_rate))`,
367367
defaultContext,
@@ -370,12 +370,16 @@ describe("compileToIR", () => {
370370
if (result.ok) {
371371
expect(result.ir).toEqual({
372372
type: "listComprehension",
373-
variable: "_iter",
373+
variable: "token",
374374
collection: { type: "symbol", name: "tokens" },
375375
body: {
376376
type: "binary",
377377
op: "*",
378-
left: { type: "symbol", name: "_iter_x" },
378+
left: {
379+
type: "propertyAccess",
380+
object: { type: "symbol", name: "token" },
381+
property: "x",
382+
},
379383
right: { type: "parameter", name: "infection_rate" },
380384
},
381385
});
@@ -391,9 +395,9 @@ describe("compileToIR", () => {
391395
if (result.ok) {
392396
expect(result.ir).toEqual({
393397
type: "listComprehension",
394-
variable: "_iter",
398+
variable: "token",
395399
collection: { type: "symbol", name: "tokens" },
396-
body: { type: "symbol", name: "_iter" },
400+
body: { type: "symbol", name: "token" },
397401
});
398402
}
399403
});

libs/@hashintel/petrinaut/src/expression/ts-to-ir/compile-to-ir.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,31 +285,57 @@ function compileBlockToIR(
285285
/**
286286
* Compiles `collection.map(callback)` to a list comprehension IR node.
287287
*/
288+
/**
289+
* Derives a singular iteration variable name from the collection expression.
290+
* `tokens` → `token`, `items` → `item`, fallback → `item`.
291+
*/
292+
function deriveIterVarName(
293+
collection: ts.Expression,
294+
sourceFile: ts.SourceFile,
295+
): string {
296+
const text = collection.getText(sourceFile);
297+
if (text.endsWith("s") && text.length > 1) {
298+
return text.slice(0, -1);
299+
}
300+
return "item";
301+
}
302+
288303
function compileMapCallToIR(
289304
collection: ts.Expression,
290305
callback: ts.ArrowFunction | ts.FunctionExpression,
291306
context: CompilationContext,
292307
outerScope: Scope,
293308
sourceFile: ts.SourceFile,
294309
): IRResult {
295-
const iterVar = "_iter";
296310
const mapScope: Scope = {
297311
localBindingNames: new Set(outerScope.localBindingNames),
298312
symbolOverrides: new Map(outerScope.symbolOverrides),
299313
distributionBindings: new Set(outerScope.distributionBindings),
300314
};
301315

302316
const param = callback.parameters[0];
317+
let iterVar: string;
318+
303319
if (param) {
304320
const paramName = param.name;
305321
if (ts.isObjectBindingPattern(paramName)) {
322+
// Destructured: ({ x, y }) => ...
323+
// Use a derived name and map fields to propertyAccess on it
324+
iterVar = deriveIterVarName(collection, sourceFile);
306325
for (const element of paramName.elements) {
307326
const fieldName = element.name.getText(sourceFile);
308-
mapScope.symbolOverrides.set(fieldName, `${iterVar}_${fieldName}`);
327+
mapScope.symbolOverrides.set(
328+
fieldName,
329+
`\0propAccess:${iterVar}:${fieldName}`,
330+
);
309331
}
310332
} else {
311-
mapScope.symbolOverrides.set(paramName.getText(sourceFile), iterVar);
333+
// Simple: (token) => ...
334+
iterVar = paramName.getText(sourceFile);
335+
mapScope.symbolOverrides.set(iterVar, iterVar);
312336
}
337+
} else {
338+
iterVar = deriveIterVarName(collection, sourceFile);
313339
}
314340

315341
// Compile the body
@@ -529,9 +555,22 @@ function emitIR(
529555
return { ok: true, ir: { type: "infinity" } };
530556
}
531557
if (scope.symbolOverrides.has(name)) {
558+
const override = scope.symbolOverrides.get(name)!;
559+
// Destructured .map() fields are encoded as propertyAccess sentinels
560+
if (override.startsWith("\0propAccess:")) {
561+
const [, objName, property] = override.split(":");
562+
return {
563+
ok: true,
564+
ir: {
565+
type: "propertyAccess",
566+
object: { type: "symbol", name: objName! },
567+
property: property!,
568+
},
569+
};
570+
}
532571
return {
533572
ok: true,
534-
ir: { type: "symbol", name: scope.symbolOverrides.get(name)! },
573+
ir: { type: "symbol", name: override },
535574
};
536575
}
537576
if (scope.localBindingNames.has(name)) {

0 commit comments

Comments
 (0)