microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
117 lines • 5.15 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeIL = void 0;
const lodash_1 = __importDefault(require("lodash"));
const il_opcodes_1 = require("./il-opcodes");
const utils_1 = require("./utils");
/**
* Normalize IL for the purposes of comparison during testing
*/
function normalizeIL(unit, opts = {}) {
opts = {
cullUnreachableBlocks: true,
cullUnreachableInstructions: true,
inlineUnnecessaryJumps: true,
...opts
};
return {
...unit,
functions: new Map((0, utils_1.entriesInOrder)(unit.functions).map(([funcID, func]) => [funcID, normalizeFunction(func)]))
};
function normalizeFunction(func) {
let blocks = func.blocks;
if (opts.cullUnreachableBlocks) {
blocks = cullUnreachableBlocks(blocks, func.entryBlockID);
}
if (opts.cullUnreachableInstructions) {
blocks = cullUnreachableInstructions(blocks);
}
if (opts.inlineUnnecessaryJumps) {
blocks = inlineUnnecessaryJumps(blocks);
}
return { ...func, blocks };
}
}
exports.normalizeIL = normalizeIL;
function cullUnreachableBlocks(blocks, entryBlockID) {
const blockIsReachableSet = new Set();
blockIsReachable(entryBlockID);
return lodash_1.default.pickBy(blocks, b => blockIsReachableSet.has(b.id));
function blockIsReachable(blockID) {
if (blockIsReachableSet.has(blockID)) {
return;
}
blockIsReachableSet.add(blockID);
const block = (0, utils_1.notUndefined)(blocks[blockID]);
for (const op of block.operations) {
for (const label of (0, il_opcodes_1.labelOperandsOfOperation)(op)) {
blockIsReachable(label.targetBlockId);
}
}
}
}
function cullUnreachableInstructions(blocks) {
// The purpose of this function is to remove extra terminating instructions
// from the code. This can happen if the user has provided code that
// explicitly terminates a block before the end of the block (e.g. using
// `break`). This culling is not for performance optimization. The reason it's
// needed is for testing purposes, to get the code into a canonical form for
// comparison.
return lodash_1.default.mapValues(blocks, block => ({
...block,
operations: cullOperations(block.operations)
}));
function cullOperations(operations) {
// Find the first instruction that terminates the block
const index = operations.findIndex(op => il_opcodes_1.blockTerminatingOpcodes.has(op.opcode));
// Blocks in IL do not have a defined order, so there is no such thing as
// "falling through" to the next block. Every block must terminate with a
// terminating instruction.
if (index === -1)
return (0, utils_1.unexpected)();
if (index === operations.length - 1)
return operations;
return operations.slice(0, index + 1);
}
}
function inlineUnnecessaryJumps(blocks) {
blocks = { ...blocks };
// In the case where you jump to a block, and the block is only jumped to from
// one source location, the jump can be removed and the blocks can just be
// merged.
const reachabilityCount = new Map();
const blockIsJumpedTo = new Map();
for (const [blockID, block] of Object.entries(blocks)) {
for (const operation of block.operations) {
for (const { targetBlockId } of (0, il_opcodes_1.labelOperandsOfOperation)(operation)) {
reachabilityCount.set(targetBlockId, (reachabilityCount.get(targetBlockId) ?? 0) + 1);
if (operation.opcode === 'Jump') {
blockIsJumpedTo.set(targetBlockId, {
fromBlockId: blockID,
fromOperation: operation
});
}
}
}
}
// "to" and "from" here refer to jumping to and jumping from
for (const toBlockId of Object.keys(blocks)) {
const toBlock = blocks[toBlockId];
const shouldInlineBlock = reachabilityCount.get(toBlockId) === 1 && blockIsJumpedTo.has(toBlockId);
if (shouldInlineBlock) {
const { fromBlockId, fromOperation } = blockIsJumpedTo.get(toBlockId);
const fromBlock = blocks[fromBlockId];
let operations = [...fromBlock.operations];
// We're inlining at the end, so it's expected the last operation is at the end
(0, utils_1.hardAssert)(operations[operations.length - 1] === fromOperation);
operations.splice(operations.length - 1, 1, ...toBlock.operations);
delete blocks[toBlockId];
blocks[fromBlockId] = { ...blocks[fromBlockId], operations };
}
}
return blocks;
}
//# sourceMappingURL=normalize-il.js.map