ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,514 lines (1,443 loc) • 38.7 kB
JavaScript
import './debug-to-string-BsFOvUtQ.js';
import { u as unwrap, e as expect, S as StackImpl } from './collections-B8me-ZlQ.js';
import { u as unwrapHandle } from './constants-oDhF27qL.js';
import { isDevelopingApp } from '@embroider/macros';
import { assertGlobalContextWasSet } from '../@glimmer/global-context/index.js';
import { track, updateTag as UPDATE_TAG, debug, resetTracking, beginTrackFrame, endTrackFrame } from '../@glimmer/validator/index.js';
import { U as UNDEFINED_REFERENCE, v as valueForRef, u as updateRef, d as createConstRef, c as childRefFor } from './reference-B6HMX4y0.js';
import { a as assert } from './assert-CUCJBR2C.js';
import { a as ProgramImpl } from './program-CcLlGnAU.js';
import { k as DebugRenderTreeImpl, l as isArgumentError, m as DOMChangesImpl, A as APPEND_OPCODES, n as move, j as clear, V as VMArgumentsImpl, p as externs, J as JumpIfNotModifiedOpcode, B as BeginTrackFrameOpcode, E as EndTrackFrameOpcode } from './dynamic-CuBsUXX8.js';
import { D as DOMTreeConstruction, N as NewTreeBuilder } from './element-builder-BuVym8EM.js';
import { a as assign } from './object-utils-AijlD-JH.js';
import { destroyChildren, associateDestroyableChild, destroy, registerDestructor } from '../@glimmer/destroyable/index.js';
import { c as createIteratorItemRef } from './iterable-BKS7az3P.js';
import { r as reverse } from './array-utils-CZQxrdD3.js';
import { a as $pc, b as $ra, c as $fp, d as $sp, i as isLowLevelRegister } from './registers-ylirb0dq.js';
import { p as VM_RETURN_TO_OP, q as VM_RETURN_OP, r as VM_JUMP_OP, s as VM_INVOKE_VIRTUAL_OP, o as VM_INVOKE_STATIC_OP, t as VM_POP_FRAME_OP, u as VM_PUSH_FRAME_OP } from './capabilities-DHiXCCuB.js';
class DynamicScopeImpl {
bucket;
constructor(bucket) {
if (bucket) {
this.bucket = assign({}, bucket);
} else {
this.bucket = {};
}
}
get(key) {
return unwrap(this.bucket[key]);
}
set(key, reference) {
return this.bucket[key] = reference;
}
child() {
return new DynamicScopeImpl(this.bucket);
}
}
class ScopeImpl {
static root(owner, {
self,
size = 0
}) {
let refs = new Array(size + 1).fill(UNDEFINED_REFERENCE);
return new ScopeImpl(owner, refs, null).init({
self
});
}
static sized(owner, size = 0) {
let refs = new Array(size + 1).fill(UNDEFINED_REFERENCE);
return new ScopeImpl(owner, refs, null);
}
owner;
slots;
callerScope;
constructor(owner,
// the 0th slot is `self`
slots,
// a single program can mix owners via curried components, and the state lives on root scopes
callerScope) {
this.owner = owner;
this.slots = slots;
this.callerScope = callerScope;
}
init({
self
}) {
this.slots[0] = self;
return this;
}
/**
* @debug
*/
snapshot() {
return this.slots.slice();
}
getSelf() {
return this.get(0);
}
getSymbol(symbol) {
return this.get(symbol);
}
getBlock(symbol) {
let block = this.get(symbol);
return block === UNDEFINED_REFERENCE ? null : block;
}
bind(symbol, value) {
this.set(symbol, value);
}
bindSelf(self) {
this.set(0, self);
}
bindSymbol(symbol, value) {
this.set(symbol, value);
}
bindBlock(symbol, value) {
this.set(symbol, value);
}
bindCallerScope(scope) {
this.callerScope = scope;
}
getCallerScope() {
return this.callerScope;
}
child() {
return new ScopeImpl(this.owner, this.slots.slice(), this.callerScope);
}
get(index) {
if (index >= this.slots.length) {
throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`);
}
return this.slots[index];
}
set(index, value) {
if (index >= this.slots.length) {
throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`);
}
this.slots[index] = value;
}
}
const TRANSACTION = Symbol('TRANSACTION');
class TransactionImpl {
scheduledInstallModifiers = [];
scheduledUpdateModifiers = [];
createdComponents = [];
updatedComponents = [];
didCreate(component) {
this.createdComponents.push(component);
}
didUpdate(component) {
this.updatedComponents.push(component);
}
scheduleInstallModifier(modifier) {
this.scheduledInstallModifiers.push(modifier);
}
scheduleUpdateModifier(modifier) {
this.scheduledUpdateModifiers.push(modifier);
}
commit() {
let {
createdComponents,
updatedComponents
} = this;
for (const {
manager,
state
} of createdComponents) {
manager.didCreate(state);
}
for (const {
manager,
state
} of updatedComponents) {
manager.didUpdate(state);
}
let {
scheduledInstallModifiers,
scheduledUpdateModifiers
} = this;
for (const {
manager,
state,
definition
} of scheduledInstallModifiers) {
let modifierTag = manager.getTag(state);
if (modifierTag !== null) {
let tag = track(() => manager.install(state), isDevelopingApp() && `- While rendering:\n (instance of a \`${definition.resolvedName || manager.getDebugName(definition.state)}\` modifier)`);
UPDATE_TAG(modifierTag, tag);
} else {
manager.install(state);
}
}
for (const {
manager,
state,
definition
} of scheduledUpdateModifiers) {
let modifierTag = manager.getTag(state);
if (modifierTag !== null) {
let tag = track(() => manager.update(state), isDevelopingApp() && `- While rendering:\n (instance of a \`${definition.resolvedName || manager.getDebugName(definition.state)}\` modifier)`);
UPDATE_TAG(modifierTag, tag);
} else {
manager.update(state);
}
}
}
}
class EnvironmentImpl {
[TRANSACTION] = null;
updateOperations;
// Delegate methods and values
isInteractive;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isArgumentCaptureError;
debugRenderTree;
constructor(options, delegate) {
this.delegate = delegate;
this.isInteractive = delegate.isInteractive;
this.debugRenderTree = this.delegate.enableDebugTooling ? new DebugRenderTreeImpl() : undefined;
this.isArgumentCaptureError = this.delegate.enableDebugTooling ? isArgumentError : undefined;
if (options.appendOperations) {
this.appendOperations = options.appendOperations;
this.updateOperations = options.updateOperations;
} else if (options.document) {
this.appendOperations = new DOMTreeConstruction(options.document);
this.updateOperations = new DOMChangesImpl(options.document);
} else if (isDevelopingApp()) {
throw new Error('you must pass document or appendOperations to a new runtime');
}
}
getAppendOperations() {
return this.appendOperations;
}
getDOM() {
return expect(this.updateOperations);
}
begin() {
assert(!this[TRANSACTION]);
this.debugRenderTree?.begin();
this[TRANSACTION] = new TransactionImpl();
}
get transaction() {
return expect(this[TRANSACTION]);
}
didCreate(component) {
this.transaction.didCreate(component);
}
didUpdate(component) {
this.transaction.didUpdate(component);
}
scheduleInstallModifier(modifier) {
if (this.isInteractive) {
this.transaction.scheduleInstallModifier(modifier);
}
}
scheduleUpdateModifier(modifier) {
if (this.isInteractive) {
this.transaction.scheduleUpdateModifier(modifier);
}
}
commit() {
let transaction = this.transaction;
this[TRANSACTION] = null;
transaction.commit();
this.debugRenderTree?.commit();
this.delegate.onTransactionCommit();
}
}
function runtimeOptions(options, delegate, artifacts, resolver) {
return {
env: new EnvironmentImpl(options, delegate),
program: new ProgramImpl(artifacts.constants, artifacts.heap),
resolver
};
}
function inTransaction(env, block) {
if (!env[TRANSACTION]) {
env.begin();
try {
block();
} finally {
env.commit();
}
} else {
block();
}
}
function initializeRegistersWithSP(sp) {
return [0, -1, sp, 0];
}
class LowLevelVM {
currentOpSize = 0;
registers;
context;
constructor(stack, context, externs, registers) {
this.stack = stack;
this.externs = externs;
this.context = context;
this.registers = registers;
}
fetchRegister(register) {
return this.registers[register];
}
loadRegister(register, value) {
this.registers[register] = value;
}
setPc(pc) {
this.registers[$pc] = pc;
}
// Start a new frame and save $ra and $fp on the stack
pushFrame() {
this.stack.push(this.registers[$ra]);
this.stack.push(this.registers[$fp]);
this.registers[$fp] = this.registers[$sp] - 1;
}
// Restore $ra, $sp and $fp
popFrame() {
this.registers[$sp] = this.registers[$fp] - 1;
this.registers[$ra] = this.stack.get(0);
this.registers[$fp] = this.stack.get(1);
}
pushSmallFrame() {
this.stack.push(this.registers[$ra]);
}
popSmallFrame() {
this.registers[$ra] = this.stack.pop();
}
// Jump to an address in `program`
goto(offset) {
this.setPc(this.target(offset));
}
target(offset) {
return this.registers[$pc] + offset - this.currentOpSize;
}
// Save $pc into $ra, then jump to a new address in `program` (jal in MIPS)
call(handle) {
this.registers[$ra] = this.registers[$pc];
this.setPc(this.context.program.heap.getaddr(handle));
}
// Put a specific `program` address in $ra
returnTo(offset) {
this.registers[$ra] = this.target(offset);
}
// Return to the `program` address stored in $ra
return() {
this.setPc(this.registers[$ra]);
}
nextStatement() {
let {
registers,
context
} = this;
let pc = registers[$pc];
if (pc === -1) {
return null;
}
// We have to save off the current operations size so that
// when we do a jump we can calculate the correct offset
// to where we are going. We can't simply ask for the size
// in a jump because we have have already incremented the
// program counter to the next instruction prior to executing.
let opcode = context.program.opcode(pc);
let operationSize = this.currentOpSize = opcode.size;
this.registers[$pc] += operationSize;
return opcode;
}
evaluateOuter(opcode, vm) {
{
this.evaluateInner(opcode, vm);
}
}
evaluateInner(opcode, vm) {
if (opcode.isMachine) {
this.evaluateMachine(opcode, vm);
} else {
this.evaluateSyscall(opcode, vm);
}
}
evaluateMachine(opcode, vm) {
switch (opcode.type) {
case VM_PUSH_FRAME_OP:
return void this.pushFrame();
case VM_POP_FRAME_OP:
return void this.popFrame();
case VM_INVOKE_STATIC_OP:
return void this.call(opcode.op1);
case VM_INVOKE_VIRTUAL_OP:
return void vm.call(this.stack.pop());
case VM_JUMP_OP:
return void this.goto(opcode.op1);
case VM_RETURN_OP:
return void vm.return();
case VM_RETURN_TO_OP:
return void this.returnTo(opcode.op1);
}
}
evaluateSyscall(opcode, vm) {
APPEND_OPCODES.evaluate(vm, opcode, opcode.type);
}
}
class UpdatingVM {
env;
dom;
alwaysRevalidate;
frameStack = new StackImpl();
constructor(env, {
alwaysRevalidate = false
}) {
this.env = env;
this.dom = env.getDOM();
this.alwaysRevalidate = alwaysRevalidate;
}
execute(opcodes, handler) {
if (isDevelopingApp()) {
let hasErrored = true;
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
debug.runInTrackingTransaction(() => this._execute(opcodes, handler), '- While rendering:');
// using a boolean here to avoid breaking ergonomics of "pause on uncaught exceptions"
// which would happen with a `catch` + `throw`
hasErrored = false;
} finally {
if (hasErrored) {
// eslint-disable-next-line no-console
console.error(`\n\nError occurred:\n\n${resetTracking()}\n\n`);
}
}
} else {
this._execute(opcodes, handler);
}
}
_execute(opcodes, handler) {
let {
frameStack
} = this;
this.try(opcodes, handler);
while (!frameStack.isEmpty()) {
let opcode = this.frame.nextStatement();
if (opcode === undefined) {
frameStack.pop();
continue;
}
opcode.evaluate(this);
}
}
get frame() {
return expect(this.frameStack.current);
}
goto(index) {
this.frame.goto(index);
}
try(ops, handler) {
this.frameStack.push(new UpdatingVMFrame(ops, handler));
}
throw() {
this.frame.handleException();
this.frameStack.pop();
}
}
class BlockOpcode {
children;
bounds;
constructor(state, context, bounds, children) {
this.state = state;
this.context = context;
this.children = children;
this.bounds = bounds;
}
parentElement() {
return this.bounds.parentElement();
}
firstNode() {
return this.bounds.firstNode();
}
lastNode() {
return this.bounds.lastNode();
}
evaluate(vm) {
vm.try(this.children, null);
}
}
class TryOpcode extends BlockOpcode {
type = 'try';
// Shadows property on base class
evaluate(vm) {
vm.try(this.children, this);
}
handleException() {
let {
state,
bounds,
context: {
env
}
} = this;
destroyChildren(this);
let tree = NewTreeBuilder.resume(env, bounds);
let vm = state.evaluate(tree);
let children = this.children = [];
let result = vm.execute(vm => {
vm.updateWith(this);
vm.pushUpdating(children);
});
associateDestroyableChild(this, result.drop);
}
}
class ListItemOpcode extends TryOpcode {
retained = false;
index = -1;
constructor(state, context, bounds, key, memo, value) {
super(state, context, bounds, []);
this.key = key;
this.memo = memo;
this.value = value;
}
shouldRemove() {
return !this.retained;
}
reset() {
this.retained = false;
}
}
class ListBlockOpcode extends BlockOpcode {
type = 'list-block';
opcodeMap = new Map();
marker = null;
lastIterator;
constructor(state, context, bounds, children, iterableRef) {
super(state, context, bounds, children);
this.iterableRef = iterableRef;
this.lastIterator = valueForRef(iterableRef);
}
initializeChild(opcode) {
opcode.index = this.children.length - 1;
this.opcodeMap.set(opcode.key, opcode);
}
evaluate(vm) {
let iterator = valueForRef(this.iterableRef);
if (this.lastIterator !== iterator) {
let {
bounds
} = this;
let {
dom
} = vm;
let marker = this.marker = dom.createComment('');
dom.insertAfter(bounds.parentElement(), marker, expect(bounds.lastNode()));
this.sync(iterator);
this.parentElement().removeChild(marker);
this.marker = null;
this.lastIterator = iterator;
}
// Run now-updated updating opcodes
super.evaluate(vm);
}
sync(iterator) {
let {
opcodeMap: itemMap,
children
} = this;
let currentOpcodeIndex = 0;
let seenIndex = 0;
this.children = this.bounds.boundList = [];
while (true) {
let item = iterator.next();
if (item === null) break;
let opcode = children[currentOpcodeIndex];
let {
key
} = item;
// Items that have already been found and moved will already be retained,
// we can continue until we find the next unretained item
while (opcode !== undefined && opcode.retained) {
opcode = children[++currentOpcodeIndex];
}
if (opcode !== undefined && opcode.key === key) {
this.retainItem(opcode, item);
currentOpcodeIndex++;
} else if (itemMap.has(key)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
let itemOpcode = itemMap.get(key);
// The item opcode was seen already, so we should move it.
if (itemOpcode.index < seenIndex) {
this.moveItem(itemOpcode, item, opcode);
} else {
// Update the seen index, we are going to be moving this item around
// so any other items that come before it will likely need to move as
// well.
seenIndex = itemOpcode.index;
let seenUnretained = false;
// iterate through all of the opcodes between the current position and
// the position of the item's opcode, and determine if they are all
// retained.
for (let i = currentOpcodeIndex + 1; i < seenIndex; i++) {
if (!unwrap(children[i]).retained) {
seenUnretained = true;
break;
}
}
// If we have seen only retained opcodes between this and the matching
// opcode, it means that all the opcodes in between have been moved
// already, and we can safely retain this item's opcode.
if (!seenUnretained) {
this.retainItem(itemOpcode, item);
currentOpcodeIndex = seenIndex + 1;
} else {
this.moveItem(itemOpcode, item, opcode);
currentOpcodeIndex++;
}
}
} else {
this.insertItem(item, opcode);
}
}
for (const opcode of children) {
if (!opcode.retained) {
this.deleteItem(opcode);
} else {
opcode.reset();
}
}
}
retainItem(opcode, item) {
let {
children
} = this;
updateRef(opcode.memo, item.memo);
updateRef(opcode.value, item.value);
opcode.retained = true;
opcode.index = children.length;
children.push(opcode);
}
insertItem(item, before) {
let {
opcodeMap,
bounds,
state,
children,
context: {
env
}
} = this;
let {
key
} = item;
let nextSibling = before === undefined ? this.marker : before.firstNode();
let elementStack = NewTreeBuilder.forInitialRender(env, {
element: bounds.parentElement(),
nextSibling
});
let vm = state.evaluate(elementStack);
vm.execute(vm => {
let opcode = vm.enterItem(item);
opcode.index = children.length;
children.push(opcode);
opcodeMap.set(key, opcode);
associateDestroyableChild(this, opcode);
});
}
moveItem(opcode, item, before) {
let {
children
} = this;
updateRef(opcode.memo, item.memo);
updateRef(opcode.value, item.value);
opcode.retained = true;
let currentSibling, nextSibling;
if (before === undefined) {
move(opcode, this.marker);
} else {
currentSibling = opcode.lastNode().nextSibling;
nextSibling = before.firstNode();
// Items are moved throughout the algorithm, so there are cases where the
// the items already happen to be siblings (e.g. an item in between was
// moved before this move happened). Check to see if they are siblings
// first before doing the move.
if (currentSibling !== nextSibling) {
move(opcode, nextSibling);
}
}
opcode.index = children.length;
children.push(opcode);
}
deleteItem(opcode) {
destroy(opcode);
clear(opcode);
this.opcodeMap.delete(opcode.key);
}
}
class UpdatingVMFrame {
current = 0;
constructor(ops, exceptionHandler) {
this.ops = ops;
this.exceptionHandler = exceptionHandler;
}
goto(index) {
this.current = index;
}
nextStatement() {
return this.ops[this.current++];
}
handleException() {
if (this.exceptionHandler) {
this.exceptionHandler.handleException();
}
}
}
class RenderResultImpl {
constructor(env, updating, bounds, drop) {
this.env = env;
this.updating = updating;
this.bounds = bounds;
this.drop = drop;
associateDestroyableChild(this, drop);
registerDestructor(this, () => clear(this.bounds));
}
rerender({
alwaysRevalidate = false
} = {
alwaysRevalidate: false
}) {
let {
env,
updating
} = this;
let vm = new UpdatingVM(env, {
alwaysRevalidate
});
vm.execute(updating, this);
}
parentElement() {
return this.bounds.parentElement();
}
firstNode() {
return this.bounds.firstNode();
}
lastNode() {
return this.bounds.lastNode();
}
handleException() {
}
}
class EvaluationStackImpl {
static restore(snapshot, pc) {
const stack = new this(snapshot.slice(), initializeRegistersWithSP(snapshot.length - 1));
stack.registers[$pc] = pc;
stack.registers[$sp] = snapshot.length - 1;
stack.registers[$fp] = -1;
return stack;
}
registers;
// fp -> sp
constructor(stack = [], registers) {
this.stack = stack;
this.registers = registers;
}
push(value) {
this.stack[++this.registers[$sp]] = value;
}
dup(position = this.registers[$sp]) {
this.stack[++this.registers[$sp]] = this.stack[position];
}
copy(from, to) {
this.stack[to] = this.stack[from];
}
pop(n = 1) {
let top = this.stack[this.registers[$sp]];
this.registers[$sp] -= n;
return top;
}
peek(offset = 0) {
return this.stack[this.registers[$sp] - offset];
}
get(offset, base = this.registers[$fp]) {
return this.stack[base + offset];
}
set(value, offset, base = this.registers[$fp]) {
this.stack[base + offset] = value;
}
slice(start, end) {
return this.stack.slice(start, end);
}
capture(items) {
let end = this.registers[$sp] + 1;
let start = end - items;
return this.stack.slice(start, end);
}
reset() {
this.stack.length = 0;
}
static {
}
}
class Stacks {
drop = {};
scope = new StackImpl();
dynamicScope = new StackImpl();
updating = new StackImpl();
cache = new StackImpl();
list = new StackImpl();
destroyable = new StackImpl();
constructor(scope, dynamicScope) {
this.scope.push(scope);
this.dynamicScope.push(dynamicScope);
this.destroyable.push(this.drop);
}
}
class VM {
#stacks;
args;
lowlevel;
debug;
trace;
get stack() {
return this.lowlevel.stack;
}
/* Registers */
get pc() {
return this.lowlevel.fetchRegister($pc);
}
#registers = [null, null, null, null, null, null, null, null, null];
/**
* Fetch a value from a syscall register onto the stack.
*
* ## Opcodes
*
* - Append: `Fetch`
*
* ## State changes
*
* [!] push Eval Stack <- $register
*/
fetch(register) {
let value = this.fetchValue(register);
this.stack.push(value);
}
/**
* Load a value from the stack into a syscall register.
*
* ## Opcodes
*
* - Append: `Load`
*
* ## State changes
*
* [!] pop Eval Stack -> `value`
* [$] $register <- `value`
*/
load(register) {
let value = this.stack.pop();
this.loadValue(register, value);
}
/**
* Load a value into a syscall register.
*
* ## State changes
*
* [$] $register <- `value`
*
* @utility
*/
loadValue(register, value) {
this.#registers[register] = value;
}
/**
* Fetch a value from a register (machine or syscall).
*
* ## State changes
*
* [ ] get $register
*
* @utility
*/
fetchValue(register) {
if (isLowLevelRegister(register)) {
return this.lowlevel.fetchRegister(register);
}
return this.#registers[register];
}
// Save $pc into $ra, then jump to a new address in `program` (jal in MIPS)
call(handle) {
if (handle !== null) {
this.lowlevel.call(handle);
}
}
// Return to the `program` address stored in $ra
return() {
this.lowlevel.return();
}
#tree;
context;
constructor({
scope,
dynamicScope,
stack,
pc
}, context, tree) {
if (isDevelopingApp()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
assertGlobalContextWasSet();
}
let evalStack = EvaluationStackImpl.restore(stack, pc);
this.#tree = tree;
this.context = context;
this.#stacks = new Stacks(scope, dynamicScope);
this.args = new VMArgumentsImpl();
this.lowlevel = new LowLevelVM(evalStack, context, externs(), evalStack.registers);
this.pushUpdating();
}
static initial(context, options) {
let scope = ScopeImpl.root(options.owner, options.scope ?? {
self: UNDEFINED_REFERENCE,
size: 0
});
const state = closureState(context.program.heap.getaddr(options.handle), scope, options.dynamicScope);
return new VM(state, context, options.tree);
}
compile(block) {
let handle = unwrapHandle(block.compile(this.context));
return handle;
}
get constants() {
return this.context.program.constants;
}
get program() {
return this.context.program;
}
get env() {
return this.context.env;
}
captureClosure(args, pc = this.lowlevel.fetchRegister($pc)) {
return {
pc,
scope: this.scope(),
dynamicScope: this.dynamicScope(),
stack: this.stack.capture(args)
};
}
capture(args, pc = this.lowlevel.fetchRegister($pc)) {
return new Closure(this.captureClosure(args, pc), this.context);
}
/**
* ## Opcodes
*
* - Append: `BeginComponentTransaction`
*
* ## State Changes
*
* [ ] create `guard` (`JumpIfNotModifiedOpcode`)
* [ ] create `tracker` (`BeginTrackFrameOpcode`)
* [!] push Updating Stack <- `guard`
* [!] push Updating Stack <- `tracker`
* [!] push Cache Stack <- `guard`
* [!] push Tracking Stack
*/
beginCacheGroup(name) {
let opcodes = this.updating();
let guard = new JumpIfNotModifiedOpcode();
opcodes.push(guard);
opcodes.push(new BeginTrackFrameOpcode(name));
this.#stacks.cache.push(guard);
beginTrackFrame(name);
}
/**
* ## Opcodes
*
* - Append: `CommitComponentTransaction`
*
* ## State Changes
*
* Create a new `EndTrackFrameOpcode` (`end`)
*
* [!] pop CacheStack -> `guard`
* [!] pop Tracking Stack -> `tag`
* [ ] create `end` (`EndTrackFrameOpcode`) with `guard`
* [-] consume `tag`
*/
commitCacheGroup() {
let opcodes = this.updating();
let guard = expect(this.#stacks.cache.pop());
let tag = endTrackFrame();
opcodes.push(new EndTrackFrameOpcode(guard));
guard.finalize(tag, opcodes.length);
}
/**
* ## Opcodes
*
* - Append: `Enter`
*
* ## State changes
*
* [!] push Element Stack as `block`
* [ ] create `try` (`TryOpcode`) with `block`, capturing `args` from the Eval Stack
*
* Did Enter (`try`):
* [-] associate destroyable `try`
* [!] push Destroyable Stack <- `try`
* [!] push Updating List <- `try`
* [!] push Updating Stack <- `try.children`
*/
enter(args) {
let updating = [];
let state = this.capture(args);
let block = this.tree().pushResettableBlock();
let tryOpcode = new TryOpcode(state, this.context, block, updating);
this.didEnter(tryOpcode);
}
/**
* ## Opcodes
*
* - Append: `Iterate`
* - Update: `ListBlock`
*
* ## State changes
*
* Create a new ref for the iterator item (`value`).
* Create a new ref for the iterator key (`key`).
*
* [ ] create `valueRef` (`Reference`) from `value`
* [ ] create `keyRef` (`Reference`) from `key`
* [!] push Eval Stack <- `valueRef`
* [!] push Eval Stack <- `keyRef`
* [!] push Element Stack <- `UpdatableBlock` as `block`
* [ ] capture `closure` with *2* items from the Eval Stack
* [ ] create `iteration` (`ListItemOpcode`) with `closure`, `block`, `key`, `keyRef` and `valueRef`
*
* Did Enter (`iteration`):
* [-] associate destroyable `iteration`
* [!] push Destroyable Stack <- `iteration`
* [!] push Updating List <- `iteration`
* [!] push Updating Stack <- `iteration.children`
*/
enterItem({
key,
value,
memo
}) {
let {
stack
} = this;
let valueRef = createIteratorItemRef(value);
let memoRef = createIteratorItemRef(memo);
stack.push(valueRef);
stack.push(memoRef);
let state = this.capture(2);
let block = this.tree().pushResettableBlock();
let opcode = new ListItemOpcode(state, this.context, block, key, memoRef, valueRef);
this.didEnter(opcode);
return opcode;
}
registerItem(opcode) {
this.listBlock().initializeChild(opcode);
}
/**
* ## Opcodes
*
* - Append: `EnterList`
*
* ## State changes
*
* [ ] capture `closure` with *0* items from the Eval Stack, and `$pc` from `offset`
* [ ] create `updating` (empty `Array`)
* [!] push Element Stack <- `list` (`BlockList`) with `updating`
* [ ] create `list` (`ListBlockOpcode`) with `closure`, `list`, `updating` and `iterableRef`
* [!] push List Stack <- `list`
*
* Did Enter (`list`):
* [-] associate destroyable `list`
* [!] push Destroyable Stack <- `list`
* [!] push Updating List <- `list`
* [!] push Updating Stack <- `list.children`
*/
enterList(iterableRef, offset) {
let updating = [];
let addr = this.lowlevel.target(offset);
let state = this.capture(0, addr);
let list = this.tree().pushBlockList(updating);
let opcode = new ListBlockOpcode(state, this.context, list, updating, iterableRef);
this.#stacks.list.push(opcode);
this.didEnter(opcode);
}
/**
* ## Opcodes
*
* - Append: `Enter`
* - Append: `Iterate`
* - Append: `EnterList`
* - Update: `ListBlock`
*
* ## State changes
*
* [-] associate destroyable `opcode`
* [!] push Destroyable Stack <- `opcode`
* [!] push Updating List <- `opcode`
* [!] push Updating Stack <- `opcode.children`
*
*/
didEnter(opcode) {
this.associateDestroyable(opcode);
this.#stacks.destroyable.push(opcode);
this.updateWith(opcode);
this.pushUpdating(opcode.children);
}
/**
* ## Opcodes
*
* - Append: `Exit`
* - Append: `ExitList`
*
* ## State changes
*
* [!] pop Destroyable Stack
* [!] pop Element Stack
* [!] pop Updating Stack
*/
exit() {
this.#stacks.destroyable.pop();
this.#tree.popBlock();
this.popUpdating();
}
/**
* ## Opcodes
*
* - Append: `ExitList`
*
* ## State changes
*
* Pop List:
* [!] pop Destroyable Stack
* [!] pop Element Stack
* [!] pop Updating Stack
*
* [!] pop List Stack
*/
exitList() {
this.exit();
this.#stacks.list.pop();
}
/**
* ## Opcodes
*
* - Append: `RootScope`
* - Append: `VirtualRootScope`
*
* ## State changes
*
* [!] push Scope Stack
*/
pushRootScope(size, owner) {
let scope = ScopeImpl.sized(owner, size);
this.#stacks.scope.push(scope);
return scope;
}
/**
* ## Opcodes
*
* - Append: `ChildScope`
*
* ## State changes
*
* [!] push Scope Stack <- `child` of current Scope
*/
pushChildScope() {
this.#stacks.scope.push(this.scope().child());
}
/**
* ## Opcodes
*
* - Append: `Yield`
*
* ## State changes
*
* [!] push Scope Stack <- `scope`
*/
pushScope(scope) {
this.#stacks.scope.push(scope);
}
/**
* ## Opcodes
*
* - Append: `PopScope`
*
* ## State changes
*
* [!] pop Scope Stack
*/
popScope() {
this.#stacks.scope.pop();
}
/**
* ## Opcodes
*
* - Append: `PushDynamicScope`
*
* ## State changes:
*
* [!] push Dynamic Scope Stack <- child of current Dynamic Scope
*/
pushDynamicScope() {
let child = this.dynamicScope().child();
this.#stacks.dynamicScope.push(child);
return child;
}
/**
* ## Opcodes
*
* - Append: `BindDynamicScope`
*
* ## State changes:
*
* [!] pop Dynamic Scope Stack `names.length` times
*/
bindDynamicScope(names) {
let scope = this.dynamicScope();
for (const name of reverse(names)) {
scope.set(name, this.stack.pop());
}
}
/**
* ## State changes
*
* - [!] push Updating Stack
*
* @utility
*/
pushUpdating(list = []) {
this.#stacks.updating.push(list);
}
/**
* ## State changes
*
* [!] pop Updating Stack
*
* @utility
*/
popUpdating() {
return expect(this.#stacks.updating.pop());
}
/**
* ## State changes
*
* [!] push Updating List
*
* @utility
*/
updateWith(opcode) {
this.updating().push(opcode);
}
listBlock() {
return expect(this.#stacks.list.current);
}
/**
* ## State changes
*
* [-] associate destroyable `child`
*
* @utility
*/
associateDestroyable(child) {
let parent = expect(this.#stacks.destroyable.current);
associateDestroyableChild(parent, child);
}
updating() {
return expect(this.#stacks.updating.current);
}
/**
* Get Tree Builder
*/
tree() {
return this.#tree;
}
/**
* Get current Scope
*/
scope() {
return expect(this.#stacks.scope.current);
}
/**
* Get current Dynamic Scope
*/
dynamicScope() {
return expect(this.#stacks.dynamicScope.current);
}
popDynamicScope() {
this.#stacks.dynamicScope.pop();
}
/// SCOPE HELPERS
getOwner() {
return this.scope().owner;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSelf() {
return this.scope().getSelf();
}
referenceForSymbol(symbol) {
return this.scope().getSymbol(symbol);
}
/// EXECUTION
execute(initialize) {
if (isDevelopingApp()) {
let hasErrored = true;
try {
let value = this._execute(initialize);
// using a boolean here to avoid breaking ergonomics of "pause on uncaught exceptions"
// which would happen with a `catch` + `throw`
hasErrored = false;
return value;
} finally {
if (hasErrored) {
// If any existing blocks are open, due to an error or something like
// that, we need to close them all and clean things up properly.
let elements = this.tree();
while (elements.hasBlocks) {
elements.popBlock();
}
// eslint-disable-next-line no-console
console.error(`\n\nError occurred:\n\n${resetTracking()}\n\n`);
}
}
} else {
return this._execute(initialize);
}
}
_execute(initialize) {
if (initialize) initialize(this);
let result;
do result = this.next(); while (!result.done);
return result.value;
}
next() {
let {
env
} = this;
let opcode = this.lowlevel.nextStatement();
let result;
if (opcode !== null) {
this.lowlevel.evaluateOuter(opcode, this);
result = {
done: false,
value: null
};
} else {
// Unload the stack
this.stack.reset();
result = {
done: true,
value: new RenderResultImpl(env, this.popUpdating(), this.#tree.popBlock(), this.#stacks.drop)
};
}
return result;
}
}
function closureState(pc, scope, dynamicScope) {
return {
pc,
scope,
dynamicScope,
stack: []
};
}
/**
* A closure captures the state of the VM for a particular block of code that is necessary to
* re-invoke the block in the future.
*
* In practice, this allows us to clear the previous render and "replay" the block's execution,
* rendering content in the same position as the first render.
*/
class Closure {
state;
context;
constructor(state, context) {
this.state = state;
this.context = context;
}
evaluate(tree) {
return new VM(this.state, this.context, tree);
}
}
class TemplateIteratorImpl {
constructor(vm) {
this.vm = vm;
}
next() {
return this.vm.next();
}
sync() {
if (isDevelopingApp()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
return debug.runInTrackingTransaction(() => this.vm.execute(), '- While rendering:');
} else {
return this.vm.execute();
}
}
}
function renderSync(env, iterator) {
let result;
inTransaction(env, () => result = iterator.sync());
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
return result;
}
function renderMain(context, owner, self, tree, layout, dynamicScope = new DynamicScopeImpl()) {
let handle = unwrapHandle(layout.compile(context));
let numSymbols = layout.symbolTable.symbols.length;
let vm = VM.initial(context, {
scope: {
self,
size: numSymbols
},
dynamicScope,
tree,
handle,
owner
});
return new TemplateIteratorImpl(vm);
}
function renderInvocation(vm, context, owner, definition, args) {
// Get a list of tuples of argument names and references, like
// [['title', reference], ['name', reference]]
const argList = Object.keys(args).map(key => [key, args[key]]);
const blockNames = ['main', 'else', 'attrs'];
// Prefix argument names with `@` symbol
const argNames = argList.map(([name]) => `@${name}`);
let reified = vm.constants.component(definition, owner, undefined, '{ROOT}');
vm.lowlevel.pushFrame();
// Push blocks on to the stack, three stack values per block
for (let i = 0; i < 3 * blockNames.length; i++) {
vm.stack.push(null);
}
vm.stack.push(null);
// For each argument, push its backing reference on to the stack
argList.forEach(([, reference]) => {
vm.stack.push(reference);
});
// Configure VM based on blocks and args just pushed on to the stack.
vm.args.setup(vm.stack, argNames, blockNames, 0, true);
const compilable = expect(reified.compilable);
const layoutHandle = unwrapHandle(compilable.compile(context));
const invocation = {
handle: layoutHandle,
symbolTable: compilable.symbolTable
};
// Needed for the Op.Main opcode: arguments, component invocation object, and
// component definition.
vm.stack.push(vm.args);
vm.stack.push(invocation);
vm.stack.push(reified);
return new TemplateIteratorImpl(vm);
}
function renderComponent(context, tree, owner, definition, args = {}, dynamicScope = new DynamicScopeImpl()) {
let vm = VM.initial(context, {
tree,
handle: context.stdlib.main,
dynamicScope,
owner
});
return renderInvocation(vm, context, owner, definition, recordToReference(args));
}
function recordToReference(record) {
const root = createConstRef(record, 'args');
return Object.keys(record).reduce((acc, key) => {
acc[key] = childRefFor(root, key);
return acc;
}, {});
}
export { DynamicScopeImpl as D, EnvironmentImpl as E, LowLevelVM as L, ScopeImpl as S, UpdatingVM as U, renderMain as a, renderSync as b, runtimeOptions as c, inTransaction as i, renderComponent as r };