UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,246 lines (1,231 loc) 58.3 kB
import { EMPTY_STRING_ARRAY, reverse, enumerate, dict, EMPTY_ARRAY, assign, Stack as StackImpl } from '../util/index.js'; import { InstructionEncoderImpl } from '../encoder/index.js'; import { $v0, $fp, $s0, $sp, InternalComponentCapabilities, $s1, TYPE_SIZE, MACHINE_MASK, ARG_SHIFT, ContentType } from '../vm/index.js'; import { SexpOpcodes as opcodes } from '../wire-format/index.js'; import { hasCapability } from '../manager/index.js'; import { isDevelopingApp } from '@embroider/macros'; let debugToString; if (isDevelopingApp()) { let getFunctionName = fn => { let functionName = fn.name; if ("" === functionName) { let match = /function (\w+)\s*\(/u.exec(String(fn)); functionName = match && match[1] || ""; } return functionName.replace(/^bound /u, ""); }, getObjectName = obj => { let name, className; // If the class has a decent looking name, and the `toString` is one of the // default Ember toStrings, replace the constructor portion of the toString // with the class name. We check the length of the class name to prevent doing // this when the value is minified. return "function" == typeof obj.constructor && (className = getFunctionName(obj.constructor)), "toString" in obj && obj.toString !== Object.prototype.toString && obj.toString !== Function.prototype.toString && ( // eslint-disable-next-line @typescript-eslint/no-base-to-string name = obj.toString()), name && /<.*:ember\d+>/u.test(name) && className && "_" !== className[0] && className.length > 2 && "Class" !== className ? name.replace(/<.*:/u, `<${className}:`) : name || className; }, getPrimitiveName = value => String(value); debugToString = value => "function" == typeof value ? getFunctionName(value) || "(unknown function)" : "object" == typeof value && null !== value ? getObjectName(value) || "(unknown object)" : getPrimitiveName(value); } var debugToString$1 = debugToString; function encodeImmediate(num) { return (num |= 0) < 0 ? function (num) { return -536870913 & num; }(num) : function (num) { return ~num; }(num); } let debugCompiler; function makeResolutionTypeVerifier(typeToVerify) { return opcode => { if (!function (opcode) { return Array.isArray(opcode) && 2 === opcode.length; }(opcode)) return false; let type = opcode[0]; return type === opcodes.GetStrictKeyword || type === opcodes.GetLexicalSymbol || type === typeToVerify; }; } [1, -1].forEach(x => { return num = encodeImmediate(x), (num |= 0) > -536870913 ? function (num) { return ~num; }(num) : function (num) { return 536870912 | num; }(num); var num; }); const isGetFreeComponent = makeResolutionTypeVerifier(opcodes.GetFreeAsComponentHead), isGetFreeModifier = makeResolutionTypeVerifier(opcodes.GetFreeAsModifierHead), isGetFreeHelper = makeResolutionTypeVerifier(opcodes.GetFreeAsHelperHead), isGetFreeComponentOrHelper = makeResolutionTypeVerifier(opcodes.GetFreeAsComponentOrHelperHead); function assertResolverInvariants(meta) { if (isDevelopingApp()) { if (!meta.symbols.upvars) throw new Error("Attempted to resolve a component, helper, or modifier, but no free vars were found"); if (!meta.owner) throw new Error("Attempted to resolve a component, helper, or modifier, but no owner was associated with the template it was being resolved from"); } return meta; } /** * <Foo/> * <Foo></Foo> * <Foo @arg={{true}} /> */ function lookupBuiltInHelper(expr, resolver, meta, constants, type) { let { symbols: { upvars: upvars } } = assertResolverInvariants(meta), name = upvars[expr[1]], helper = resolver?.lookupBuiltInHelper?.(name) ?? null; if (isDevelopingApp() && null === helper) // Keyword helper did not exist, which means that we're attempting to use a // value of some kind that is not in scope throw meta.isStrictMode, new Error(`Attempted to resolve a ${type} in a strict mode template, but that value was not in scope: ${ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme meta.symbols.upvars[expr[1]] ?? "{unknown variable}"}`); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme return constants.helper(helper, name); } function labelOperand(value) { return { type: 1, value: value }; } function stdlibOperand(value) { return { type: 5, value: value }; } function symbolTableOperand(value) { return { type: 7, value: value }; } function layoutOperand(value) { return { type: 8, value: value }; } class Labels { label(name, index) { this.labels[name] = index; } target(at, target) { this.targets.push({ at: at, target: target }); } patch(heap) { let { targets: targets, labels: labels } = this; for (const { at: at, target: target } of targets) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme let address = labels[target] - at; heap.getbyaddr(at), heap.setbyaddr(at, address); } } constructor() { this.labels = dict(), this.targets = []; } } function encodeOp(encoder, context, meta, op) { let { program: { constants: constants }, resolver: resolver } = context; if (function (op) { return op < 1e3; }(op[0])) { let [type, ...operands] = op; encoder.push(constants, type, ...operands); } else switch (op[0]) { case 1e3: return encoder.label(op[1]); case 1001: return encoder.startLabels(); case 1002: return encoder.stopLabels(); case 1004: return function (resolver, constants, meta, [, expr, then]) { isGetFreeComponent(expr); let type = expr[0]; if (isDevelopingApp() && expr[0] === opcodes.GetStrictKeyword) throw meta.isStrictMode, new Error(`Attempted to resolve a component in a strict mode template, but that value was not in scope: ${ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme meta.symbols.upvars[expr[1]] ?? "{unknown variable}"}`); if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, owner: owner, symbols: { lexical: lexical } } = meta, definition = scopeValues[expr[1]]; then(constants.component(definition, owner, false, lexical?.at(expr[1]))); } else { let { symbols: { upvars: upvars }, owner: owner } = assertResolverInvariants(meta), name = upvars[expr[1]], definition = resolver?.lookupComponent?.(name, owner) ?? null; if (isDevelopingApp() && ("object" != typeof definition || null === definition)) throw meta.isStrictMode, new Error(`Attempted to resolve \`${name}\`, which was expected to be a component, but nothing was found.`); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme then(constants.resolvedComponent(definition, name)); } } /** * (helper) * (helper arg) */(resolver, constants, meta, op); case 1003: /** * <div {{modifier}}/> * <div {{modifier arg}}/> * <Foo {{modifier}}/> */ return function (resolver, constants, meta, [, expr, then]) { isGetFreeModifier(expr); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, symbols: { lexical: lexical } } = meta, definition = scopeValues[expr[1]]; then(constants.modifier(definition, lexical?.at(expr[1]) ?? void 0)); } else if (type === opcodes.GetStrictKeyword) { let { symbols: { upvars: upvars } } = assertResolverInvariants(meta), name = upvars[expr[1]], modifier = resolver?.lookupBuiltInModifier?.(name) ?? null; if (isDevelopingApp() && null === modifier) throw meta.isStrictMode, new Error(`Attempted to resolve a modifier in a strict mode template, but it was not in scope: ${name}`); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme then(constants.modifier(modifier, name)); } else { let { symbols: { upvars: upvars }, owner: owner } = assertResolverInvariants(meta), name = upvars[expr[1]], modifier = resolver?.lookupModifier?.(name, owner) ?? null; if (isDevelopingApp() && null === modifier) throw meta.isStrictMode, new Error(`Attempted to resolve \`${name}\`, which was expected to be a modifier, but nothing was found.`); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme then(constants.modifier(modifier)); } } /** * {{component-or-helper arg}} */(resolver, constants, meta, op); case 1005: return function (resolver, constants, meta, [, expr, then]) { isGetFreeHelper(expr); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues } = meta, definition = scopeValues[expr[1]]; then(constants.helper(definition)); } else if (type === opcodes.GetStrictKeyword) then(lookupBuiltInHelper(expr, resolver, meta, constants, "helper"));else { let { symbols: { upvars: upvars }, owner: owner } = assertResolverInvariants(meta), name = upvars[expr[1]], helper = resolver?.lookupHelper?.(name, owner) ?? null; if (isDevelopingApp() && null === helper) throw meta.isStrictMode, new Error(`Attempted to resolve \`${name}\`, which was expected to be a helper, but nothing was found.`); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme then(constants.helper(helper, name)); } }(resolver, constants, meta, op); case 1007: return function (resolver, constants, meta, [, expr, { ifComponent: ifComponent, ifHelper: ifHelper }]) { isGetFreeComponentOrHelper(expr); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, owner: owner, symbols: { lexical: lexical } } = meta, definition = scopeValues[expr[1]], component = constants.component(definition, owner, true, lexical?.at(expr[1])); if (null !== component) return void ifComponent(component); let helper = constants.helper(definition, null, true); if (isDevelopingApp() && null === helper) throw meta.isStrictMode, new Error( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme `Attempted to use a value as either a component or helper, but it did not have a component manager or helper manager associated with it. The value was: ${debugToString$1(definition)}`); ifHelper(helper); } else if (type === opcodes.GetStrictKeyword) ifHelper(lookupBuiltInHelper(expr, resolver, meta, constants, "component or helper"));else { let { symbols: { upvars: upvars }, owner: owner } = assertResolverInvariants(meta), name = upvars[expr[1]], definition = resolver?.lookupComponent?.(name, owner) ?? null; if (null !== definition) ifComponent(constants.resolvedComponent(definition, name));else { let helper = resolver?.lookupHelper?.(name, owner) ?? null; if (isDevelopingApp() && null === helper) throw meta.isStrictMode, new Error(`Attempted to resolve \`${name}\`, which was expected to be a component or helper, but nothing was found.`); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme ifHelper(constants.helper(helper, name)); } } } /** * {{maybeHelperOrComponent}} */(resolver, constants, meta, op); case 1008: return function (resolver, constants, meta, [, expr, { ifComponent: ifComponent, ifHelper: ifHelper, ifValue: ifValue }]) { isGetFreeComponentOrHelper(expr); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, owner: owner, symbols: { lexical: lexical } } = meta, definition = scopeValues[expr[1]]; if ("function" != typeof definition && ("object" != typeof definition || null === definition)) // The value is not an object, so it can't be a component or helper. return void ifValue(constants.value(definition)); let component = constants.component(definition, owner, true, lexical?.at(expr[1])); if (null !== component) return void ifComponent(component); let helper = constants.helper(definition, null, true); if (null !== helper) return void ifHelper(helper); ifValue(constants.value(definition)); } else if (type === opcodes.GetStrictKeyword) ifHelper(lookupBuiltInHelper(expr, resolver, meta, constants, "value"));else { let { symbols: { upvars: upvars }, owner: owner } = assertResolverInvariants(meta), name = upvars[expr[1]], definition = resolver?.lookupComponent?.(name, owner) ?? null; if (null !== definition) return void ifComponent(constants.resolvedComponent(definition, name)); let helper = resolver?.lookupHelper?.(name, owner) ?? null; null !== helper && ifHelper(constants.helper(helper, name)); } }(resolver, constants, meta, op); case 1010: { let [, freeVar, andThen] = op; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme andThen(meta.symbols.upvars[freeVar], meta.moduleName); break; } case 1011: { let [, valueIndex, then] = op, value = meta.scopeValues[valueIndex]; then(constants.value(value)); break; } default: throw new Error(`Unexpected high level opcode ${op[0]}`); } } class EncoderImpl { constructor(heap, meta, stdlib) { this.heap = heap, this.meta = meta, this.stdlib = stdlib, this.labelsStack = new StackImpl(), this.encoder = new InstructionEncoderImpl([]), this.errors = [], this.handle = heap.malloc(); } error(error) { this.encoder.encode(30, 0), this.errors.push(error); } commit(size) { let handle = this.handle; return this.heap.pushMachine(5), this.heap.finishMalloc(handle, size), (list = this.errors) && list.length > 0 ? { errors: this.errors, handle: handle } : handle; var list; } push(constants, type, ...args) { let { heap: heap } = this; if (isDevelopingApp() && type > TYPE_SIZE) throw new Error(`Opcode type over 8-bits. Got ${type}.`); var value; let first = type | ((value = type) >= 0 && value <= 15 ? MACHINE_MASK : 0) | args.length << ARG_SHIFT; heap.pushRaw(first); for (let i = 0; i < args.length; i++) { let op = args[i]; heap.pushRaw(this.operand(constants, op)); } } operand(constants, operand) { if ("number" == typeof operand) return operand; if ("object" == typeof operand && null !== operand) { if (Array.isArray(operand)) return constants.array(operand); switch (operand.type) { case 1: return this.currentLabels.target(this.heap.offset, operand.value), -1; case 2: return constants.value(this.meta.isStrictMode); case 3: case 6: case 7: case 8: return constants.value(operand.value); case 4: return constants.value((block = operand.value, containing = this.meta, new CompilableTemplateImpl(block[0], containing, { parameters: block[1] || EMPTY_ARRAY }))); case 5: return this.stdlib[operand.value]; } } var block, containing; return constants.value(operand); } get currentLabels() { return this.labelsStack.current; } label(name) { this.currentLabels.label(name, this.heap.offset + 1); } startLabels() { this.labelsStack.push(new Labels()); } stopLabels() { this.labelsStack.pop().patch(this.heap); } } function templateCompilationContext(evaluation, meta) { return { evaluation: evaluation, encoder: new EncoderImpl(evaluation.program.heap, meta, evaluation.stdlib), meta: meta }; } class Compilers { add(name, func) { this.names[name] = this.funcs.push(func) - 1; } compile(op, sexp) { let name = sexp[0], index = this.names[name], func = this.funcs[index]; sexp[0], func(op, sexp); } constructor() { this.names = {}, // eslint-disable-next-line @typescript-eslint/no-explicit-any this.funcs = []; } } const EXPRESSIONS = new Compilers(); function withPath(op, path) { if (void 0 !== path && 0 !== path.length) for (let i = 0; i < path.length; i++) op(22, path[i]); } function expr(op, expression) { Array.isArray(expression) ? EXPRESSIONS.compile(op, expression) : (PushPrimitive(op, expression), op(31)); } /** * Push a reference onto the stack corresponding to a statically known primitive * @param value A JavaScript primitive (undefined, null, boolean, number or string) */ function PushPrimitiveReference(op, value) { PushPrimitive(op, value), op(31); } /** * Push an encoded representation of a JavaScript primitive on the stack * * @param value A JavaScript primitive (undefined, null, boolean, number or string) */ function PushPrimitive(op, primitive) { let p = primitive; var value; "number" == typeof p && (p = (value = p) % 1 == 0 && value <= 536870911 && value >= -536870912 ? encodeImmediate(p) : function (value) { return { type: 6, value: value }; }(p)), op(30, p); } /** * Invoke a foreign function (a "helper") based on a statically known handle * * @param op The op creation function * @param handle A handle * @param positional An optional list of expressions to compile * @param named An optional list of named arguments (name + expression) to compile */ function Call(op, handle, positional, named) { op(0), SimpleArgs(op, positional, named, false), op(16, handle), op(1), op(36, $v0); } /** * Invoke a foreign function (a "helper") based on a dynamically loaded definition * * @param op The op creation function * @param positional An optional list of expressions to compile * @param named An optional list of named arguments (name + expression) to compile */ function CallDynamic(op, positional, named, append) { op(0), SimpleArgs(op, positional, named, false), op(33, $fp, 1), op(107), append ? (op(36, $v0), append(), op(1), op(34, 1)) : (op(1), op(34, 1), op(36, $v0)); } /** * Evaluate statements in the context of new dynamic scope entries. Move entries from the * stack into named entries in the dynamic scope, then evaluate the statements, then pop * the dynamic scope * * @param names a list of dynamic scope names * @param block a function that returns a list of statements to evaluate */ function Curry(op, type, definition, positional, named) { op(0), SimpleArgs(op, positional, named, false), op(86), expr(op, definition), op(77, type, { type: 2, value: void 0 }), op(1), op(36, $v0); } /** * Yield to a block located at a particular symbol location. * * @param to the symbol containing the block to yield to * @param params optional block parameters to yield to the block */ function YieldBlock(op, to, positional) { SimpleArgs(op, positional, null, true), op(23, to), op(24), op(61), op(64), op(40), op(1); } /** * Push an (optional) yieldable block onto the stack. The yieldable block must be known * statically at compile time. * * @param block An optional Compilable block */ function PushYieldableBlock(op, block) { !function (op, parameters) { null !== parameters ? op(63, symbolTableOperand({ parameters: parameters })) : PushPrimitive(op, null); }(op, block && block[1]), op(62), PushCompilable(op, block); } /** * Invoke a block that is known statically at compile time. * * @param block a Compilable block */ function InvokeStaticBlock(op, block) { op(0), PushCompilable(op, block), op(61), op(2), op(1); } /** * Invoke a static block, preserving some number of stack entries for use in * updating. * * @param block A compilable block * @param callerCount A number of stack entries to preserve */ function InvokeStaticBlockWithStack(op, block, callerCount) { let parameters = block[1], calleeCount = parameters.length, count = Math.min(callerCount, calleeCount); if (0 !== count) { if (op(0), count) { op(39); for (let i = 0; i < count; i++) op(33, $fp, callerCount - i), op(19, parameters[i]); } PushCompilable(op, block), op(61), op(2), count && op(40), op(1); } else InvokeStaticBlock(op, block); } function PushCompilable(op, _block) { null === _block ? PushPrimitive(op, null) : op(28, { type: 4, value: _block }); } /** * Compile arguments, pushing an Arguments object onto the stack. * * @param args.params * @param args.hash * @param args.blocks * @param args.atNames */ function SimpleArgs(op, positional, named, atNames) { if (null === positional && null === named) return void op(83); let flags = CompilePositional(op, positional) << 4; atNames && (flags |= 8); let names = EMPTY_STRING_ARRAY; if (named) { names = named[0]; let val = named[1]; for (let i = 0; i < val.length; i++) expr(op, val[i]); } op(82, names, EMPTY_STRING_ARRAY, flags); } /** * Compile an optional list of positional arguments, which pushes each argument * onto the stack and returns the number of parameters compiled * * @param positional an optional list of positional arguments */ function CompilePositional(op, positional) { if (null === positional) return 0; for (let i = 0; i < positional.length; i++) expr(op, positional[i]); return positional.length; } function meta(layout) { let [, locals, upvars, lexicalSymbols] = layout.block; return { symbols: { locals: locals, upvars: upvars, lexical: lexicalSymbols }, scopeValues: layout.scope?.() ?? null, isStrictMode: layout.isStrictMode, moduleName: layout.moduleName, owner: layout.owner, size: locals.length }; } EXPRESSIONS.add(opcodes.Concat, (op, [, parts]) => { for (let part of parts) expr(op, part); op(27, parts.length); }), EXPRESSIONS.add(opcodes.Call, (op, [, expression, positional, named]) => { isGetFreeHelper(expression) ? op(1005, expression, handle => { Call(op, handle, positional, named); }) : (expr(op, expression), CallDynamic(op, positional, named)); }), EXPRESSIONS.add(opcodes.Curry, (op, [, expr, type, positional, named]) => { Curry(op, type, expr, positional, named); }), EXPRESSIONS.add(opcodes.GetSymbol, (op, [, sym, path]) => { op(21, sym), withPath(op, path); }), EXPRESSIONS.add(opcodes.GetLexicalSymbol, (op, [, sym, path]) => { op(1011, sym, handle => { op(29, handle), withPath(op, path); }); }), EXPRESSIONS.add(opcodes.GetStrictKeyword, (op, expr) => { op(1010, expr[1], () => { op(1005, expr, handle => { Call(op, handle, null, null); }); }); }), EXPRESSIONS.add(opcodes.GetFreeAsHelperHead, (op, expr) => { op(1010, expr[1], () => { op(1005, expr, handle => { Call(op, handle, null, null); }); }); }), EXPRESSIONS.add(opcodes.Undefined, op => PushPrimitiveReference(op, void 0)), EXPRESSIONS.add(opcodes.HasBlock, (op, [, block]) => { expr(op, block), op(25); }), EXPRESSIONS.add(opcodes.HasBlockParams, (op, [, block]) => { expr(op, block), op(24), op(61), op(26); }), EXPRESSIONS.add(opcodes.IfInline, (op, [, condition, truthy, falsy]) => { // Push in reverse order expr(op, falsy), expr(op, truthy), expr(op, condition), op(109); }), EXPRESSIONS.add(opcodes.Not, (op, [, value]) => { expr(op, value), op(110); }), EXPRESSIONS.add(opcodes.GetDynamicVar, (op, [, expression]) => { expr(op, expression), op(111); }), EXPRESSIONS.add(opcodes.Log, (op, [, positional]) => { op(0), SimpleArgs(op, positional, null, false), op(112), op(1), op(36, $v0); }); class NamedBlocksImpl { constructor(blocks) { this.blocks = blocks, this.names = blocks ? Object.keys(blocks) : []; } get(name) { return this.blocks && this.blocks[name] || null; } has(name) { let { blocks: blocks } = this; return null !== blocks && name in blocks; } with(name, block) { let { blocks: blocks } = this; return new NamedBlocksImpl(blocks ? assign({}, blocks, { [name]: block }) : { [name]: block }); } get hasAny() { return null !== this.blocks; } } const EMPTY_BLOCKS = new NamedBlocksImpl(null); function namedBlocks(blocks) { if (null === blocks) return EMPTY_BLOCKS; let out = dict(), [keys, values] = blocks; for (const [i, key] of enumerate(keys)) out[key] = values[i]; return new NamedBlocksImpl(out); } function SwitchCases(op, bootstrap, matcher) { // Setup the switch DSL let clauses = [], count = 0; // Call the callback matcher(function (match, callback) { clauses.push({ match: match, callback: callback, label: "CLAUSE" + count++ }); }), // Emit the opcodes for the switch op(69, 1), bootstrap(), op(1001); // First, emit the jump opcodes. We don't need a jump for the last // opcode, since it bleeds directly into its clause. for (let clause of clauses.slice(0, -1)) op(67, labelOperand(clause.label), clause.match); // Enumerate the clauses in reverse order. Earlier matches will // require fewer checks. for (let i = clauses.length - 1; i >= 0; i--) { let clause = clauses[i]; op(1e3, clause.label), op(34, 1), clause.callback(), // The first match is special: it is placed directly before the END // label, so no additional jump is needed at the end of it. 0 !== i && op(4, labelOperand("END")); } op(1e3, "END"), op(1002), op(70); } /** * A convenience for pushing some arguments on the stack and * running some code if the code needs to be re-executed during * updating execution if some of the arguments have changed. * * # Initial Execution * * The `args` function should push zero or more arguments onto * the stack and return the number of arguments pushed. * * The `body` function provides the instructions to execute both * during initial execution and during updating execution. * * Internally, this function starts by pushing a new frame, so * that the body can return and sets the return point ($ra) to * the ENDINITIAL label. * * It then executes the `args` function, which adds instructions * responsible for pushing the arguments for the block to the * stack. These arguments will be restored to the stack before * updating execution. * * Next, it adds the Enter opcode, which marks the current position * in the DOM, and remembers the current $pc (the next instruction) * as the first instruction to execute during updating execution. * * Next, it runs `body`, which adds the opcodes that should * execute both during initial execution and during updating execution. * If the `body` wishes to finish early, it should Jump to the * `FINALLY` label. * * Next, it adds the FINALLY label, followed by: * * - the Exit opcode, which finalizes the marked DOM started by the * Enter opcode. * - the Return opcode, which returns to the current return point * ($ra). * * Finally, it adds the ENDINITIAL label followed by the PopFrame * instruction, which restores $fp, $sp and $ra. * * # Updating Execution * * Updating execution for this `replayable` occurs if the `body` added an * assertion, via one of the `JumpIf`, `JumpUnless` or `AssertSame` opcodes. * * If, during updating executon, the assertion fails, the initial VM is * restored, and the stored arguments are pushed onto the stack. The DOM * between the starting and ending markers is cleared, and the VM's cursor * is set to the area just cleared. * * The return point ($ra) is set to -1, the exit instruction. * * Finally, the $pc is set to to the instruction saved off by the * Enter opcode during initial execution, and execution proceeds as * usual. * * The only difference is that when a `Return` instruction is * encountered, the program jumps to -1 rather than the END label, * and the PopFrame opcode is not needed. */ function Replayable(op, args, body) { // Start a new label frame, to give END and RETURN // a unique meaning. op(1001), op(0), // If the body invokes a block, its return will return to // END. Otherwise, the return in RETURN will return to END. op(6, labelOperand("ENDINITIAL")), // Start a new updating closure, remembering `count` elements // from the stack. Everything after this point, and before END, // will execute both initially and to update the block. // The enter and exit opcodes also track the area of the DOM // associated with this block. If an assertion inside the block // fails (for example, the test value changes from true to false // in an #if), the DOM is cleared and the program is re-executed, // restoring `count` elements to the stack and executing the // instructions between the enter and exit. op(69, args()), // Evaluate the body of the block. The body of the block may // return, which will jump execution to END during initial // execution, and exit the updating routine. body(), // All execution paths in the body should run the FINALLY once // they are done. It is executed both during initial execution // and during updating execution. op(1e3, "FINALLY"), // Finalize the DOM. op(70), // In initial execution, this is a noop: it returns to the // immediately following opcode. In updating execution, this // exits the updating routine. op(5), // Cleanup code for the block. Runs on initial execution // but not on updating. op(1e3, "ENDINITIAL"), op(1), op(1002); } /** * A specialized version of the `replayable` convenience that allows the * caller to provide different code based upon whether the item at * the top of the stack is true or false. * * As in `replayable`, the `ifTrue` and `ifFalse` code can invoke `return`. * * During the initial execution, a `return` will continue execution * in the cleanup code, which finalizes the current DOM block and pops * the current frame. * * During the updating execution, a `return` will exit the updating * routine, as it can reuse the DOM block and is always only a single * frame deep. */ function ReplayableIf(op, args, ifTrue, ifFalse) { return Replayable(op, args, () => { // If the conditional is false, jump to the ELSE label. op(66, labelOperand("ELSE")), // Otherwise, execute the code associated with the true branch. ifTrue(), // We're done, so return. In the initial execution, this runs // the cleanup code. In the updating VM, it exits the updating // routine. op(4, labelOperand("FINALLY")), op(1e3, "ELSE"), // If the conditional is false, and code associatied ith the // false branch was provided, execute it. If there was no code // associated with the false branch, jumping to the else statement // has no other behavior. void 0 !== ifFalse && ifFalse(); }); } function InvokeComponent(op, component, _elementBlock, positional, named, _blocks) { let { compilable: compilable, capabilities: capabilities, handle: handle } = component, elementBlock = _elementBlock ? [_elementBlock, []] : null, blocks = namedBlocks(_blocks); compilable ? (op(78, handle), function (op, { capabilities: capabilities, layout: layout, elementBlock: elementBlock, positional: positional, named: named, blocks: blocks }) { let { symbolTable: symbolTable } = layout; if (hasCapability(capabilities, InternalComponentCapabilities.prepareArgs)) return void InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock: elementBlock, positional: positional, named: named, atNames: true, blocks: blocks, layout: layout }); op(36, $s0), op(33, $sp, 1), op(35, $s0), op(0); // Setup arguments let { symbols: symbols } = symbolTable, blockSymbols = [], argSymbols = [], argNames = [], blockNames = blocks.names; // As we push values onto the stack, we store the symbols associated with them // so that we can set them on the scope later on with SetVariable and SetBlock // Starting with the attrs block, if it exists and is referenced in the component if (null !== elementBlock) { let symbol = symbols.indexOf("&attrs"); -1 !== symbol && (PushYieldableBlock(op, elementBlock), blockSymbols.push(symbol)); } // Followed by the other blocks, if they exist and are referenced in the component. // Also store the index of the associated symbol. for (const name of blockNames) { let symbol = symbols.indexOf(`&${name}`); -1 !== symbol && (PushYieldableBlock(op, blocks.get(name)), blockSymbols.push(symbol)); } // Next up we have arguments. If the component has the `createArgs` capability, // then it wants access to the arguments in JavaScript. We can't know whether // or not an argument is used, so we have to give access to all of them. if (hasCapability(capabilities, InternalComponentCapabilities.createArgs)) { // First we push positional arguments let flags = CompilePositional(op, positional) << 4; // setup the flags with the count of positionals, and to indicate that atNames // are used flags |= 8; let names = EMPTY_STRING_ARRAY; // Next, if named args exist, push them all. If they have an associated symbol // in the invoked component (e.g. they are used within its template), we push // that symbol. If not, we still push the expression as it may be used, and // we store the symbol as -1 (this is used later). if (null !== named) { names = named[0]; let val = named[1]; for (let i = 0; i < val.length; i++) { let symbol = symbols.indexOf(names[i]); expr(op, val[i]), argSymbols.push(symbol); } } // Finally, push the VM arguments themselves. These args won't need access // to blocks (they aren't accessible from userland anyways), so we push an // empty array instead of the actual block names. op(82, names, EMPTY_STRING_ARRAY, flags), // And push an extra pop operation to remove the args before we begin setting // variables on the local context argSymbols.push(-1); } else if (null !== named) { // If the component does not have the `createArgs` capability, then the only // expressions we need to push onto the stack are those that are actually // referenced in the template of the invoked component (e.g. have symbols). let names = named[0], val = named[1]; for (let i = 0; i < val.length; i++) { let name = names[i], symbol = symbols.indexOf(name); -1 !== symbol && (expr(op, val[i]), argSymbols.push(symbol), argNames.push(name)); } } op(97, $s0), hasCapability(capabilities, InternalComponentCapabilities.dynamicScope) && op(59), hasCapability(capabilities, InternalComponentCapabilities.createInstance) && // eslint-disable-next-line @typescript-eslint/no-explicit-any op(87, 0 | blocks.has("default")), op(88, $s0), hasCapability(capabilities, InternalComponentCapabilities.createArgs) ? op(90, $s0) : op(90, $s0, argNames), // Setup the new root scope for the component op(37, symbols.length + 1, Object.keys(blocks).length > 0 ? 1 : 0), // Pop the self reference off the stack and set it to the symbol for `this` // in the new scope. This is why all subsequent symbols are increased by one. op(19, 0); // Going in reverse, now we pop the args/blocks off the stack, starting with // arguments, and assign them to their symbols in the new scope. for (const symbol of reverse(argSymbols)) // for (let i = argSymbols.length - 1; i >= 0; i--) { // let symbol = argSymbols[i]; -1 === symbol ? // The expression was not bound to a local symbol, it was only pushed to be // used with VM args in the javascript side op(34, 1) : op(19, symbol + 1); // if any positional params exist, pop them off the stack as well null !== positional && op(34, positional.length); // Finish up by popping off and assigning blocks for (const symbol of reverse(blockSymbols)) op(20, symbol + 1); op(28, layoutOperand(layout)), op(61), op(2), op(100, $s0), op(1), op(40), hasCapability(capabilities, InternalComponentCapabilities.dynamicScope) && op(60), op(98), op(35, $s0); }(op, { capabilities: capabilities, layout: compilable, elementBlock: elementBlock, positional: positional, named: named, blocks: blocks })) : (op(78, handle), InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock: elementBlock, positional: positional, named: named, atNames: true, blocks: blocks })); } function InvokeDynamicComponent(op, definition, _elementBlock, positional, named, _blocks, atNames, curried) { let elementBlock = _elementBlock ? [_elementBlock, []] : null, blocks = namedBlocks(_blocks); Replayable(op, () => (expr(op, definition), op(33, $sp, 0), 2), () => { op(66, labelOperand("ELSE")), curried ? op(81) : op(80, { type: 2, value: void 0 }), op(79), InvokeNonStaticComponent(op, { capabilities: true, elementBlock: elementBlock, positional: positional, named: named, atNames: atNames, blocks: blocks }), op(1e3, "ELSE"); }); } function InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock: elementBlock, positional: positional, named: named, atNames: atNames, blocks: namedBlocks, layout: layout }) { let bindableBlocks = !!namedBlocks, bindableAtNames = true === capabilities || hasCapability(capabilities, InternalComponentCapabilities.prepareArgs) || !(!named || 0 === named[0].length), blocks = namedBlocks.with("attrs", elementBlock); op(36, $s0), op(33, $sp, 1), op(35, $s0), op(0), function (op, positional, named, blocks, atNames) { let blockNames = blocks.names; for (const name of blockNames) PushYieldableBlock(op, blocks.get(name)); let flags = CompilePositional(op, positional) << 4; atNames && (flags |= 8), blocks.hasAny && (flags |= 7); let names = EMPTY_ARRAY; if (named) { names = named[0]; let val = named[1]; for (let i = 0; i < val.length; i++) expr(op, val[i]); } op(82, names, blockNames, flags); }(op, positional, named, blocks, atNames), op(85, $s0), invokePreparedComponent(op, blocks.has("default"), bindableBlocks, bindableAtNames, () => { layout ? (op(63, symbolTableOperand(layout.symbolTable)), op(28, layoutOperand(layout)), op(61)) : op(92, $s0), op(95, $s0); }), op(35, $s0); } function invokePreparedComponent(op, hasBlock, bindableBlocks, bindableAtNames, populateLayout = null) { op(97, $s0), op(59), // eslint-disable-next-line @typescript-eslint/no-explicit-any op(87, 0 | hasBlock), // this has to run after createComponent to allow // for late-bound layouts, but a caller is free // to populate the layout earlier if it wants to // and do nothing here. populateLayout && populateLayout(), op(88, $s0), op(90, $s0), op(38, $s0), op(19, 0), bindableAtNames && op(17, $s0), bindableBlocks && op(18, $s0), op(34, 1), op(96, $s0), op(100, $s0), op(1), op(40), op(60), op(98); } const STATEMENTS = new Compilers(), INFLATE_ATTR_TABLE = ["class", "id", "value", "name", "type", "style", "href"], INFLATE_TAG_TABLE = ["div", "span", "p", "a"]; function inflateTagName(tagName) { return "string" == typeof tagName ? tagName : INFLATE_TAG_TABLE[tagName]; } function inflateAttrName(attrName) { return "string" == typeof attrName ? attrName : INFLATE_ATTR_TABLE[attrName]; } function hashToArgs(hash) { return null === hash ? null : [hash[0].map(key => `@${key}`), hash[1]]; } STATEMENTS.add(opcodes.Comment, (op, sexp) => op(42, sexp[1])), STATEMENTS.add(opcodes.CloseElement, op => op(55)), STATEMENTS.add(opcodes.FlushElement, op => op(54)), STATEMENTS.add(opcodes.Modifier, (op, [, expression, positional, named]) => { isGetFreeModifier(expression) ? op(1003, expression, handle => { op(0), SimpleArgs(op, positional, named, false), op(57, handle), op(1); }) : (expr(op, expression), op(0), SimpleArgs(op, positional, named, false), op(33, $fp, 1), op(108), op(1)); }), STATEMENTS.add(opcodes.StaticAttr, (op, [, name, value, namespace]) => { op(51, inflateAttrName(name), value, namespace ?? null); }), STATEMENTS.add(opcodes.StaticComponentAttr, (op, [, name, value, namespace]) => { op(105, inflateAttrName(name), value, namespace ?? null); }), STATEMENTS.add(opcodes.DynamicAttr, (op, [, name, value, namespace]) => { expr(op, value), op(52, inflateAttrName(name), false, namespace ?? null); }), STATEMENTS.add(opcodes.TrustingDynamicAttr, (op, [, name, value, namespace]) => { expr(op, value), op(52, inflateAttrName(name), true, namespace ?? null); }), STATEMENTS.add(opcodes.ComponentAttr, (op, [, name, value, namespace]) => { expr(op, value), op(53, inflateAttrName(name), false, namespace ?? null); }), STATEMENTS.add(opcodes.TrustingComponentAttr, (op, [, name, value, namespace]) => { expr(op, value), op(53, inflateAttrName(name), true, namespace ?? null); }), STATEMENTS.add(opcodes.OpenElement, (op, [, tag]) => { op(48, inflateTagName(tag)); }), STATEMENTS.add(opcodes.OpenElementWithSplat, (op, [, tag]) => { op(89), op(48, inflateTagName(tag)); }), STATEMENTS.add(opcodes.Component, (op, [, expr, elementBlock, named, blocks]) => { isGetFreeComponent(expr) ? op(1004, expr, component => { InvokeComponent(op, component, elementBlock, null, named, blocks); }) : // otherwise, the component name was an expression, so resolve the expression // and invoke it as a dynamic component InvokeDynamicComponent(op, expr, elementBlock, null, named, blocks, true, true); }), STATEMENTS.add(opcodes.Yield, (op, [, to, params]) => YieldBlock(op, to, params)), STATEMENTS.add(opcodes.AttrSplat, (op, [, to]) => YieldBlock(op, to, null)), STATEMENTS.add(opcodes.Debugger, (op, [, locals, upvars, lexical]) => { op(103, function (locals, upvars, lexical) { return { type: 3, value: { locals: locals, upvars: upvars, lexical: lexical } }; }(locals, upvars, lexical)); }), STATEMENTS.add(opcodes.Append, (op, [, value]) => { // Special case for static values if (Array.isArray(value)) { if (isGetFreeComponentOrHelper(value)) op(1008, value, { ifComponent(component) { InvokeComponent(op, component, null, null, null, null); }, ifHelper(handle) { op(0), Call(op, handle, null, null), op(3, stdlibOperand("cautious-non-dynamic-append")), op(1); }, ifValue(handle) { op(0), op(29, handle), op(3, stdlibOperand("cautious-non-dynamic-append")), op(1); } });else if (value[0] === opcodes.Call) { let [, expression, positional, named] = value; isGetFreeComponentOrHelper(expression) ? op(1007, expression, { ifComponent(component) { InvokeComponent(op, component, null, positional, hashToArgs(named), null); }, ifHelper(handle) { op(0), Call(op, handle, positional, named), op(3, stdlibOperand("cautious-non-dynamic-append")), op(1); } }) : SwitchCases(op, () => { expr(op, expression), op(106); }, when => { when(ContentType.Component, () => { op(81), op(79), InvokeNonStaticComponent(op, { capabilities: true, elementBlock: null, positional: positional, named: named, atNames: false, blocks: namedBlocks(null) }); }), when(ContentType.Helper, () => { CallDynamic(op, positional, named, () => { op(3, stdlibOperand("cautious-non-dynamic-append")); }); }); }); } else op(0), expr(op, value), op(3, stdlibOperand("cautious-append")), op(1); } else op(41, null == value ? "" : String(value)); }), STATEMENTS.add(opcodes.TrustingAppend, (op, [, value]) => { Array.isArray(value) ? (op(0), expr(op, value), op(3, stdlibOperand("trusting-append")), op(1)) : op(41, null == value ? "" : String(value)); }), STATEMENTS.add(opcodes.Block, (op, [, expr, positional, named, blocks]) => { isGetFreeComponent(expr) ? op(1004, expr, component => { InvokeComponent(op, component, null, positional, hashToArgs(named), blocks); }) : InvokeDynamicComponent(op, expr, null, positional, named, blocks, false, false); }), STATEMENTS.add(opcodes.InElement, (op, [, block, guid, destination, insertBefore]) => { ReplayableIf(op, () => (expr(op, guid), void 0 === insertBefore ? PushPrimitiveReference(op, void 0) : expr(op, insertBefore), expr(op, destination), op(33, $sp, 0), 4), () => { op(50), InvokeStaticBlock(op, block), op(56); }); }), STATEMENTS.add(opcodes.If, (op, [, condition, block, inverse]) => ReplayableIf(op, () => (expr(op, condition), op(71), 1), () => { InvokeStaticBlock(op, block); }, inverse ? () => { InvokeStaticBlock(op, inverse); } : void 0)), STATEMENTS.add(opcodes.Each, (op, [, value, key, block, inverse]) => Replayable(op, () => (key ? expr(op, key) : PushPrimitiveReference(op, null), expr(op, value), 2), () => { op(72, labelOperand("BODY"), labelOperand("ELSE")), op(0), op(33, $fp, 1), op(6, labelOperand("ITER")), op(1e3, "ITER"), op(74, labelOperand("BREAK")), op(1e3, "BODY"), InvokeStaticBlockWithStack(op, block, 2), op(34, 2), op(4, labelOperand("FINALLY")), op(1e3, "BREAK"), op(1), op(73), op(4, labelOperand("FINALLY")), op(1e3, "ELSE"), inverse && InvokeStaticBlock(op, inverse); })), STATEMENTS.add(opcodes.Let, (op, [, positional, block]) => { InvokeStaticBlockWithStack(op, block, CompilePositional(op, positional)); }), STATEMENTS.add(opcodes.WithDynamicVars, (op, [, named, block]) => { if (named) { let [names, expressions] = named; CompilePositional(op, expressions), function (op, names, block) { op(59), op(58, names), block(), op(60); }(op, names, () => { InvokeStaticBlock(op, block); }); } else InvokeStaticBlock(op, block); }), STATEMENTS.add(opcodes.InvokeComponent, (op, [, expr, positional, named, blocks]) => { isGetFreeComponent(expr) ? op(1004, expr, component => { InvokeComponent(op, component, null, positional, hashToArgs(named), blocks); }) : InvokeDynamicComponent(op, expr, null, positional, named, blocks, false, false); }); class CompilableTemplateImpl { constructor(statements, meta, // Part of CompilableTemplate symbolTable, // Used for debugging moduleName = "plain block") { this.statements = statements, this.meta = meta, this.symbolTable = symbolTable, this.moduleName = moduleName, this.compiled = null; } // Part of CompilableTemplate compile(context) { return function (compilable, context) { if (null !== compilable.compiled) return compilable.compiled; compilable.compiled = -1; let { statements: statements, meta: meta } = compilable, result = compileStatements(statements, meta, context); return compilable.compiled = result, result; }(this, context); } } function compilable(layout, moduleName) { let [statements, symbols] = layout.block; return new CompilableTemplateImpl(statements, meta(layout), { symbols: symbols }, moduleName); } function compileStatements(statements, meta, syntaxContext) { let sCompiler = STATEMENTS, context = templateCompilationContext(syntaxContext, meta), { encoder: encoder, evaluation: evaluation } = context; function pushOp(...op) { encodeOp(encoder, evaluation, meta, op); } for (const statement of statements) sCompiler.compile(pushOp, statement); return context.encoder.commit(meta.size); } const DEFAULT_CAPABILITIES = { dynamicLayout: true, dynamicTag: true, prepareArgs: true, createArgs: true, attributeHook: false, elementHook: false, dynamicScope: true, createCaller: false, updateHook: true, createInstance: true, wrapped: false, willDestroy: false, hasSubOwner: false }, MINIMAL_CAPABILITIES = { dynamicLayout: false, dynamicTag: false, prepareArgs: false, createArgs: false, attributeHook: false, elementHook: false, dynamicScope: false, createCaller: false, updateHook: false, createInstance: false, wrapped: false, willDestroy: false, hasSubOwner: false }; class StdLib { constructor(main, trustingGuardedAppend, cautiousGuardedAppend, trustingNonDynamicAppend