[new-arch] Store HIR blocks in reverse postorder

* Changes HIR to store blocks in reverse postorder, which allows forward data 
flow analysis to iterate the blocks in order and (in the absence of loops) see 
all predecessors before visiting a successor. 

* Updates reference kind inference to exploit this ordering 

Note that the approach of modifying the ordering in `mapTerminalSuccessors()` 
feels gross, i'd like to split this up a bit.
This commit is contained in:
Joseph Savona
2022-10-12 16:11:15 -07:00
parent 3d4130140a
commit 5149ce0fbb
8 changed files with 262 additions and 93 deletions
+8
View File
@@ -72,6 +72,13 @@ export type HIRFunction = {
*/
export type HIR = {
entry: BlockId;
/**
* Basic blocks are stored as a map to aid certain operations that need to
* lookup blocks by their id. However, the order of the items in the map is
* reverse postorder, that is, barring cycles, predecessors appear before
* successors. This is designed to facilitate forward data flow analysis.
*/
blocks: Map<BlockId, BasicBlock>;
};
@@ -84,6 +91,7 @@ export type HIR = {
* statements and not implicit exceptions which may occur.
*/
export type BasicBlock = {
id: BlockId;
instructions: Array<Instruction>;
terminal: Terminal;
};
+49 -36
View File
@@ -111,6 +111,7 @@ export default class HIRBuilder {
build(): HIR {
const { id: blockId, instructions } = this.#current;
this.#completed.set(blockId, {
id: blockId,
instructions,
terminal: { kind: "return", value: null },
});
@@ -126,6 +127,7 @@ export default class HIRBuilder {
terminate(terminal: Terminal) {
const { id: blockId, instructions } = this.#current;
this.#completed.set(blockId, {
id: blockId,
instructions,
terminal,
});
@@ -140,6 +142,7 @@ export default class HIRBuilder {
terminateWithContinuation(terminal: Terminal, continuation: WipBlock) {
const { id: blockId, instructions } = this.#current;
this.#completed.set(blockId, {
id: blockId,
instructions,
terminal,
});
@@ -160,7 +163,7 @@ export default class HIRBuilder {
*/
complete(block: WipBlock, terminal: Terminal) {
const { id: blockId, instructions } = block;
this.#completed.set(blockId, { instructions, terminal });
this.#completed.set(blockId, { id: blockId, instructions, terminal });
}
/**
@@ -175,7 +178,7 @@ export default class HIRBuilder {
this.#current = newBlock(nextId);
const terminal = fn(nextId);
const { id: blockId, instructions } = this.#current;
this.#completed.set(blockId, { instructions, terminal });
this.#completed.set(blockId, { id: blockId, instructions, terminal });
this.#current = current;
return nextId;
}
@@ -288,7 +291,10 @@ export default class HIRBuilder {
/**
* Helper to shrink a CFG to eliminate unreachable node and eliminate jump-only blocks.
*/
function shrink(func: HIR): HIR {
function shrink(func: {
blocks: Map<BlockId, BasicBlock>;
entry: BlockId;
}): HIR {
const gotos = new Map();
/**
* Given a target block for some terminator, resolves the ideal block that should be
@@ -313,45 +319,49 @@ function shrink(func: HIR): HIR {
}
}
/**
* Fixpoint iteration to explore all blocks reachable from the entry block,
* and to resolve their terminator targets to avoid indirections.
* This implicitly prunes unreachable blocks since they are never visited and
* therefore not added to the output.
*/
const queue = [func.entry];
// new set of output blocks
const visited: Set<BlockId> = new Set();
const blocks: Map<BlockId, BasicBlock> = new Map();
while (queue.length !== 0) {
const blockId = queue.shift()!;
if (blocks.has(blockId)) {
continue;
}
const { instructions, terminal: prevTerminal } = func.blocks.get(blockId)!;
const terminal = mapTerminalSuccessors(prevTerminal, (prevTarget) => {
const target = resolveBlockTarget(prevTarget);
queue.push(target);
return target;
});
blocks.set(blockId, {
instructions,
terminal,
});
}
for (const block of blocks.values()) {
// Visit all the blocks and map their successors to remove indirections,
// and eliminate unreachable blocks. Stores blocks in postorder,
// successors before predecessors.
function visit(blockId: BlockId) {
visited.add(blockId);
const block = func.blocks.get(blockId)!;
const { instructions, terminal: prevTerminal } = block;
const terminal = mapTerminalSuccessors(
prevTerminal,
(prevTarget, isFallthrough) => {
const target = resolveBlockTarget(prevTarget);
if (!visited.has(target) && !isFallthrough) {
visit(target);
}
return target;
}
);
block.terminal = terminal;
blocks.set(blockId, block);
}
visit(func.entry);
// Cleanup any fallthrough blocks that weren't visited
// also store into reverse postorder.
const reversedBlocks: Map<BlockId, BasicBlock> = new Map();
for (const blockId of Array.from(blocks.keys()).reverse()) {
const block = blocks.get(blockId)!;
if (block.terminal.kind === "if" || block.terminal.kind === "switch") {
if (
block.terminal.fallthrough !== null &&
!blocks.has(block.terminal.fallthrough)
!visited.has(block.terminal.fallthrough)
) {
block.terminal.fallthrough = null;
}
}
reversedBlocks.set(blockId, block);
}
return {
blocks,
blocks: reversedBlocks,
entry: func.entry,
};
}
@@ -368,6 +378,9 @@ function getTargetIfIndirection(block: BasicBlock): number | null {
/**
* Maps a terminal node's block assignments using the provided function.
*
* TODO: this visits successors in reverse ordering to facilitate shrink()'s
* goal of producing a reverse postorder graph where siblings are in-order.
*/
export function mapTerminalSuccessors(
terminal: Terminal,
@@ -382,10 +395,10 @@ export function mapTerminalSuccessors(
};
}
case "if": {
const consequent = fn(terminal.consequent, false);
const alternate = fn(terminal.alternate, false);
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough, true) : null;
const alternate = fn(terminal.alternate, false);
const consequent = fn(terminal.consequent, false);
return {
kind: "if",
test: terminal.test,
@@ -395,19 +408,19 @@ export function mapTerminalSuccessors(
};
}
case "switch": {
const cases = terminal.cases.map((case_) => {
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough, true) : null;
const cases = [...terminal.cases].reverse().map((case_) => {
const target = fn(case_.block, false);
return {
test: case_.test,
block: target,
};
});
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough, true) : null;
return {
kind: "switch",
test: terminal.test,
cases,
cases: cases.reverse(),
fallthrough,
};
}
@@ -99,51 +99,58 @@ export default function inferReferenceCapability(fn: HIRFunction) {
initialEnvironment.define(place, value);
}
// Queue of blocks to visit, with block and the incoming Environment value
const queue: Array<QueueEntry> = [
{ blockId: fn.body.entry, environment: initialEnvironment },
];
// Map of blocks to the last incoming environment that was processed
// Map of blocks to the last (merged) incoming environment that was processed
const environmentsByBlock: Map<BlockId, Environment> = new Map();
while (queue.length !== 0) {
const { blockId, environment: incomingEnvironment } = queue.shift()!;
let previousEnvironment = environmentsByBlock.get(blockId);
let nextEnvironment: Environment | null = null;
if (previousEnvironment === undefined) {
// If no previous environment, save the incoming environment and
// infer the block with this environment
environmentsByBlock.set(blockId, incomingEnvironment);
nextEnvironment = incomingEnvironment.clone();
// Multiple predecessors may be visited prior to reaching a given successor,
// so track the list of incoming environments for each successor block.
// These are merged when reaching that block again.
const queuedEnvironments: Map<BlockId, Environment> = new Map();
function queue(blockId: BlockId, environment: Environment) {
let queuedEnvironment = queuedEnvironments.get(blockId);
if (queuedEnvironment != null) {
// merge the queued environments for this block
environment = queuedEnvironment.merge(environment) ?? environment;
queuedEnvironments.set(blockId, environment);
} else {
// If there's a previous environment, merge the previous/new incoming
// environments. If there are no changes, then the block can be skipped.
// otherwise save the merged environment and revisit the block with it.
const mergedEnvironment = previousEnvironment.merge(incomingEnvironment);
if (mergedEnvironment !== null) {
environmentsByBlock.set(blockId, mergedEnvironment);
nextEnvironment = mergedEnvironment.clone();
} else {
continue;
// this is the first queued environment for this block, see whether
// there are changed relative to the last time it was processed.
const prevEnvironment = environmentsByBlock.get(blockId);
const nextEnvironment =
prevEnvironment != null
? prevEnvironment.merge(environment)
: environment;
if (nextEnvironment != null) {
queuedEnvironments.set(blockId, nextEnvironment);
}
}
}
queue(fn.body.entry, initialEnvironment);
const environment = nextEnvironment; // rebind to preserve the non-null refinement
const block = fn.body.blocks.get(blockId)!;
inferBlock(environment, block);
// TODO: add a `forEachTerminalSuccessor` helper, we don't actually want the result
// here
const _ = mapTerminalSuccessors(
block.terminal,
(nextBlockId, isFallthrough) => {
if (!isFallthrough) {
queue.push({ blockId: nextBlockId, environment });
}
return nextBlockId;
while (queuedEnvironments.size !== 0) {
for (const [blockId, block] of fn.body.blocks) {
const incomingEnvironment = queuedEnvironments.get(blockId);
queuedEnvironments.delete(blockId);
if (incomingEnvironment == null) {
continue;
}
);
environmentsByBlock.set(blockId, incomingEnvironment);
const environment = incomingEnvironment.clone();
inferBlock(environment, block);
// TODO: add a `forEachTerminalSuccessor` helper, we don't actually want the result
// here
const _ = mapTerminalSuccessors(
block.terminal,
(nextBlockId, isFallthrough) => {
if (!isFallthrough) {
queue(nextBlockId, environment);
}
return nextBlockId;
}
);
}
}
}
@@ -48,17 +48,6 @@ bb3:
Const mutable $11 = null
Const mutable $12 = Binary readonly item$10 == readonly $11
If (readonly $12) then:bb8 else:bb9
bb2:
Const mutable count$17 = readonly renderedItems$4.length
Const mutable $18 = "div"
Const mutable $19 = "\n "
Const mutable $20 = "h1"
Const mutable $21 = " Items"
Const mutable $22 = JSX <readonly $20>{freeze count$17}{readonly $21}</readonly $20>
Const mutable $23 = "\n "
Const mutable $24 = "\n "
Const mutable $25 = JSX <readonly $18>{readonly $19}{readonly $22}{readonly $23}{freeze renderedItems$4}{readonly $24}</readonly $18>
Return readonly $25
bb8:
Const mutable $13 = readonly $12
Goto bb7
@@ -74,6 +63,17 @@ bb4:
Call mutable renderedItems$4.push(readonly $15)
Const mutable $16 = Binary readonly renderedItems$4.length >= readonly max$7
If (readonly $16) then:bb2 else:bb1
bb2:
Const mutable count$17 = readonly renderedItems$4.length
Const mutable $18 = "div"
Const mutable $19 = "\n "
Const mutable $20 = "h1"
Const mutable $21 = " Items"
Const mutable $22 = JSX <readonly $20>{freeze count$17}{readonly $21}</readonly $20>
Const mutable $23 = "\n "
Const mutable $24 = "\n "
Const mutable $25 = JSX <readonly $18>{readonly $19}{readonly $22}{readonly $23}{freeze renderedItems$4}{readonly $24}</readonly $18>
Return readonly $25
```
## Code
@@ -201,12 +201,12 @@ bb0:
Const mutable a$2 = Array []
Call mutable a$2.push(readonly props$1.a)
If (readonly props$1.b) then:bb1 else:bb2
bb1:
Call mutable a$2.push(readonly props$1.d)
Return freeze a$2
bb2:
Call mutable a$2.push(readonly props$1.c)
Goto bb1
bb1:
Call mutable a$2.push(readonly props$1.d)
Return freeze a$2
```
## Code
@@ -0,0 +1,114 @@
## Input
```javascript
function Component(props) {
let x;
if (props.cond) {
switch (props.test) {
case 0: {
x = props.v0;
break;
}
case 1: {
x = props.v1;
break;
}
case 2: {
}
default: {
x = props.v2;
}
}
} else {
if (props.cond2) {
x = props.b;
} else {
x = props.c;
}
}
x;
}
```
## HIR
```
bb0:
Let mutable x$2 = undefined
If (readonly props$1.cond) then:bb2 else:bb10
bb2:
Const mutable $3 = 2
Const mutable $4 = 1
Const mutable $5 = 0
Switch (<unknown> props$1.test)
Case readonly $5: bb8
Case readonly $4: bb6
Case readonly $3: bb4
Default: bb4
bb8:
Reassign mutable x$2 = readonly props$1.v0
Goto bb1
bb6:
Reassign mutable x$2 = readonly props$1.v1
Goto bb1
bb4:
Reassign mutable x$2 = readonly props$1.v2
Goto bb1
bb10:
If (readonly props$1.cond2) then:bb12 else:bb13
bb12:
Reassign mutable x$2 = readonly props$1.b
Goto bb1
bb13:
Reassign mutable x$2 = readonly props$1.c
Goto bb1
bb1:
readonly x$2
Return
```
## Code
```javascript
function Component$0(props$1) {
let x$2 = undefined;
if (props$1.cond) {
switch (props$1.test) {
case 0: {
x$2 = props$1.v0;
("<<TODO: handle complex control flow in codegen>>");
}
case 1: {
x$2 = props$1.v1;
("<<TODO: handle complex control flow in codegen>>");
}
case 2: {
x$2 = props$1.v2;
("<<TODO: handle complex control flow in codegen>>");
}
default: {
x$2 = props$1.v2;
("<<TODO: handle complex control flow in codegen>>");
}
}
x$2;
return;
} else {
if (props$1.cond2) {
x$2 = props$1.b;
("<<TODO: handle complex control flow in codegen>>");
} else {
x$2 = props$1.c;
("<<TODO: handle complex control flow in codegen>>");
}
x$2;
return;
}
x$2;
return;
}
```
@@ -0,0 +1,27 @@
function Component(props) {
let x;
if (props.cond) {
switch (props.test) {
case 0: {
x = props.v0;
break;
}
case 1: {
x = props.v1;
break;
}
case 2: {
}
default: {
x = props.v2;
}
}
} else {
if (props.cond2) {
x = props.b;
} else {
x = props.c;
}
}
x;
}
@@ -42,11 +42,6 @@ bb0:
Case readonly $5: bb6
Default: bb1
Case readonly $4: bb2
bb1:
Const mutable child$7 = JSX <readonly Component$0 data={freeze x$2} ></readonly Component$0>
Call readonly y$3.push(readonly props$1.p4)
Const mutable $8 = JSX <readonly Component$0 data={freeze y$3} >{readonly child$7}</readonly Component$0>
Return readonly $8
bb6:
Call mutable x$2.push(readonly props$1.p2)
Reassign mutable y$3 = Array []
@@ -54,6 +49,11 @@ bb6:
bb2:
Reassign mutable y$3 = readonly x$2
Goto bb1
bb1:
Const mutable child$7 = JSX <readonly Component$0 data={freeze x$2} ></readonly Component$0>
Call readonly y$3.push(readonly props$1.p4)
Const mutable $8 = JSX <readonly Component$0 data={freeze y$3} >{readonly child$7}</readonly Component$0>
Return readonly $8
```
## Code