microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
493 lines (490 loc) • 26 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.pass2_computeSlots = void 0;
const utils_1 = require("../../utils");
const common_1 = require("../common");
function pass2_computeSlots({ file, cur, importedModuleNamespaceSlots, importBindings, model, awaitStackDepths, }) {
/*
This function calculates the size of each closure scope, and the index of each
variable in the closure scope.
The structure of this implementation is:
computeModuleSlots
computeFunctionSlots
computeBlockLikeSlots
For the module, `computeModuleSlots` deals with global variables and
import/exports before deferring to `computeFunctionSlots` which deals with all
remaining function-level declarations before in turn deferring to
`computeBlockLikeSlots` for all the lexical declarations (for the module
scope). `computeBlockLikeSlots` then recurses on the children scopes, which
may be functions or blocks.
*/
const { scopes, globalSlots, freeVariables } = model;
const root = scopes.get(file.program) || (0, utils_1.unexpected)();
(0, common_1.visitingNode)(cur, file);
if (root.type !== 'ModuleScope')
(0, utils_1.unexpected)();
// Recurse the tree starting at the root
computeModuleSlots(root);
function computeModuleSlots(moduleScope) {
const globalSlotNames = new Set([...freeVariables]);
const newGlobalSlot = (nameHint) => {
// Note: the generated names can't conflict with existing module names
// OR free variable names since we use the same IL instruction to load
// both.
const name = (0, utils_1.uniqueNameInSet)(nameHint, globalSlotNames);
const slot = { type: 'GlobalSlot', name };
globalSlots.push(slot);
return slot;
};
// TODO: It would make a lot of sense if the "thisModule" slot was also just
// an external linking reference, like with imports
model.thisModuleSlot = newGlobalSlot('thisModule');
const getImportedModuleNamespaceSlot = (source) => {
let slot = importedModuleNamespaceSlots.get(source);
if (!slot) {
const name = (0, utils_1.uniqueNameInSet)(source, globalSlotNames);
slot = { type: 'GlobalSlot', name };
importedModuleNamespaceSlots.set(source, slot);
model.moduleImports.set(source, slot);
}
return slot;
};
// Root-level bindings
for (const binding of Object.values(moduleScope.bindings)) {
binding.slot = computeModuleSlot(binding);
}
// Compute entry-function slots (this will skip any bindings that already
// have slots assigned, such as module slots)
computeFunctionSlots(moduleScope);
return;
function computeModuleSlot(binding) {
if (importBindings.has(binding)) {
return computeImportBindingSlot(binding);
}
else if (binding.isExported) {
return computeExportBindingSlot(binding);
}
else if (binding.isAccessedByNestedFunction) {
// Note: We only need to allocate a global slot if the variable is
// accessed by a nested function, otherwise it can just be a local
// variable in the module entry function
return newGlobalSlot(binding.name);
}
else {
// Fall back to normal function variable behavior
return undefined;
}
}
function computeImportBindingSlot(binding) {
const { source, specifier } = importBindings.get(binding) ?? (0, utils_1.unexpected)();
const moduleNamespaceObjectSlot = getImportedModuleNamespaceSlot(source);
switch (specifier.type) {
// import x as y from 'z'
case 'ImportSpecifier':
return {
type: 'ModuleImportExportSlot',
moduleNamespaceObjectSlot,
propertyName: specifier.imported.type === 'Identifier' ? specifier.imported.name :
specifier.imported.type === 'StringLiteral' ? specifier.imported.value :
(0, utils_1.assertUnreachable)(specifier.imported)
};
// import * as y from 'z';
case 'ImportNamespaceSpecifier':
return moduleNamespaceObjectSlot;
// import y from 'z';
case 'ImportDefaultSpecifier':
return {
type: 'ModuleImportExportSlot',
moduleNamespaceObjectSlot,
propertyName: 'default'
};
default: (0, utils_1.assertUnreachable)(specifier);
}
}
function computeExportBindingSlot(binding) {
return {
type: 'ModuleImportExportSlot',
moduleNamespaceObjectSlot: model.thisModuleSlot,
propertyName: binding.name
};
}
}
// Note: this function takes either FunctionScope or ModuleScope because it
// is also used to compute slots for the entry function. Essentially, we
// treat the module as a special kind of function that also has module
// slots.
function computeFunctionSlots(functionScope) {
let stackDepth = 0;
const pushLocalSlot = (debugName) => ({ type: 'LocalSlot', index: stackDepth++, debugName });
const nextClosureSlot = (debugName) => {
functionScope.closureSlots = functionScope.closureSlots ?? [];
const slot = { type: 'ClosureSlot', index: functionScope.closureSlots.length, debugName };
functionScope.closureSlots.push(slot);
return slot;
};
// Compute slots for nested functions and variables and recurse
computeBlockLikeSlots(functionScope, nextClosureSlot);
// Compute slots in a block-like scope (including lexical slots in a
// function, but not things like parameter slots or `var` declarations which
// are handled at the function level)
function computeBlockLikeSlots(blockScope, nextClosureSlotInParent) {
/*
* Note: this function actually deals with function scopes as well, since
* the function body is like a block.
*
* Within a block, there are slots for:
*
* - nested function declarations (which are hoisted to the beginning of
* the block, not necessarily the beginning of the containing function)
* - lexical bindings (let and const)
* - exception binding, if the block is a catch handler
*/
if (blockScope.isAsyncFunction) {
// Synchronous return value (engine assumes this is the first slot in the frame)
stackDepth === 0 || (0, utils_1.unexpected)();
pushLocalSlot('syncReturnValue');
// Space for async catch target. This will physically be realized with
// the AsyncStart instruction.
stackDepth += 2;
// Function pointer that continues the current async function
nextClosureSlot('async-continuation');
// The engine machinery always assumes the continuation is the first slot
(0, utils_1.hardAssert)(functionScope.closureSlots?.length === 1);
// Function pointer that references the callback to invoke when the current async function completes.
nextClosureSlot('async-callback');
// If we know the stack depth at all the await points, we use that to
// calculate how many slots need to be reserved to preserve the stack
// at the await points.
if (awaitStackDepths) {
let maxStackDepthAtAwait = undefined;
for (const awaitExpr of functionScope.awaitExpressions) {
const line = awaitExpr.loc?.start.line ?? (0, utils_1.unexpected)();
const col = (awaitExpr.loc?.start.column ?? (0, utils_1.unexpected)()) + 1;
const depthAtAwait = awaitStackDepths.get(`${line}:${col}`) ?? (0, utils_1.unexpected)();
if (maxStackDepthAtAwait === undefined || depthAtAwait > maxStackDepthAtAwait) {
maxStackDepthAtAwait = depthAtAwait;
}
}
// It's possible that there are no await points
if (maxStackDepthAtAwait !== undefined) {
// Local slot 0 is the async return value, and slots 1 and 2 are
// reserved for the async catch target. So the stack depth at the
// await points should be 3. The await statements only need to
// preserve the slots above these since the AsyncStart and
// AsyncResume instructions set up the first 3 slots. The `-1` is
// because the top of the stack holds the awaited value, which is
// not part of what is preserved to the closure when the async
// function is suspended.
(0, utils_1.hardAssert)(stackDepth === 3);
const slotsRequired = maxStackDepthAtAwait - stackDepth - 1;
for (let i = 0; i < slotsRequired; i++) {
nextClosureSlot(`await-save-${i}`);
}
}
}
}
else {
// Note: can only use closure embedding in non-async functions, because
// the async callback uses the same slot number (0) as embedded
// closures.
// The first nested function under this scope, with the same lifetime as
// this scope, can be embedded in this scope.
// See [Closure Embedding](../../../doc/internals/closure-embedding.md)
const embeddingFunction = blockScope.embeddingCandidates.find(f => f.functionIsClosure);
if (embeddingFunction) {
// Reserve the first slot for the function pointer for this closure
const embeddedClosureSlot = nextClosureSlotInBlockOrParent(`embedded-closure:${embeddingFunction.funcName ?? 'anonymous'}`);
blockScope.embeddedChildClosure = embeddingFunction;
embeddingFunction.embeddedInParentSlot = embeddedClosureSlot;
}
}
computeIlParameterSlots(blockScope, nextClosureSlotInBlockOrParent, pushLocalSlot);
const isTryScope = 'isTryScope' in blockScope && blockScope.isTryScope;
const stackDepthBeforeStartTry = stackDepth;
if (isTryScope) {
blockScope.prologue.push({ type: 'StartTry' });
stackDepth += 2;
}
const blockStartStackDepth = stackDepth;
let expectedVariablePopCount = 0;
const isCatchScope = 'isCatchScope' in blockScope && blockScope.isCatchScope;
if (isCatchScope) {
blockScope.prologue.push({ type: 'DummyPushException' });
// The catch exception slot needs to be first because the `throw`
// instruction pushes it onto the stack.
if ('catchExceptionBinding' in blockScope) {
const binding = blockScope.catchExceptionBinding;
// Allocate a slot for the exception variable. Note: if the exception
// will be local, then `nextBlockLocalOrClosureSlot` increments the
// stack depth to value that `throw` results in anyway, so we don't need
// to add or subtract to `stackDepth` to compensate for the `throw`. If
// the exception will be closure-allocated,
// `nextBlockLocalOrClosureSlot` will not increment the stack depth so
// it will be one slot lower than the actual entry to the `catch`, but
// then we do the `InitCatchParam` below which pops exception into the
// closure slot, thus bringing the stack depth to the correct value.
binding.slot = nextBlockLocalOrClosureSlot(binding, binding.name);
// The binding will either be a local or closure slot. If it's local
// then it's already populated by the `throw` operation (which pushes
// the exception to the top of the stack). If it's a closure-scoped
// slot, we need to pop it off the top of the stack and put it into the
// closure scope.
if (binding.slot.type === 'ClosureSlot') {
blockScope.prologue.push({
type: 'InitCatchParam',
slot: accessSlotForInitialization(binding.slot)
});
}
else {
(0, utils_1.hardAssert)(binding.slot.type === 'LocalSlot');
expectedVariablePopCount++;
}
}
else {
// Else, there is no binding, so we need to pop the catch parameter to
// discard it
blockScope.prologue.push({ type: 'DiscardCatchParam' });
}
}
// Hoisted var declarations.
//
// Note: most var declarations will hoisted to the function level, but var
// declarations inside a `catch` block are only hoisted as far as the
// catch, not the function.
for (const binding of blockScope.varDeclarations) {
// Var declarations at the module level may already have global slots allocated
if (binding.slot)
continue;
(0, utils_1.hardAssert)(binding.kind === 'var');
binding.slot = nextFunctionLocalOrClosureSlot(binding, binding.name);
if (binding.slot) {
blockScope.prologue.push({
type: 'InitVarDeclaration',
slot: accessSlotForInitialization(binding.slot)
});
if (binding.slot.type === 'LocalSlot') {
expectedVariablePopCount++;
}
}
}
// Nested function declarations
for (const decl of blockScope.nestedFunctionDeclarations) {
const { binding, func } = decl;
// Function declarations at the module level may already have global slots allocated
if (!binding.slot) {
binding.slot = nextBlockLocalOrClosureSlot(binding, binding.name);
}
const functionInfo = model.scopes.get(func) ?? (0, utils_1.unexpected)();
if (functionInfo.type !== 'FunctionScope')
(0, utils_1.unexpected)();
const functionId = functionInfo.ilFunctionId;
if (binding.slot) {
blockScope.prologue.push({
type: 'InitFunctionDeclaration',
functionId,
closureType: functionInfo.functionIsClosure
? functionInfo.embeddedInParentSlot
? 'embedded'
: 'non-embedded'
: 'none',
slot: accessSlotForInitialization(binding.slot)
});
if (binding.slot.type === 'LocalSlot') {
expectedVariablePopCount++;
}
}
}
// Lexical declarations
for (const binding of blockScope.lexicalDeclarations) {
// Lexical declarations at the module level may already have global slots allocated
if (binding.slot)
continue;
binding.slot = nextBlockLocalOrClosureSlot(binding, binding.name);
// Note: closure slots are already initialized when the scope is created
if (binding.slot && binding.slot.type === 'LocalSlot') {
blockScope.prologue.push({
type: 'InitLexicalDeclaration',
slot: accessSlotForInitialization(binding.slot),
nameHint: binding.name,
});
expectedVariablePopCount++;
}
}
for (const child of blockScope.children) {
switch (child.type) {
case 'BlockScope':
computeBlockLikeSlots(child, nextClosureSlotInBlockOrParent);
break;
case 'FunctionScope':
case 'ClassScope':
computeFunctionSlots(child);
break;
case 'ModuleScope': (0, utils_1.unexpected)();
default: (0, utils_1.assertUnreachable)(child);
}
}
// The parent reference slot is the last slot in the closure
if (blockScope.closureSlots && blockScope.accessesParentScope) {
nextClosureSlotInBlock('parent-reference');
}
// Now that all the slots have been computed, we know if there are any
// closure slots that need to be created in the prologue
if (blockScope.closureSlots) {
blockScope.closureSlots.length >= 1 || (0, utils_1.unexpected)();
const slotCount = blockScope.closureSlots.length;
if (blockScope.isAsyncFunction) {
blockScope.prologue.unshift({
type: 'AsyncStart',
slotCount,
captureParent: Boolean(blockScope.accessesParentScope)
});
}
else /* not async function */ {
if (blockScope.accessesParentScope) {
blockScope.prologue.unshift({ type: 'ScopePush', slotCount });
// Note: not required during a return because the return will restore the caller's scope.
blockScope.epilogue.push({ type: 'ScopePop', requiredDuringReturn: false });
}
else {
blockScope.prologue.unshift({ type: 'ScopeNew', slotCount });
// Note: not required during a return because the return will restore the caller's scope.
blockScope.epilogue.push({ type: 'ScopeDiscard', requiredDuringReturn: false });
}
}
}
// Note: we don't need to pop variables off the stack in a `try` block
// because the `EndTry` already truncates the stack to the right level.
if (blockScope.type === 'BlockScope' && !isTryScope) {
const count = stackDepth - blockStartStackDepth;
(0, utils_1.hardAssert)(count === expectedVariablePopCount);
if (count) {
blockScope.epilogue.push({ type: 'Pop', requiredDuringReturn: false, count });
}
}
stackDepth = blockStartStackDepth;
if (isTryScope) {
blockScope.epilogue.push({
type: 'EndTry',
requiredDuringReturn: true,
stackDepthAfter: stackDepthBeforeStartTry
});
stackDepth = stackDepthBeforeStartTry;
}
function nextBlockLocalOrClosureSlot(binding, debugName) {
(0, utils_1.hardAssert)(!binding.slot);
if (binding.isAccessedByNestedFunction) {
return nextClosureSlotInBlockOrParent(debugName);
}
else {
// Note that variables from multiple successive blocks can share the same local slot
return pushLocalSlot(debugName);
}
}
function nextClosureSlotInBlockOrParent(debugName) {
// If this is a block with the same lifetime as its parent block or
// function, we can optimize by storing variables in the parent
if (blockScope.sameInstanceCountAsParent) {
return nextClosureSlotInParent(debugName);
}
else {
return nextClosureSlotInBlock(debugName);
}
}
function nextClosureSlotInBlock(debugName) {
blockScope.closureSlots = blockScope.closureSlots ?? [];
const slot = { type: 'ClosureSlot', index: blockScope.closureSlots.length, debugName };
blockScope.closureSlots.push(slot);
return slot;
}
}
function nextFunctionLocalOrClosureSlot(binding, debugName) {
(0, utils_1.hardAssert)(!binding.slot);
if (binding.isAccessedByNestedFunction) {
return nextClosureSlot(debugName);
}
else {
// Note that variables from multiple successive blocks can share the same local slot
return pushLocalSlot(debugName);
}
}
}
}
exports.pass2_computeSlots = pass2_computeSlots;
function computeIlParameterSlots(scope, nextClosureSlot, pushLocalSlot) {
// Function declarations introduce a new lexical `this` into scope,
// whereas arrow functions do not (the lexical this falls through to the
// parent).
const thisBinding = scope.thisBinding;
if (thisBinding) {
// The `this` binding is never writtenTo, so it never needs to be copied
// into a local variable slot. But if it's used by a child (e.g. arrow
// function) then it needs initialization to copy it from `LoadArg` to
// `StoreScoped`.
(0, utils_1.hardAssert)(!thisBinding.isWrittenTo);
if (thisBinding.isAccessedByNestedFunction || (scope.isAsyncFunction && thisBinding.isUsed)) {
thisBinding.slot = nextClosureSlot('this');
scope.prologue.push({
type: 'InitThis',
slot: accessSlotForInitialization(thisBinding.slot)
});
}
else {
// Here, there's no need for initialization
// (ilParameterInitializations) since it won't be copied into a
// parameter slot.
thisBinding.slot = {
type: 'ArgumentSlot',
argIndex: 0
};
}
}
// Note: this function is actually called at the block level, not the function
// level, but most blocks will have an empty parameter bindings list. The
// exception to the rule is constructor functions which are functions in the
// source text but manifest as *blocks* inside a larger "physical constructor
// functions" inside the IL.
// Compute slots for the named parameters of the function
for (const [paramI, binding] of scope.parameterBindings.entries()) {
// Note: `LoadArg(0)` always refers to the caller-passed `this` value
const argIndex = paramI + 1;
if (binding.isAccessedByNestedFunction || scope.isAsyncFunction) {
binding.slot = nextClosureSlot(binding.name);
scope.prologue.push({
type: 'InitParameter',
argIndex,
slot: accessSlotForInitialization(binding.slot)
});
}
else if (binding.isWrittenTo) {
// In this case, the binding is writable but not in the closure
// scope. We need an initializer to copy the initial argument value
// into the parameter slot
binding.slot = pushLocalSlot(binding.name);
scope.prologue.push({
type: 'InitParameter',
argIndex,
slot: binding.slot
});
}
else {
// In this case, the parameter is used but never mutated so it can
// directly use LoadArg. We don't need any new prologue steps
// because the arguments are already in these slots when the
// function runs
binding.slot = { type: 'ArgumentSlot', argIndex };
}
}
}
/**
* Gives an accessor for a slot for the purposes of initializing the slot.
*/
function accessSlotForInitialization(slot) {
if (slot.type === 'ClosureSlot') {
return {
type: 'ClosureSlotAccess',
relativeIndex: slot.index
};
}
return slot;
}
//# sourceMappingURL=pass-2-compute-slots.js.map