UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,891 lines (1,816 loc) 51.8 kB
import { u as unwrap, e as expect, c as isIndexable, S as StackImpl } from './collections-C3Y8z_9v.js'; import { a as unwrapHandle } from './template-BRrQR6KS.js'; import { v as valueForRef, f as childRefFor, U as UNDEFINED_REFERENCE, a as createComputeRef, T as TRUE_REFERENCE, F as FALSE_REFERENCE, b as isConstRef, u as updateRef, c as createConstRef } from './reference-BshxG6wn.js'; import { a as assert } from './assert-Zqc4wiAV.js'; import { P as ProgramImpl } from './program-B7CWdS8W.js'; import { t as track, U as UPDATE_TAG, m as beginTrackFrame, q as endTrackFrame } from './cache-BIlOoPA7.js'; import { A as APPEND_OPCODES, D as DebugRenderTreeImpl, i as isArgumentError, h as isCurriedType, d as curry, g as check, j as resolveCurriedValue, f as reifyPositional, k as AssertFilter, V as VMArgumentsImpl, l as externs, J as JumpIfNotModifiedOpcode, B as BeginTrackFrameOpcode, m as EndTrackFrameOpcode } from './debug-render-tree-CF5O4-WI.js'; import { B as BLACKLIST_TABLE, f as DOMOperations, D as DOMTreeConstruction, N as NewTreeBuilder } from './element-builder-CnlTPNyw.js'; import { a as assign } from './object-utils-AijlD-JH.js'; import { associateDestroyableChild, _hasDestroyableChildren, destroy, destroyChildren, registerDestructor } from '../@glimmer/destroyable/index.js'; import { createIteratorRef, createIteratorItemRef } from '../@glimmer/reference/index.js'; import { r as reverse } from './array-utils-CZQxrdD3.js'; import { a as $v0, d as $pc, e as $ra, f as $fp, g as $sp, i as isLowLevelRegister } from './registers-C_W2qYHJ.js'; import { a as VM_RETURN_TO_OP, b as VM_RETURN_OP, c as VM_JUMP_OP, d as VM_INVOKE_VIRTUAL_OP, V as VM_INVOKE_STATIC_OP, e as VM_POP_FRAME_OP, f as VM_PUSH_FRAME_OP } from './vm-ops-ImHv0Wtg.js'; import { a as CURRIED_COMPONENT, b as CURRIED_HELPER } from './curried-BZnYakIg.js'; import { ai as VM_DEBUGGER_OP, p as decodeHandle, aj as VM_CURRY_OP, ak as VM_DYNAMIC_HELPER_OP, al as VM_HELPER_OP, am as VM_GET_VARIABLE_OP, an as VM_SET_VARIABLE_OP, ao as VM_SET_BLOCK_OP, ap as VM_ROOT_SCOPE_OP, aq as VM_GET_PROPERTY_OP, ar as VM_GET_BLOCK_OP, as as VM_SPREAD_BLOCK_OP, at as VM_HAS_BLOCK_OP, au as VM_HAS_BLOCK_PARAMS_OP, av as VM_CONCAT_OP, aw as VM_IF_INLINE_OP, ax as VM_NOT_OP, ay as VM_GET_DYNAMIC_VAR_OP, az as VM_LOG_OP, a as VM_CONTENT_TYPE_OP, aA as VM_DYNAMIC_CONTENT_TYPE_OP, c as VM_APPEND_HTML_OP, g as VM_APPEND_SAFE_HTML_OP, d as VM_APPEND_TEXT_OP, h as VM_APPEND_DOCUMENT_FRAGMENT_OP, i as VM_APPEND_NODE_OP, aB as VM_ENTER_LIST_OP, aC as VM_EXIT_LIST_OP, aD as VM_ITERATE_OP } from './syscall-ops-BPFtDquC.js'; import { toBool } from '../@glimmer/global-context/index.js'; import { a as getInternalHelperManager, c as hasInternalComponentManager, d as hasInternalHelperManager } from './api-zh_k31vb.js'; import { ContentType } from '../@glimmer/vm/index.js'; import { b as isEmpty, d as isString, s as shouldCoerce, i as isSafeString, e as isFragment, f as isNode, m as move, c as clear } from './normalize-D9jDJVGg.js'; // Allow the contents of `debugCallback` without extra annotations /* eslint-disable @typescript-eslint/no-unused-expressions */ function debugCallback(context, get) { // eslint-disable-next-line no-console console.info('Use `context`, and `get(<path>)` to debug this template.'); // for example... context === get('this'); // eslint-disable-next-line no-debugger debugger; } let callback = debugCallback; // For testing purposes function setDebuggerCallback(cb) { callback = cb; } function resetDebuggerCallback() { callback = debugCallback; } class ScopeInspector { #symbols; constructor(scope, symbols) { this.scope = scope; this.#symbols = symbols; } get(path) { let { scope } = this; let symbols = this.#symbols; let parts = path.split('.'); let [head, ...tail] = path.split('.'); let ref; if (head === 'this') { ref = scope.getSelf(); } else if (symbols.locals[head]) { ref = unwrap(scope.getSymbol(symbols.locals[head])); } else { ref = this.scope.getSelf(); tail = parts; } return tail.reduce((r, part) => childRefFor(r, part), ref); } } APPEND_OPCODES.add(VM_DEBUGGER_OP, (vm, { op1: _debugInfo }) => { let debuggerInfo = vm.constants.getValue(decodeHandle(_debugInfo)); let inspector = new ScopeInspector(vm.scope(), debuggerInfo); callback(valueForRef(vm.getSelf()), path => valueForRef(inspector.get(path))); }); ['b', 'big', 'blockquote', 'body', 'br', 'center', 'code', 'dd', 'div', 'dl', 'dt', 'em', 'embed', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'i', 'img', 'li', 'listing', 'main', 'meta', 'nobr', 'ol', 'p', 'pre', 'ruby', 's', 'small', 'span', 'strong', 'strike', 'sub', 'sup', 'table', 'tt', 'u', 'ul', 'var'].forEach(tag => BLACKLIST_TABLE[tag] = 1); const WHITESPACE = /[\t\n\v\f\r \xa0\u{1680}\u{180e}\u{2000}-\u{200a}\u{2028}\u{2029}\u{202f}\u{205f}\u{3000}\u{feff}]/u; function isWhitespace(string) { return WHITESPACE.test(string); } class DOMChangesImpl extends DOMOperations { namespace; constructor(document) { super(document); this.document = document; this.namespace = null; } setAttribute(element, name, value) { element.setAttribute(name, value); } removeAttribute(element, name) { element.removeAttribute(name); } insertAfter(element, node, reference) { this.insertBefore(element, node, reference.nextSibling); } } const DOMChanges = DOMChangesImpl; 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)); 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)); 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 ; } 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 createCurryRef(type, inner, owner, args, resolver, isStrict) { let lastValue, curriedDefinition; return createComputeRef(() => { let value = valueForRef(inner); if (value === lastValue) { return curriedDefinition; } if (isCurriedType(value, type)) { curriedDefinition = args ? curry(type, value, owner, args) : args; } else if (type === CURRIED_COMPONENT && typeof value === 'string' && value) { curriedDefinition = curry(type, value, owner, args); } else if (isIndexable(value)) { curriedDefinition = curry(type, value, owner, args); } else { curriedDefinition = null; } lastValue = value; return curriedDefinition; }); } function createConcatRef(partsRefs) { return createComputeRef(() => { const parts = []; for (const ref of partsRefs) { const value = valueForRef(ref); if (value !== null && value !== undefined) { parts.push(castToString(value)); } } if (parts.length > 0) { return parts.join(''); } return null; }); } function castToString(value) { if (typeof value === 'string') { return value; } else if (typeof value.toString !== 'function') { return ''; } // eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme return String(value); } APPEND_OPCODES.add(VM_CURRY_OP, (vm, { op1: type, op2: _isStrict }) => { let stack = vm.stack; let definition = check(stack.pop()); let capturedArgs = check(stack.pop()); let owner = vm.getOwner(); vm.context.resolver; vm.loadValue($v0, createCurryRef(type, definition, owner, capturedArgs)); }); APPEND_OPCODES.add(VM_DYNAMIC_HELPER_OP, vm => { let stack = vm.stack; let ref = check(stack.pop()); let args = check(stack.pop()).capture(); let helperRef; let initialOwner = vm.getOwner(); let helperInstanceRef = createComputeRef(() => { if (helperRef !== undefined) { destroy(helperRef); } let definition = valueForRef(ref); if (isCurriedType(definition, CURRIED_HELPER)) { let { definition: resolvedDef, owner, positional, named } = resolveCurriedValue(definition); let helper = resolveHelper(resolvedDef); if (named !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment args.named = assign({}, ...named, args.named); } if (positional !== undefined) { args.positional = positional.concat(args.positional); } helperRef = helper(args, owner); associateDestroyableChild(helperInstanceRef, helperRef); } else if (isIndexable(definition)) { let helper = resolveHelper(definition); helperRef = helper(args, initialOwner); if (_hasDestroyableChildren(helperRef)) { associateDestroyableChild(helperInstanceRef, helperRef); } } else { helperRef = UNDEFINED_REFERENCE; } }); let helperValueRef = createComputeRef(() => { valueForRef(helperInstanceRef); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme return valueForRef(helperRef); }); vm.associateDestroyable(helperInstanceRef); vm.loadValue($v0, helperValueRef); }); function resolveHelper(definition, ref) { let managerOrHelper = getInternalHelperManager(definition, true); let helper; if (managerOrHelper === null) { helper = null; } else { helper = typeof managerOrHelper === 'function' ? managerOrHelper : managerOrHelper.getHelper(definition); } return helper; } APPEND_OPCODES.add(VM_HELPER_OP, (vm, { op1: handle }) => { let stack = vm.stack; let helper = check(vm.constants.getValue(handle)); let args = check(stack.pop()); let value = helper(args.capture(), vm.getOwner(), vm.dynamicScope()); if (_hasDestroyableChildren(value)) { vm.associateDestroyable(value); } vm.loadValue($v0, value); }); APPEND_OPCODES.add(VM_GET_VARIABLE_OP, (vm, { op1: symbol }) => { let expr = vm.referenceForSymbol(symbol); vm.stack.push(expr); }); APPEND_OPCODES.add(VM_SET_VARIABLE_OP, (vm, { op1: symbol }) => { let expr = check(vm.stack.pop()); vm.scope().bindSymbol(symbol, expr); }); APPEND_OPCODES.add(VM_SET_BLOCK_OP, (vm, { op1: symbol }) => { let handle = check(vm.stack.pop()); let scope = check(vm.stack.pop()); let table = check(vm.stack.pop()); vm.scope().bindBlock(symbol, [handle, scope, table]); }); APPEND_OPCODES.add(VM_ROOT_SCOPE_OP, (vm, { op1: size }) => { vm.pushRootScope(size, vm.getOwner()); }); APPEND_OPCODES.add(VM_GET_PROPERTY_OP, (vm, { op1: _key }) => { let key = vm.constants.getValue(_key); let expr = check(vm.stack.pop()); vm.stack.push(childRefFor(expr, key)); }); APPEND_OPCODES.add(VM_GET_BLOCK_OP, (vm, { op1: _block }) => { let { stack } = vm; let block = vm.scope().getBlock(_block); stack.push(block); }); APPEND_OPCODES.add(VM_SPREAD_BLOCK_OP, vm => { let { stack } = vm; let block = check(stack.pop()); if (block && !isUndefinedReference(block)) { let [handleOrCompilable, scope, table] = block; stack.push(table); stack.push(scope); stack.push(handleOrCompilable); } else { stack.push(null); stack.push(null); stack.push(null); } }); function isUndefinedReference(input) { return input === UNDEFINED_REFERENCE; } APPEND_OPCODES.add(VM_HAS_BLOCK_OP, vm => { let { stack } = vm; let block = check(stack.pop()); if (block && !isUndefinedReference(block)) { stack.push(TRUE_REFERENCE); } else { stack.push(FALSE_REFERENCE); } }); APPEND_OPCODES.add(VM_HAS_BLOCK_PARAMS_OP, vm => { // FIXME(mmun): should only need to push the symbol table vm.stack.pop(); vm.stack.pop(); let table = check(vm.stack.pop()); let hasBlockParams = table && table.parameters.length; vm.stack.push(hasBlockParams ? TRUE_REFERENCE : FALSE_REFERENCE); }); APPEND_OPCODES.add(VM_CONCAT_OP, (vm, { op1: count }) => { let out = new Array(count); for (let i = count; i > 0; i--) { let offset = i - 1; out[offset] = check(vm.stack.pop()); } vm.stack.push(createConcatRef(out)); }); APPEND_OPCODES.add(VM_IF_INLINE_OP, vm => { let condition = check(vm.stack.pop()); let truthy = check(vm.stack.pop()); let falsy = check(vm.stack.pop()); vm.stack.push(createComputeRef(() => { if (toBool(valueForRef(condition))) { return valueForRef(truthy); } else { return valueForRef(falsy); } })); }); APPEND_OPCODES.add(VM_NOT_OP, vm => { let ref = check(vm.stack.pop()); vm.stack.push(createComputeRef(() => { return !toBool(valueForRef(ref)); })); }); APPEND_OPCODES.add(VM_GET_DYNAMIC_VAR_OP, vm => { let scope = vm.dynamicScope(); let stack = vm.stack; let nameRef = check(stack.pop()); stack.push(createComputeRef(() => { let name = String(valueForRef(nameRef)); return valueForRef(scope.get(name)); })); }); APPEND_OPCODES.add(VM_LOG_OP, vm => { let { positional } = check(vm.stack.pop()).capture(); vm.loadValue($v0, createComputeRef(() => { // eslint-disable-next-line no-console console.log(...reifyPositional(positional)); })); }); class DynamicTextContent { constructor(node, reference, lastValue) { this.node = node; this.reference = reference; this.lastValue = lastValue; } evaluate() { let value = valueForRef(this.reference); let { lastValue } = this; if (value === lastValue) return; let normalized; if (isEmpty(value)) { normalized = ''; } else if (isString(value)) { normalized = value; } else { normalized = String(value); } if (normalized !== lastValue) { let textNode = this.node; textNode.nodeValue = this.lastValue = normalized; } } } function toContentType(value) { if (shouldCoerce(value)) { return ContentType.String; } else if (isCurriedType(value, CURRIED_COMPONENT) || hasInternalComponentManager(value)) { return ContentType.Component; } else if (isCurriedType(value, CURRIED_HELPER) || hasInternalHelperManager(value)) { return ContentType.Helper; } else if (isSafeString(value)) { return ContentType.SafeString; } else if (isFragment(value)) { return ContentType.Fragment; } else if (isNode(value)) { return ContentType.Node; } else { return ContentType.String; } } function toDynamicContentType(value) { if (!isIndexable(value)) { return ContentType.String; } if (isCurriedType(value, CURRIED_COMPONENT) || hasInternalComponentManager(value)) { return ContentType.Component; } else { return ContentType.Helper; } } APPEND_OPCODES.add(VM_CONTENT_TYPE_OP, vm => { let reference = check(vm.stack.peek()); vm.stack.push(toContentType(valueForRef(reference))); if (!isConstRef(reference)) { vm.updateWith(new AssertFilter(reference, toContentType)); } }); APPEND_OPCODES.add(VM_DYNAMIC_CONTENT_TYPE_OP, vm => { let reference = check(vm.stack.peek()); vm.stack.push(toDynamicContentType(valueForRef(reference))); if (!isConstRef(reference)) { vm.updateWith(new AssertFilter(reference, toDynamicContentType)); } }); APPEND_OPCODES.add(VM_APPEND_HTML_OP, vm => { let reference = check(vm.stack.pop()); let rawValue = valueForRef(reference); let value = isEmpty(rawValue) ? '' : String(rawValue); vm.tree().appendDynamicHTML(value); }); APPEND_OPCODES.add(VM_APPEND_SAFE_HTML_OP, vm => { let reference = check(vm.stack.pop()); let rawValue = check(valueForRef(reference)).toHTML(); let value = isEmpty(rawValue) ? '' : check(rawValue); vm.tree().appendDynamicHTML(value); }); APPEND_OPCODES.add(VM_APPEND_TEXT_OP, vm => { let reference = check(vm.stack.pop()); let rawValue = valueForRef(reference); let value = isEmpty(rawValue) ? '' : String(rawValue); let node = vm.tree().appendDynamicText(value); if (!isConstRef(reference)) { vm.updateWith(new DynamicTextContent(node, reference, value)); } }); APPEND_OPCODES.add(VM_APPEND_DOCUMENT_FRAGMENT_OP, vm => { let reference = check(vm.stack.pop()); let value = check(valueForRef(reference)); vm.tree().appendDynamicFragment(value); }); APPEND_OPCODES.add(VM_APPEND_NODE_OP, vm => { let reference = check(vm.stack.pop()); let value = check(valueForRef(reference)); vm.tree().appendDynamicNode(value); }); APPEND_OPCODES.add(VM_ENTER_LIST_OP, (vm, { op1: relativeStart, op2: elseTarget }) => { let stack = vm.stack; let listRef = check(stack.pop()); let keyRef = check(stack.pop()); let keyValue = valueForRef(keyRef); // eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme let key = keyValue === null ? '@identity' : String(keyValue); let iteratorRef = createIteratorRef(listRef, key); let iterator = valueForRef(iteratorRef); vm.updateWith(new AssertFilter(iteratorRef, iterator => iterator.isEmpty())); if (iterator.isEmpty()) { // TODO: Fix this offset, should be accurate vm.lowlevel.goto(elseTarget + 1); } else { vm.enterList(iteratorRef, relativeStart); vm.stack.push(iterator); } }); APPEND_OPCODES.add(VM_EXIT_LIST_OP, vm => { vm.exitList(); }); APPEND_OPCODES.add(VM_ITERATE_OP, (vm, { op1: breaks }) => { let stack = vm.stack; let iterator = check(stack.peek()); let item = iterator.next(); if (item !== null) { vm.registerItem(vm.enterItem(item)); } else { vm.lowlevel.goto(breaks); } }); 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) { { 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) { 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(); } /** * ## 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) { { 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() { { 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