UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,514 lines (1,443 loc) 38.7 kB
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 };