Skip to content

Commit d4656e4

Browse files
ziadziad
authored andcommitted
* fix bugs with branch tables and loops with multiple exits
* sort function names
1 parent 5669b9a commit d4656e4

7 files changed

Lines changed: 161 additions & 50 deletions

File tree

playground/explorer.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,12 +1533,12 @@ export default class Explorer {
15331533
}
15341534
return {
15351535
label,
1536-
section: 'function',
1536+
section: 'function' as const,
15371537
index: funcIndex,
15381538
tooltip: `func ${globalIndex}\n${tipSignature}\n${funcEntry.body.length} bytes, ${totalLocals} locals`,
15391539
heatColor,
15401540
};
1541-
}),
1541+
}).sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true })),
15421542
};
15431543
root.children!.push(functionsNode);
15441544
}
@@ -3951,9 +3951,7 @@ export default class Explorer {
39513951
row.className = 'detail-info-row';
39523952

39533953
const nameLink = document.createElement('a');
3954-
nameLink.className = 'detail-info-link';
3955-
nameLink.style.flex = '0 0 auto';
3956-
nameLink.style.maxWidth = '50%';
3954+
nameLink.className = 'detail-info-link module-interface-name';
39573955
nameLink.textContent = importEntry.fieldName;
39583956
nameLink.title = `${importEntry.moduleName}.${importEntry.fieldName}`;
39593957
nameLink.href = '#';
@@ -4025,9 +4023,7 @@ export default class Explorer {
40254023
const navIndex = (targetSection && targetItemIndex >= 0) ? targetItemIndex : exportIndex;
40264024

40274025
const nameLink = document.createElement('a');
4028-
nameLink.className = 'detail-info-link';
4029-
nameLink.style.flex = '0 0 auto';
4030-
nameLink.style.maxWidth = '50%';
4026+
nameLink.className = 'detail-info-link module-interface-name';
40314027
nameLink.textContent = exportEntry.name;
40324028
nameLink.title = exportEntry.name;
40334029
nameLink.href = '#';

playground/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,10 @@
12961296
margin: 0;
12971297
}
12981298

1299+
.module-interface-name {
1300+
flex: 0 0 280px;
1301+
}
1302+
12991303
/* ─── Function tabs ─── */
13001304

13011305
.func-tab-container { margin: 12px 20px; }

playground/playground.bundle.js

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23139,9 +23139,9 @@ log(mod.toString());`
2313923139
condition: terminator.condition,
2314023140
body: prependNode(preBody, bodyNode2)
2314123141
};
23142-
const exitId2 = falseTarget;
23143-
if (exitId2 !== regionEnd && !processed.has(exitId2)) {
23144-
const afterLoop = structureRegion(exitId2, regionEnd);
23142+
const exitId = falseTarget;
23143+
if (exitId !== regionEnd && !processed.has(exitId)) {
23144+
const afterLoop = structureRegion(exitId, regionEnd);
2314523145
return { kind: "sequence", children: [whileNode2, afterLoop] };
2314623146
}
2314723147
return whileNode2;
@@ -23155,22 +23155,39 @@ log(mod.toString());`
2315523155
condition: negatedCondition,
2315623156
body: prependNode(preBody, bodyNode2)
2315723157
};
23158-
const exitId2 = trueTarget;
23159-
if (exitId2 !== regionEnd && !processed.has(exitId2)) {
23160-
const afterLoop = structureRegion(exitId2, regionEnd);
23158+
const exitId = trueTarget;
23159+
if (exitId !== regionEnd && !processed.has(exitId)) {
23160+
const afterLoop = structureRegion(exitId, regionEnd);
2316123161
return { kind: "sequence", children: [whileNode2, afterLoop] };
2316223162
}
2316323163
return whileNode2;
2316423164
}
2316523165
}
2316623166
const bodyNode = structureLoopBody(headerId, headerId, loop);
2316723167
const whileNode = { kind: "while", condition: null, body: bodyNode };
23168-
const exitId = findSingleExit(loop);
23169-
if (exitId !== null && exitId !== regionEnd && !processed.has(exitId)) {
23170-
const afterLoop = structureRegion(exitId, regionEnd);
23171-
return { kind: "sequence", children: [whileNode, afterLoop] };
23168+
const afterChildren = [whileNode];
23169+
if (loop.exitIds.size > 0) {
23170+
const postDom = dominance.postImmediateDominator.get(headerId);
23171+
const convergenceId = postDom !== void 0 && !loop.bodyIds.has(postDom) ? postDom : null;
23172+
for (const exitId of loop.exitIds) {
23173+
if (exitId === regionEnd || processed.has(exitId)) {
23174+
continue;
23175+
}
23176+
if (exitId === convergenceId) {
23177+
continue;
23178+
}
23179+
const exitPath = structureRegion(exitId, convergenceId ?? regionEnd);
23180+
afterChildren.push(exitPath);
23181+
}
23182+
if (convergenceId !== null && convergenceId !== regionEnd && !processed.has(convergenceId)) {
23183+
const afterConvergence = structureRegion(convergenceId, regionEnd);
23184+
afterChildren.push(afterConvergence);
23185+
}
23186+
}
23187+
if (afterChildren.length === 1) {
23188+
return whileNode;
2317223189
}
23173-
return whileNode;
23190+
return { kind: "sequence", children: afterChildren };
2317423191
}
2317523192
function structureLoopBody(startId, headerId, loop) {
2317623193
const children = [];
@@ -23271,6 +23288,22 @@ log(mod.toString());`
2327123288
currentBlockId = ifResult.mergeBlockId;
2327223289
continue;
2327323290
}
23291+
if (terminator.kind === "branch_table") {
23292+
children.push(blockToNodeWithoutTerminator(block));
23293+
const switchResult = structureSwitch(terminator, null, currentBlockId);
23294+
children.push(switchResult.node);
23295+
currentBlockId = switchResult.mergeBlockId;
23296+
if (currentBlockId !== null) {
23297+
if (currentBlockId === headerId) {
23298+
children.push({ kind: "continue" });
23299+
currentBlockId = null;
23300+
} else if (!loop.bodyIds.has(currentBlockId)) {
23301+
children.push({ kind: "break" });
23302+
currentBlockId = null;
23303+
}
23304+
}
23305+
continue;
23306+
}
2327423307
children.push(blockToNode(block));
2327523308
break;
2327623309
}
@@ -23394,17 +23427,17 @@ log(mod.toString());`
2339423427
return { node: labeledBody, mergeBlockId: null };
2339523428
}
2339623429
function findSingleExit(loop) {
23430+
if (loop.exitIds.size === 0) {
23431+
return null;
23432+
}
2339723433
if (loop.exitIds.size === 1) {
2339823434
return loop.exitIds.values().next().value ?? null;
2339923435
}
23400-
if (loop.exitIds.size > 1) {
23401-
const postDom = dominance.postImmediateDominator.get(loop.headerId);
23402-
if (postDom !== void 0 && !loop.bodyIds.has(postDom)) {
23403-
return postDom;
23404-
}
23405-
return loop.exitIds.values().next().value ?? null;
23436+
const postDom = dominance.postImmediateDominator.get(loop.headerId);
23437+
if (postDom !== void 0 && !loop.bodyIds.has(postDom)) {
23438+
return postDom;
2340623439
}
23407-
return null;
23440+
return loop.exitIds.values().next().value ?? null;
2340823441
}
2340923442
const result = structureRegion(ssaFunc.entryBlockId, ssaFunc.exitBlockId);
2341023443
const unvisited = [];
@@ -25962,6 +25995,9 @@ log(mod.toString());`
2596225995
case "expr":
2596325996
emit(`${formatExpression(stmt.value, 0)};`);
2596425997
break;
25998+
default:
25999+
emit(`/* unknown stmt: ${stmt.kind} */`);
26000+
break;
2596526001
}
2596626002
}
2596726003
function isIntrinsicOp(op) {
@@ -26064,6 +26100,9 @@ log(mod.toString());`
2606426100
case "field_access": {
2606526101
return formatFieldAccess(expr.base, expr.offset);
2606626102
}
26103+
default: {
26104+
return `/* unknown expr: ${expr.kind} */`;
26105+
}
2606726106
}
2606826107
}
2606926108
function formatStatementInline(stmt) {
@@ -28531,7 +28570,7 @@ ${tipSignature}
2853128570
${funcEntry.body.length} bytes, ${totalLocals} locals`,
2853228571
heatColor
2853328572
};
28534-
})
28573+
}).sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true }))
2853528574
};
2853628575
root.children.push(functionsNode);
2853728576
}
@@ -30651,9 +30690,7 @@ ${funcEntry.body.length} bytes, ${totalLocals} locals`,
3065130690
const row = document.createElement("div");
3065230691
row.className = "detail-info-row";
3065330692
const nameLink = document.createElement("a");
30654-
nameLink.className = "detail-info-link";
30655-
nameLink.style.flex = "0 0 auto";
30656-
nameLink.style.maxWidth = "50%";
30693+
nameLink.className = "detail-info-link module-interface-name";
3065730694
nameLink.textContent = importEntry.fieldName;
3065830695
nameLink.title = `${importEntry.moduleName}.${importEntry.fieldName}`;
3065930696
nameLink.href = "#";
@@ -30716,9 +30753,7 @@ ${funcEntry.body.length} bytes, ${totalLocals} locals`,
3071630753
const navSection = targetSection && targetItemIndex >= 0 ? targetSection : "export";
3071730754
const navIndex = targetSection && targetItemIndex >= 0 ? targetItemIndex : exportIndex;
3071830755
const nameLink = document.createElement("a");
30719-
nameLink.className = "detail-info-link";
30720-
nameLink.style.flex = "0 0 auto";
30721-
nameLink.style.maxWidth = "50%";
30756+
nameLink.className = "detail-info-link module-interface-name";
3072230757
nameLink.textContent = exportEntry.name;
3072330758
nameLink.title = exportEntry.name;
3072430759
nameLink.href = "#";

playground/playground.bundle.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/decompiler/LoweredEmitter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ export function emitLowered(
315315
case 'expr':
316316
emit(`${formatExpression(stmt.value, 0)};`);
317317
break;
318+
default:
319+
emit(`/* unknown stmt: ${(stmt as Statement).kind} */`);
320+
break;
318321
}
319322
}
320323

@@ -426,6 +429,9 @@ export function emitLowered(
426429
case 'field_access': {
427430
return formatFieldAccess(expr.base, expr.offset);
428431
}
432+
default: {
433+
return `/* unknown expr: ${(expr as Expression).kind} */`;
434+
}
429435
}
430436
}
431437

src/decompiler/StructuralAnalysis.ts

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,31 @@ export function structureFunction(
316316
const bodyNode = structureLoopBody(headerId, headerId, loop);
317317
const whileNode: StructuredNode = { kind: 'while', condition: null, body: bodyNode };
318318

319-
const exitId = findSingleExit(loop);
320-
if (exitId !== null && exitId !== regionEnd && !processed.has(exitId)) {
321-
const afterLoop = structureRegion(exitId, regionEnd);
322-
return { kind: 'sequence', children: [whileNode, afterLoop] };
319+
// Structure code after the loop — handle multiple exit targets
320+
const afterChildren: StructuredNode[] = [whileNode];
321+
if (loop.exitIds.size > 0) {
322+
// Find the convergence point where all exits meet
323+
const postDom = dominance.postImmediateDominator.get(headerId);
324+
const convergenceId = (postDom !== undefined && !loop.bodyIds.has(postDom)) ? postDom : null;
325+
326+
// Structure each unprocessed exit path up to the convergence point
327+
for (const exitId of loop.exitIds) {
328+
if (exitId === regionEnd || processed.has(exitId)) { continue; }
329+
if (exitId === convergenceId) { continue; }
330+
const exitPath = structureRegion(exitId, convergenceId ?? regionEnd);
331+
afterChildren.push(exitPath);
332+
}
333+
334+
// Structure from convergence point onward
335+
if (convergenceId !== null && convergenceId !== regionEnd && !processed.has(convergenceId)) {
336+
const afterConvergence = structureRegion(convergenceId, regionEnd);
337+
afterChildren.push(afterConvergence);
338+
}
339+
}
340+
if (afterChildren.length === 1) {
341+
return whileNode;
323342
}
324-
return whileNode;
343+
return { kind: 'sequence', children: afterChildren };
325344
}
326345

327346
function structureLoopBody(
@@ -441,6 +460,23 @@ export function structureFunction(
441460
continue;
442461
}
443462

463+
if (terminator.kind === 'branch_table') {
464+
children.push(blockToNodeWithoutTerminator(block));
465+
const switchResult = structureSwitch(terminator, null, currentBlockId!);
466+
children.push(switchResult.node);
467+
currentBlockId = switchResult.mergeBlockId;
468+
if (currentBlockId !== null) {
469+
if (currentBlockId === headerId) {
470+
children.push({ kind: 'continue' });
471+
currentBlockId = null;
472+
} else if (!loop.bodyIds.has(currentBlockId)) {
473+
children.push({ kind: 'break' });
474+
currentBlockId = null;
475+
}
476+
}
477+
continue;
478+
}
479+
444480
children.push(blockToNode(block));
445481
break;
446482
}
@@ -594,19 +630,19 @@ export function structureFunction(
594630
}
595631

596632
function findSingleExit(loop: NaturalLoop): number | null {
633+
if (loop.exitIds.size === 0) {
634+
return null;
635+
}
597636
if (loop.exitIds.size === 1) {
598637
return loop.exitIds.values().next().value ?? null;
599638
}
600-
if (loop.exitIds.size > 1) {
601-
// Multiple exits — use post-dominator of loop header as the merge point
602-
const postDom = dominance.postImmediateDominator.get(loop.headerId);
603-
if (postDom !== undefined && !loop.bodyIds.has(postDom)) {
604-
return postDom;
605-
}
606-
// Fallback: pick the first exit
607-
return loop.exitIds.values().next().value ?? null;
639+
// Multiple exits — use post-dominator of loop header as the convergence point
640+
const postDom = dominance.postImmediateDominator.get(loop.headerId);
641+
if (postDom !== undefined && !loop.bodyIds.has(postDom)) {
642+
return postDom;
608643
}
609-
return null;
644+
// Fallback: pick the first exit
645+
return loop.exitIds.values().next().value ?? null;
610646
}
611647

612648
const result = structureRegion(ssaFunc.entryBlockId, ssaFunc.exitBlockId);

tests/decompiler/ControlFlow.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,40 @@ describe('Decompiler: Switch (br_table)', () => {
308308
expect(output).toContain('switch');
309309
expect(output).toContain('case');
310310
});
311+
312+
test('switch inside loop dispatches cases', () => {
313+
const output = expectDecompiles((mod) => {
314+
mod.defineMemory(1);
315+
const readByte = mod.importFunction('env', 'readByte', [ValueType.Int32], []);
316+
mod.defineFunction('f', null, [], (f, a) => {
317+
const lp = a.loop(BlockType.Void);
318+
a.call(readByte);
319+
const blockDefault = a.block(BlockType.Void);
320+
const block1 = a.block(BlockType.Void);
321+
const block0 = a.block(BlockType.Void);
322+
a.br_table(blockDefault, block0, block1);
323+
a.end(); // block0
324+
a.const_i32(0);
325+
a.const_i32(100);
326+
a.store_i32(2, 0);
327+
a.br(lp);
328+
a.end(); // block1
329+
a.const_i32(0);
330+
a.const_i32(200);
331+
a.store_i32(2, 4);
332+
a.br(lp);
333+
a.end(); // blockDefault
334+
a.return();
335+
a.end(); // loop
336+
});
337+
});
338+
expect(output).toContain('while');
339+
expect(output).toContain('switch');
340+
expect(output).toContain('case');
341+
expect(output).toContain('100');
342+
expect(output).toContain('200');
343+
expect(output).not.toContain('unvisited');
344+
});
311345
});
312346

313347
describe('Decompiler: Do-While Loop', () => {

0 commit comments

Comments
 (0)