Skip to content

Commit 46e68e5

Browse files
Fix NormalizeToSubset creating duplicated symbols
- Add symbol table to StatementDecomposer to track existing variable names - Check symbol table before generating new decomp_N variable names - Add useGlobalIds option to use global IdGenerator for unique names across multiple normalizations - Update NormalizeToSubset to support useGlobalIds option - Add test cases for duplicate symbol detection Co-authored-by: tiagolascasas <25725952+tiagolascasas@users.noreply.github.com>
1 parent be36240 commit 46e68e5

File tree

3 files changed

+169
-16
lines changed

3 files changed

+169
-16
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { registerSourceCode } from "@specs-feup/lara/jest/jestHelpers.js";
2+
import Query from "@specs-feup/lara/api/weaver/Query.js";
3+
import { FunctionJp, Vardecl } from "../../Joinpoints.js";
4+
import NormalizeToSubset from "../opt/NormalizeToSubset.js";
5+
6+
const codeWithExistingDecompVars = `
7+
int foo(int a) {
8+
int decomp_0 = 5;
9+
int decomp_1 = 10;
10+
int result = (a + 1) * (decomp_0 + decomp_1);
11+
return result;
12+
}`;
13+
14+
describe("StatementDecomposer duplicate symbols", () => {
15+
registerSourceCode(codeWithExistingDecompVars);
16+
17+
it("should not create duplicate decomp_ symbols when normalizing", () => {
18+
const functionJp = Query.search(FunctionJp, { name: "foo" }).first();
19+
20+
if (functionJp === undefined) {
21+
fail("Function not found");
22+
}
23+
24+
// Get all variable names before normalization
25+
const varsBefore = Query.searchFrom(functionJp, Vardecl).map(v => v.name);
26+
expect(varsBefore).toContain("decomp_0");
27+
expect(varsBefore).toContain("decomp_1");
28+
29+
// Apply normalization
30+
NormalizeToSubset(functionJp);
31+
32+
// Get all variable names after normalization
33+
const varsAfter = Query.searchFrom(functionJp, Vardecl).map(v => v.name);
34+
35+
// Check that all variable names are unique
36+
const uniqueVars = new Set(varsAfter);
37+
expect(uniqueVars.size).toBe(varsAfter.length);
38+
39+
// Ensure no duplicate decomp_0 or decomp_1
40+
const decompCounts: Record<string, number> = {};
41+
for (const varName of varsAfter) {
42+
if (varName.startsWith("decomp_")) {
43+
decompCounts[varName] = (decompCounts[varName] || 0) + 1;
44+
}
45+
}
46+
47+
for (const [varName, count] of Object.entries(decompCounts)) {
48+
expect(count).toBe(1);
49+
}
50+
});
51+
});
52+
53+
const codeMultipleNormalization = `
54+
int bar(int x) {
55+
return (x + 1) * (x + 2);
56+
}`;
57+
58+
describe("StatementDecomposer multiple normalizations", () => {
59+
registerSourceCode(codeMultipleNormalization);
60+
61+
it("should not create duplicate symbols when normalizing multiple times", () => {
62+
const functionJp = Query.search(FunctionJp, { name: "bar" }).first();
63+
64+
if (functionJp === undefined) {
65+
fail("Function not found");
66+
}
67+
68+
// Apply normalization twice
69+
NormalizeToSubset(functionJp);
70+
NormalizeToSubset(functionJp);
71+
72+
// Get all variable names
73+
const vars = Query.searchFrom(functionJp, Vardecl).map(v => v.name);
74+
75+
// Check that all variable names are unique
76+
const uniqueVars = new Set(vars);
77+
expect(uniqueVars.size).toBe(vars.length);
78+
});
79+
});

Clava-JS/src-api/clava/code/StatementDecomposer.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { debug } from "@specs-feup/lara/api/lara/core/LaraCore.js";
2+
import IdGenerator from "@specs-feup/lara/api/lara/util/IdGenerator.js";
3+
import Query from "@specs-feup/lara/api/weaver/Query.js";
24
import {
35
BinaryOp,
46
Call,
57
Case,
6-
Decl,
8+
type Decl,
79
DeclStmt,
810
EmptyStmt,
911
ExprStmt,
10-
Expression,
11-
Joinpoint,
12+
type Expression,
13+
FunctionJp,
14+
type Joinpoint,
1215
LabelStmt,
1316
MemberCall,
17+
type Param,
1418
ReturnStmt,
1519
Scope,
1620
Statement,
@@ -25,17 +29,73 @@ import DecomposeResult from "./DecomposeResult.js";
2529
* Decomposes complex statements into several simpler ones.
2630
*/
2731
export default class StatementDecomposer {
28-
tempPrefix;
29-
startIndex;
32+
public tempPrefix;
33+
public startIndex;
34+
public useGlobalIds;
35+
private symbolTable: Set<string> | undefined;
3036

31-
constructor(tempPrefix: string = "decomp_", startIndex: number = 0) {
37+
/**
38+
* Creates a new StatementDecomposer.
39+
*
40+
* @param tempPrefix - Prefix for temporary variable names
41+
* @param startIndex - Starting index for temporary variable names (ignored if useGlobalIds is true)
42+
* @param useGlobalIds - If true, uses global IdGenerator to avoid duplicate symbols across multiple normalizations
43+
*/
44+
public constructor(tempPrefix = "decomp_", startIndex = 0, useGlobalIds = false) {
3245
this.tempPrefix = tempPrefix;
3346
this.startIndex = startIndex;
47+
this.useGlobalIds = useGlobalIds;
48+
}
49+
50+
/**
51+
* Builds a symbol table of existing variable declarations and parameters in the given scope.
52+
* This is used to avoid creating duplicate variable names.
53+
*
54+
* @param $scope - The scope to build the symbol table from (typically a function)
55+
*/
56+
private buildSymbolTable($scope: Joinpoint): void {
57+
this.symbolTable = new Set<string>();
58+
59+
// Get the enclosing function if we're not already at a function
60+
const $function = $scope instanceof FunctionJp
61+
? $scope
62+
: $scope.getAncestor("function") as FunctionJp | undefined;
63+
64+
if ($function === undefined) {
65+
return;
66+
}
67+
68+
// Add parameter names to symbol table
69+
for (const $param of $function.params as Param[]) {
70+
this.symbolTable.add($param.name);
71+
}
72+
73+
// Add all variable declarations in the function to symbol table
74+
for (const $vardecl of Query.searchFrom($function, Vardecl)) {
75+
this.symbolTable.add($vardecl.name);
76+
}
3477
}
3578

3679
private newTempVarname() {
37-
const varName = `${this.tempPrefix}${this.startIndex}`;
80+
if (this.useGlobalIds) {
81+
// Use global IdGenerator to ensure unique names across multiple normalizations
82+
return IdGenerator.next(this.tempPrefix);
83+
}
84+
85+
// Use local counter with symbol table checking to avoid duplicates
86+
let varName = `${this.tempPrefix}${this.startIndex}`;
3887
this.startIndex++;
88+
89+
// If we have a symbol table, ensure the name is unique
90+
if (this.symbolTable !== undefined) {
91+
while (this.symbolTable.has(varName)) {
92+
varName = `${this.tempPrefix}${this.startIndex}`;
93+
this.startIndex++;
94+
}
95+
// Add the new name to the symbol table
96+
this.symbolTable.add(varName);
97+
}
98+
3999
return varName;
40100
}
41101

@@ -94,6 +154,11 @@ export default class StatementDecomposer {
94154
* @returns An array with the new statements, or an empty array if no decomposition could be made
95155
*/
96156
decompose($stmt: Statement): Statement[] {
157+
// Build symbol table once per decomposition operation to avoid duplicate variable names
158+
if (this.symbolTable === undefined) {
159+
this.buildSymbolTable($stmt);
160+
}
161+
97162
try {
98163
return this.decomposeStmt($stmt);
99164
} catch (e) {

Clava-JS/src-api/clava/opt/NormalizeToSubset.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { LaraJoinPoint } from "@specs-feup/lara/api/LaraJoinPoint.js";
21
import Query from "@specs-feup/lara/api/weaver/Query.js";
3-
import { BinaryOp, Joinpoint } from "../../Joinpoints.js";
2+
import type { Joinpoint } from "../../Joinpoints.js";
3+
import { BinaryOp } from "../../Joinpoints.js";
44
import SimplifyAssignment from "../code/SimplifyAssignment.js";
55
import StatementDecomposer from "../code/StatementDecomposer.js";
66
import DecomposeDeclStmt from "../pass/DecomposeDeclStmt.js";
@@ -11,22 +11,31 @@ import SimplifyReturnStmts from "../pass/SimplifyReturnStmts.js";
1111
import SimplifySelectionStmts from "../pass/SimplifySelectionStmts.js";
1212

1313
/**
14-
*
15-
* @param $startJp -
16-
* @param options - Object with options. See default value for supported options.
14+
* Normalizes code to a simpler subset of C/C++.
15+
*
16+
* @param $startJp - Starting join point for normalization
17+
* @param options - Configuration options for normalization
1718
*/
1819
export default function NormalizeToSubset(
1920
$startJp: Joinpoint,
20-
options = { simplifyLoops: { forToWhile: true } }
21+
options: { simplifyLoops?: { forToWhile: boolean }, useGlobalIds?: boolean } = {}
2122
) {
22-
const _options = options;
23+
const _options = {
24+
simplifyLoops: { forToWhile: true },
25+
useGlobalIds: false,
26+
...options
27+
};
2328

2429
const declStmt = new DecomposeDeclStmt();
2530
const varDecls = new DecomposeVarDeclarations();
26-
const statementDecomposer = new StatementDecomposer();
31+
const statementDecomposer = new StatementDecomposer(
32+
"decomp_",
33+
0,
34+
_options.useGlobalIds
35+
);
2736
const simplifyLoops = new SimplifyLoops(
2837
statementDecomposer,
29-
_options["simplifyLoops"]
38+
_options.simplifyLoops
3039
);
3140
const simplifyIfs = new SimplifySelectionStmts(statementDecomposer);
3241
const simplifyReturns = new SimplifyReturnStmts(statementDecomposer);

0 commit comments

Comments
 (0)