Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/ast-analysis/visitors/cfg-conditionals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import type { TreeSitterNode } from '../../types.js';
import type {
AnyRules,
CfgBlockInternal,
FuncState,
LoopCtx,
ProcessStatementsFn,
} from './cfg-shared.js';
import { getBodyStatements, isCaseNode, isIfNode, nn } from './cfg-shared.js';

export function processIf(
ifStmt: TreeSitterNode,
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): CfgBlockInternal {
currentBlock.endLine = ifStmt.startPosition.row + 1;

const condBlock = S.makeBlock(
'condition',
ifStmt.startPosition.row + 1,
ifStmt.startPosition.row + 1,
'if',
);
S.addEdge(currentBlock, condBlock, 'fallthrough');

const joinBlock = S.makeBlock('body');

const consequentField = cfgRules.ifConsequentField || 'consequence';
const consequent = ifStmt.childForFieldName(consequentField);
const trueBlock = S.makeBlock('branch_true', null, null, 'then');
S.addEdge(condBlock, trueBlock, 'branch_true');
const trueStmts = getBodyStatements(consequent, cfgRules);
const trueEnd = processStatements(trueStmts, trueBlock, S, cfgRules);
if (trueEnd) {
S.addEdge(trueEnd, joinBlock, 'fallthrough');
}

if (cfgRules.elifNode) {
processElifSiblings(ifStmt, condBlock, joinBlock, S, cfgRules, processStatements);
} else {
processAlternative(ifStmt, condBlock, joinBlock, S, cfgRules, processStatements);
}

return joinBlock;
}

function processAlternative(
ifStmt: TreeSitterNode,
condBlock: CfgBlockInternal,
joinBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): void {
const alternative = ifStmt.childForFieldName('alternative');
if (!alternative) {
S.addEdge(condBlock, joinBlock, 'branch_false');
return;
}

if (cfgRules.elseViaAlternative && alternative.type !== cfgRules.elseClause) {
if (isIfNode(alternative.type, cfgRules)) {
const falseBlock = S.makeBlock('branch_false', null, null, 'else-if');
S.addEdge(condBlock, falseBlock, 'branch_false');
const elseIfEnd = processIf(alternative, falseBlock, S, cfgRules, processStatements);
if (elseIfEnd) S.addEdge(elseIfEnd, joinBlock, 'fallthrough');
} else {
const falseBlock = S.makeBlock('branch_false', null, null, 'else');
S.addEdge(condBlock, falseBlock, 'branch_false');
const falseStmts = getBodyStatements(alternative, cfgRules);
const falseEnd = processStatements(falseStmts, falseBlock, S, cfgRules);
if (falseEnd) S.addEdge(falseEnd, joinBlock, 'fallthrough');
}
} else if (alternative.type === cfgRules.elseClause) {
const elseChildren: TreeSitterNode[] = [];
for (let i = 0; i < alternative.namedChildCount; i++) {
elseChildren.push(nn(alternative.namedChild(i)));
}
if (elseChildren.length === 1 && isIfNode(elseChildren[0]!.type, cfgRules)) {
const falseBlock = S.makeBlock('branch_false', null, null, 'else-if');
S.addEdge(condBlock, falseBlock, 'branch_false');
const elseIfEnd = processIf(elseChildren[0]!, falseBlock, S, cfgRules, processStatements);
if (elseIfEnd) S.addEdge(elseIfEnd, joinBlock, 'fallthrough');
} else {
const falseBlock = S.makeBlock('branch_false', null, null, 'else');
S.addEdge(condBlock, falseBlock, 'branch_false');
const falseEnd = processStatements(elseChildren, falseBlock, S, cfgRules);
if (falseEnd) S.addEdge(falseEnd, joinBlock, 'fallthrough');
}
}
}

function processElifSiblings(
ifStmt: TreeSitterNode,
firstCondBlock: CfgBlockInternal,
joinBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): void {
let lastCondBlock = firstCondBlock;
let foundElse = false;

for (let i = 0; i < ifStmt.namedChildCount; i++) {
const child = nn(ifStmt.namedChild(i));

if (child.type === cfgRules.elifNode) {
const elifCondBlock = S.makeBlock(
'condition',
child.startPosition.row + 1,
child.startPosition.row + 1,
'else-if',
);
S.addEdge(lastCondBlock, elifCondBlock, 'branch_false');

const elifConsequentField = cfgRules.ifConsequentField || 'consequence';
const elifConsequent = child.childForFieldName(elifConsequentField);
const elifTrueBlock = S.makeBlock('branch_true', null, null, 'then');
S.addEdge(elifCondBlock, elifTrueBlock, 'branch_true');
const elifTrueStmts = getBodyStatements(elifConsequent, cfgRules);
const elifTrueEnd = processStatements(elifTrueStmts, elifTrueBlock, S, cfgRules);
if (elifTrueEnd) S.addEdge(elifTrueEnd, joinBlock, 'fallthrough');

lastCondBlock = elifCondBlock;
} else if (child.type === cfgRules.elseClause) {
const elseBlock = S.makeBlock('branch_false', null, null, 'else');
S.addEdge(lastCondBlock, elseBlock, 'branch_false');

const elseBody = child.childForFieldName('body');
let elseStmts: TreeSitterNode[];
if (elseBody) {
elseStmts = getBodyStatements(elseBody, cfgRules);
} else {
elseStmts = [];
for (let j = 0; j < child.namedChildCount; j++) {
elseStmts.push(nn(child.namedChild(j)));
}
}
const elseEnd = processStatements(elseStmts, elseBlock, S, cfgRules);
if (elseEnd) S.addEdge(elseEnd, joinBlock, 'fallthrough');

foundElse = true;
}
}

if (!foundElse) {
S.addEdge(lastCondBlock, joinBlock, 'branch_false');
}
}

export function processSwitch(
switchStmt: TreeSitterNode,
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): CfgBlockInternal {
currentBlock.endLine = switchStmt.startPosition.row + 1;

const switchHeader = S.makeBlock(
'condition',
switchStmt.startPosition.row + 1,
switchStmt.startPosition.row + 1,
'switch',
);
S.addEdge(currentBlock, switchHeader, 'fallthrough');

const joinBlock = S.makeBlock('body');
const switchCtx: LoopCtx = { headerBlock: switchHeader, exitBlock: joinBlock };
S.loopStack.push(switchCtx);

const switchBody = switchStmt.childForFieldName('body');
const container = switchBody || switchStmt;

let hasDefault = false;
for (let i = 0; i < container.namedChildCount; i++) {
const caseClause = nn(container.namedChild(i));

const isDefault = caseClause.type === cfgRules.defaultNode;
const isCase = isDefault || isCaseNode(caseClause.type, cfgRules);
if (!isCase) continue;

const caseLabel = isDefault ? 'default' : 'case';
const caseBlock = S.makeBlock('case', caseClause.startPosition.row + 1, null, caseLabel);
S.addEdge(switchHeader, caseBlock, isDefault ? 'branch_false' : 'branch_true');
if (isDefault) hasDefault = true;

const caseStmts = extractCaseBody(caseClause, cfgRules);
const caseEnd = processStatements(caseStmts, caseBlock, S, cfgRules);
if (caseEnd) S.addEdge(caseEnd, joinBlock, 'fallthrough');
}

if (!hasDefault) {
S.addEdge(switchHeader, joinBlock, 'branch_false');
}

S.loopStack.pop();
return joinBlock;
}

function extractCaseBody(caseClause: TreeSitterNode, cfgRules: AnyRules): TreeSitterNode[] {
const caseBodyNode =
caseClause.childForFieldName('body') || caseClause.childForFieldName('consequence');
if (caseBodyNode) {
return getBodyStatements(caseBodyNode, cfgRules);
}

const stmts: TreeSitterNode[] = [];
const valueNode = caseClause.childForFieldName('value');
const patternNode = caseClause.childForFieldName('pattern');
for (let j = 0; j < caseClause.namedChildCount; j++) {
const child = nn(caseClause.namedChild(j));
if (child !== valueNode && child !== patternNode && child.type !== 'switch_label') {
if (child.type === 'statement_list') {
for (let k = 0; k < child.namedChildCount; k++) {
stmts.push(nn(child.namedChild(k)));
}
} else {
stmts.push(child);
}
}
}
return stmts;
}
136 changes: 136 additions & 0 deletions src/ast-analysis/visitors/cfg-loops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { TreeSitterNode } from '../../types.js';
import type {
AnyRules,
CfgBlockInternal,
FuncState,
LoopCtx,
ProcessStatementsFn,
} from './cfg-shared.js';
import { getBodyStatements, registerLabelCtx } from './cfg-shared.js';

export function processForLoop(
forStmt: TreeSitterNode,
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): CfgBlockInternal {
const headerBlock = S.makeBlock(
'loop_header',
forStmt.startPosition.row + 1,
forStmt.startPosition.row + 1,
'for',
);
S.addEdge(currentBlock, headerBlock, 'fallthrough');

const loopExitBlock = S.makeBlock('body');
const loopCtx: LoopCtx = { headerBlock, exitBlock: loopExitBlock };
S.loopStack.push(loopCtx);
registerLabelCtx(S, headerBlock, loopExitBlock);

const body = forStmt.childForFieldName('body');
const bodyBlock = S.makeBlock('loop_body');
S.addEdge(headerBlock, bodyBlock, 'branch_true');

const bodyStmts = getBodyStatements(body, cfgRules);
const bodyEnd = processStatements(bodyStmts, bodyBlock, S, cfgRules);
if (bodyEnd) S.addEdge(bodyEnd, headerBlock, 'loop_back');

S.addEdge(headerBlock, loopExitBlock, 'loop_exit');
S.loopStack.pop();
return loopExitBlock;
}

export function processWhileLoop(
whileStmt: TreeSitterNode,
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): CfgBlockInternal {
const headerBlock = S.makeBlock(
'loop_header',
whileStmt.startPosition.row + 1,
whileStmt.startPosition.row + 1,
'while',
);
S.addEdge(currentBlock, headerBlock, 'fallthrough');

const loopExitBlock = S.makeBlock('body');
const loopCtx: LoopCtx = { headerBlock, exitBlock: loopExitBlock };
S.loopStack.push(loopCtx);
registerLabelCtx(S, headerBlock, loopExitBlock);

const body = whileStmt.childForFieldName('body');
const bodyBlock = S.makeBlock('loop_body');
S.addEdge(headerBlock, bodyBlock, 'branch_true');

const bodyStmts = getBodyStatements(body, cfgRules);
const bodyEnd = processStatements(bodyStmts, bodyBlock, S, cfgRules);
if (bodyEnd) S.addEdge(bodyEnd, headerBlock, 'loop_back');

S.addEdge(headerBlock, loopExitBlock, 'loop_exit');
S.loopStack.pop();
return loopExitBlock;
}

export function processDoWhileLoop(
doStmt: TreeSitterNode,
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): CfgBlockInternal {
const bodyBlock = S.makeBlock('loop_body', doStmt.startPosition.row + 1, null, 'do');
S.addEdge(currentBlock, bodyBlock, 'fallthrough');

const condBlock = S.makeBlock('loop_header', null, null, 'do-while');
const loopExitBlock = S.makeBlock('body');

const loopCtx: LoopCtx = { headerBlock: condBlock, exitBlock: loopExitBlock };
S.loopStack.push(loopCtx);
registerLabelCtx(S, condBlock, loopExitBlock);

const body = doStmt.childForFieldName('body');
const bodyStmts = getBodyStatements(body, cfgRules);
const bodyEnd = processStatements(bodyStmts, bodyBlock, S, cfgRules);
if (bodyEnd) S.addEdge(bodyEnd, condBlock, 'fallthrough');

S.addEdge(condBlock, bodyBlock, 'loop_back');
S.addEdge(condBlock, loopExitBlock, 'loop_exit');

S.loopStack.pop();
return loopExitBlock;
}

export function processInfiniteLoop(
loopStmt: TreeSitterNode,
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
processStatements: ProcessStatementsFn,
): CfgBlockInternal {
const headerBlock = S.makeBlock(
'loop_header',
loopStmt.startPosition.row + 1,
loopStmt.startPosition.row + 1,
'loop',
);
S.addEdge(currentBlock, headerBlock, 'fallthrough');

const loopExitBlock = S.makeBlock('body');
const loopCtx: LoopCtx = { headerBlock, exitBlock: loopExitBlock };
S.loopStack.push(loopCtx);
registerLabelCtx(S, headerBlock, loopExitBlock);

const body = loopStmt.childForFieldName('body');
const bodyBlock = S.makeBlock('loop_body');
S.addEdge(headerBlock, bodyBlock, 'branch_true');

const bodyStmts = getBodyStatements(body, cfgRules);
const bodyEnd = processStatements(bodyStmts, bodyBlock, S, cfgRules);
if (bodyEnd) S.addEdge(bodyEnd, headerBlock, 'loop_back');

S.loopStack.pop();
return loopExitBlock;
}
Loading
Loading