UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,143 lines (1,125 loc) 62.8 kB
import { isSmallInt, encodeImmediate, assert as debugAssert, EMPTY_STRING_ARRAY, unwrap, reverse, expect, debugToString as debugToString$1, Stack as StackImpl, isPresentArray, encodeHandle, EMPTY_ARRAY, assign, enumerate, dict } from '../util/index.js'; import { Op, MachineOp, $v0, $fp, InternalComponentCapabilities, $s0, $sp, ContentType, $s1, TYPE_SIZE, isMachineOp, MACHINE_MASK, ARG_SHIFT } from '../vm/index.js'; import { InstructionEncoderImpl } from '../encoder/index.js'; import { SexpOpcodes as opcodes } from '../wire-format/index.js'; import { hasCapability } from '../manager/index.js'; import { isDevelopingApp } from '@embroider/macros'; let debugCompiler; function makeResolutionTypeVerifier(typeToVerify) { return opcode => { if (!function (opcode) { return Array.isArray(opcode) && 2 === opcode.length; }(opcode)) return !1; let type = opcode[0]; return type === opcodes.GetStrictKeyword || type === opcodes.GetLexicalSymbol || type === typeToVerify; }; } new Array(Op.Size).fill(null), new Array(Op.Size).fill(null); const isGetFreeComponent = makeResolutionTypeVerifier(opcodes.GetFreeAsComponentHead), isGetFreeModifier = makeResolutionTypeVerifier(opcodes.GetFreeAsModifierHead), isGetFreeHelper = makeResolutionTypeVerifier(opcodes.GetFreeAsHelperHead), isGetFreeComponentOrHelper = makeResolutionTypeVerifier(opcodes.GetFreeAsComponentOrHelperHead); function assertResolverInvariants(meta) { if (isDevelopingApp()) { if (!meta.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 { upvars: upvars } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), helper = resolver.lookupBuiltInHelper(name); 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 debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve a ${type} in a strict mode template, but that value was not in scope: ${meta.upvars[expr[1]] ?? "{unknown variable}"}`); return constants.helper(helper, name); } const HighLevelResolutionOpcodes = { Modifier: 1003, Component: 1004, Helper: 1005, ComponentOrHelper: 1007, OptionalComponentOrHelper: 1008, Local: 1010, TemplateLocal: 1011 }, HighLevelBuilderOpcodes = { Label: 1e3, StartLabels: 1001, StopLabels: 1002, Start: 1e3, End: 1002 }, HighLevelOperands = { Label: 1, IsStrictMode: 2, DebugSymbols: 3, Block: 4, StdLib: 5, NonSmallInt: 6, SymbolTable: 7, Layout: 8 }; function labelOperand(value) { return { type: HighLevelOperands.Label, value: value }; } function isStrictMode() { return { type: HighLevelOperands.IsStrictMode, value: void 0 }; } function stdlibOperand(value) { return { type: HighLevelOperands.StdLib, value: value }; } function symbolTableOperand(value) { return { type: HighLevelOperands.SymbolTable, value: value }; } function layoutOperand(value) { return { type: HighLevelOperands.Layout, value: value }; } class Labels { labels = dict(); targets = []; 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) { let address = labels[target] - at; debugAssert(-1 === heap.getbyaddr(at), "Expected heap to contain a placeholder, but it did not"), heap.setbyaddr(at, address); } } } function encodeOp(encoder, constants, resolver, meta, op) { if (function (op) { return op < HighLevelBuilderOpcodes.Start; }(op[0])) { let [type, ...operands] = op; encoder.push(constants, type, ...operands); } else switch (op[0]) { case HighLevelBuilderOpcodes.Label: return encoder.label(op[1]); case HighLevelBuilderOpcodes.StartLabels: return encoder.startLabels(); case HighLevelBuilderOpcodes.StopLabels: return encoder.stopLabels(); case HighLevelResolutionOpcodes.Component: return function (resolver, constants, meta, [, expr, then]) { debugAssert(isGetFreeComponent(expr), "Attempted to resolve a component with incorrect opcode"); let type = expr[0]; if (isDevelopingApp() && expr[0] === opcodes.GetStrictKeyword) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve a component in a strict mode template, but that value was not in scope: ${meta.upvars[expr[1]] ?? "{unknown variable}"}`); if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, owner: owner } = meta, definition = expect(scopeValues, "BUG: scopeValues must exist if template symbol is used")[expr[1]]; then(constants.component(definition, expect(owner, "BUG: expected owner when resolving component definition"))); } else { let { upvars: upvars, owner: owner } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), definition = resolver.lookupComponent(name, owner); if (isDevelopingApp() && ("object" != typeof definition || null === definition)) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve \`${name}\`, which was expected to be a component, but nothing was found.`); then(constants.resolvedComponent(definition, name)); } } /** * (helper) * (helper arg) */(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Modifier: /** * <div {{modifier}}/> * <div {{modifier arg}}/> * <Foo {{modifier}}/> */ return function (resolver, constants, meta, [, expr, then]) { debugAssert(isGetFreeModifier(expr), "Attempted to resolve a modifier with incorrect opcode"); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues } = meta, definition = expect(scopeValues, "BUG: scopeValues must exist if template symbol is used")[expr[1]]; then(constants.modifier(definition)); } else if (type === opcodes.GetStrictKeyword) { let { upvars: upvars } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), modifier = resolver.lookupBuiltInModifier(name); if (isDevelopingApp() && null === modifier) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve a modifier in a strict mode template, but it was not in scope: ${name}`); then(constants.modifier(modifier, name)); } else { let { upvars: upvars, owner: owner } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), modifier = resolver.lookupModifier(name, owner); if (isDevelopingApp() && null === modifier) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve \`${name}\`, which was expected to be a modifier, but nothing was found.`); then(constants.modifier(modifier, name)); } } /** * {{component-or-helper arg}} */(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Helper: return function (resolver, constants, meta, [, expr, then]) { debugAssert(isGetFreeHelper(expr), "Attempted to resolve a helper with incorrect opcode"); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues } = meta, definition = expect(scopeValues, "BUG: scopeValues must exist if template symbol is used")[expr[1]]; then(constants.helper(definition)); } else if (type === opcodes.GetStrictKeyword) then(lookupBuiltInHelper(expr, resolver, meta, constants, "helper"));else { let { upvars: upvars, owner: owner } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), helper = resolver.lookupHelper(name, owner); if (isDevelopingApp() && null === helper) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve \`${name}\`, which was expected to be a helper, but nothing was found.`); then(constants.helper(helper, name)); } }(resolver, constants, meta, op); case HighLevelResolutionOpcodes.ComponentOrHelper: return function (resolver, constants, meta, [, expr, { ifComponent: ifComponent, ifHelper: ifHelper }]) { debugAssert(isGetFreeComponentOrHelper(expr), "Attempted to resolve a component or helper with incorrect opcode"); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, owner: owner } = meta, definition = expect(scopeValues, "BUG: scopeValues must exist if template symbol is used")[expr[1]], component = constants.component(definition, expect(owner, "BUG: expected owner when resolving component definition"), !0); if (null !== component) return void ifComponent(component); let helper = constants.helper(definition, null, !0); if (isDevelopingApp() && null === helper) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`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(expect(helper, "BUG: helper must exist")); } else if (type === opcodes.GetStrictKeyword) ifHelper(lookupBuiltInHelper(expr, resolver, meta, constants, "component or helper"));else { let { upvars: upvars, owner: owner } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), definition = resolver.lookupComponent(name, owner); if (null !== definition) ifComponent(constants.resolvedComponent(definition, name));else { let helper = resolver.lookupHelper(name, owner); if (isDevelopingApp() && null === helper) throw debugAssert(!meta.isStrictMode, "Strict mode errors should already be handled at compile time"), new Error(`Attempted to resolve \`${name}\`, which was expected to be a component or helper, but nothing was found.`); ifHelper(constants.helper(helper, name)); } } } /** * {{maybeHelperOrComponent}} */(resolver, constants, meta, op); case HighLevelResolutionOpcodes.OptionalComponentOrHelper: return function (resolver, constants, meta, [, expr, { ifComponent: ifComponent, ifHelper: ifHelper, ifValue: ifValue }]) { debugAssert(isGetFreeComponentOrHelper(expr), "Attempted to resolve an optional component or helper with incorrect opcode"); let type = expr[0]; if (type === opcodes.GetLexicalSymbol) { let { scopeValues: scopeValues, owner: owner } = meta, definition = expect(scopeValues, "BUG: scopeValues must exist if template symbol is used")[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, expect(owner, "BUG: expected owner when resolving component definition"), !0); if (null !== component) return void ifComponent(component); let helper = constants.helper(definition, null, !0); 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 { upvars: upvars, owner: owner } = assertResolverInvariants(meta), name = unwrap(upvars[expr[1]]), definition = resolver.lookupComponent(name, owner); if (null !== definition) return void ifComponent(constants.resolvedComponent(definition, name)); let helper = resolver.lookupHelper(name, owner); null !== helper && ifHelper(constants.helper(helper, name)); } }(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Local: { let freeVar = op[1], name = expect(meta.upvars, "BUG: attempted to resolve value but no upvars found")[freeVar]; (0, op[2])(name, meta.moduleName); break; } case HighLevelResolutionOpcodes.TemplateLocal: { let [, valueIndex, then] = op, value = expect(meta.scopeValues, "BUG: Attempted to get a template local, but template does not have any")[valueIndex]; then(constants.value(value)); break; } default: throw new Error(`Unexpected high level opcode ${op[0]}`); } } class EncoderImpl { labelsStack = new StackImpl(); encoder = new InstructionEncoderImpl([]); errors = []; handle; constructor(heap, meta, stdlib) { this.heap = heap, this.meta = meta, this.stdlib = stdlib, this.handle = heap.malloc(); } error(error) { this.encoder.encode(Op.Primitive, 0), this.errors.push(error); } commit(size) { let handle = this.handle; return this.heap.pushMachine(MachineOp.Return), this.heap.finishMalloc(handle, size), isPresentArray(this.errors) ? { errors: this.errors, handle: handle } : handle; } push(constants, type, ...args) { let { heap: heap } = this; if (isDevelopingApp() && type > TYPE_SIZE) throw new Error(`Opcode type over 8-bits. Got ${type}.`); let first = type | (isMachineOp(type) ? 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 encodeHandle(constants.array(operand)); switch (operand.type) { case HighLevelOperands.Label: return this.currentLabels.target(this.heap.offset, operand.value), -1; case HighLevelOperands.IsStrictMode: return encodeHandle(constants.value(this.meta.isStrictMode)); case HighLevelOperands.DebugSymbols: return encodeHandle(constants.array(this.meta.evalSymbols || EMPTY_STRING_ARRAY)); case HighLevelOperands.Block: return encodeHandle(constants.value((block = operand.value, containing = this.meta, new CompilableTemplateImpl(block[0], containing, { parameters: block[1] || EMPTY_ARRAY })))); case HighLevelOperands.StdLib: return expect(this.stdlib, "attempted to encode a stdlib operand, but the encoder did not have a stdlib. Are you currently building the stdlib?")[operand.value]; case HighLevelOperands.NonSmallInt: case HighLevelOperands.SymbolTable: case HighLevelOperands.Layout: return constants.value(operand.value); } } var block, containing; return encodeHandle(constants.value(operand)); } get currentLabels() { return expect(this.labelsStack.current, "bug: not in a label stack"); } label(name) { this.currentLabels.label(name, this.heap.offset + 1); } startLabels() { this.labelsStack.push(new Labels()); } stopLabels() { expect(this.labelsStack.pop(), "unbalanced push and pop labels").patch(this.heap); } } class StdLib { constructor(main, trustingGuardedAppend, cautiousGuardedAppend, trustingNonDynamicAppend, cautiousNonDynamicAppend) { this.main = main, this.trustingGuardedAppend = trustingGuardedAppend, this.cautiousGuardedAppend = cautiousGuardedAppend, this.trustingNonDynamicAppend = trustingNonDynamicAppend, this.cautiousNonDynamicAppend = cautiousNonDynamicAppend; } get "trusting-append"() { return this.trustingGuardedAppend; } get "cautious-append"() { return this.cautiousGuardedAppend; } get "trusting-non-dynamic-append"() { return this.trustingNonDynamicAppend; } get "cautious-non-dynamic-append"() { return this.cautiousNonDynamicAppend; } getAppend(trusting) { return trusting ? this.trustingGuardedAppend : this.cautiousGuardedAppend; } } class NamedBlocksImpl { names; 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] = unwrap(values[i]); return new NamedBlocksImpl(out); } /** * 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(Op.PrimitiveReference); } /** * 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 = isSmallInt(p) ? encodeImmediate(p) : (debugAssert(!isSmallInt(value = p), "Attempted to make a operand for an int that was not a small int, you should encode this as an immediate"), { type: HighLevelOperands.NonSmallInt, value: value })), op(Op.Primitive, 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(MachineOp.PushFrame), SimpleArgs(op, positional, named, !1), op(Op.Helper, handle), op(MachineOp.PopFrame), op(Op.Fetch, $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(MachineOp.PushFrame), SimpleArgs(op, positional, named, !1), op(Op.Dup, $fp, 1), op(Op.DynamicHelper), append ? (op(Op.Fetch, $v0), append(), op(MachineOp.PopFrame), op(Op.Pop, 1)) : (op(MachineOp.PopFrame), op(Op.Pop, 1), op(Op.Fetch, $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(MachineOp.PushFrame), SimpleArgs(op, positional, named, !1), op(Op.CaptureArgs), expr(op, definition), op(Op.Curry, type, isStrictMode()), op(MachineOp.PopFrame), op(Op.Fetch, $v0); } class Compilers { names = {}; funcs = []; add(name, func) { this.names[name] = this.funcs.push(func) - 1; } compile(op, sexp) { let name = sexp[0], index = unwrap(this.names[name]), func = this.funcs[index]; debugAssert(!!func, `expected an implementation for ${sexp[0]}`), func(op, sexp); } } const EXPRESSIONS = new Compilers(); function withPath(op, path) { if (void 0 !== path && 0 !== path.length) for (let i = 0; i < path.length; i++) op(Op.GetProperty, path[i]); } function expr(op, expression) { Array.isArray(expression) ? EXPRESSIONS.compile(op, expression) : (PushPrimitive(op, expression), op(Op.PrimitiveReference)); } /** * 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(Op.PushEmptyArgs); 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(Op.PushArgs, 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 [, symbols,, upvars] = layout.block; return { evalSymbols: evalSymbols(layout), upvars: upvars, scopeValues: layout.scope?.() ?? null, isStrictMode: layout.isStrictMode, moduleName: layout.moduleName, owner: layout.owner, size: symbols.length }; } function evalSymbols(layout) { let { block: block } = layout, [, symbols, hasEval] = block; return hasEval ? symbols : null; } /** * 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, !0), op(Op.GetBlock, to), op(Op.SpreadBlock), op(Op.CompileBlock), op(Op.InvokeYield), op(Op.PopScope), op(MachineOp.PopFrame); } /** * 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(Op.PushSymbolTable, symbolTableOperand({ parameters: parameters })) : PushPrimitive(op, null); }(op, block && block[1]), op(Op.PushBlockScope), PushCompilable(op, block); } /** * Invoke a block that is known statically at compile time. * * @param block a Compilable block */ function InvokeStaticBlock(op, block) { op(MachineOp.PushFrame), PushCompilable(op, block), op(Op.CompileBlock), op(MachineOp.InvokeVirtual), op(MachineOp.PopFrame); } /** * 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(MachineOp.PushFrame), count) { op(Op.ChildScope); for (let i = 0; i < count; i++) op(Op.Dup, $fp, callerCount - i), op(Op.SetVariable, parameters[i]); } PushCompilable(op, block), op(Op.CompileBlock), op(MachineOp.InvokeVirtual), count && op(Op.PopScope), op(MachineOp.PopFrame); } else InvokeStaticBlock(op, block); } function PushCompilable(op, _block) { var value; null === _block ? PushPrimitive(op, null) : op(Op.Constant, (value = _block, { type: HighLevelOperands.Block, value: value })); } 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(Op.Enter, 1), bootstrap(), op(HighLevelBuilderOpcodes.StartLabels); // 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(Op.JumpEq, 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 = unwrap(clauses[i]); op(HighLevelBuilderOpcodes.Label, clause.label), op(Op.Pop, 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(MachineOp.Jump, labelOperand("END")); } op(HighLevelBuilderOpcodes.Label, "END"), op(HighLevelBuilderOpcodes.StopLabels), op(Op.Exit); } /** * 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(HighLevelBuilderOpcodes.StartLabels), op(MachineOp.PushFrame), // If the body invokes a block, its return will return to // END. Otherwise, the return in RETURN will return to END. op(MachineOp.ReturnTo, labelOperand("ENDINITIAL")); // Push the arguments onto the stack. The args() function // tells us how many stack elements to retain for re-execution // when updating. let count = args(); // 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(Op.Enter, count), // 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(HighLevelBuilderOpcodes.Label, "FINALLY"), // Finalize the DOM. op(Op.Exit), // In initial execution, this is a noop: it returns to the // immediately following opcode. In updating execution, this // exits the updating routine. op(MachineOp.Return), // Cleanup code for the block. Runs on initial execution // but not on updating. op(HighLevelBuilderOpcodes.Label, "ENDINITIAL"), op(MachineOp.PopFrame), op(HighLevelBuilderOpcodes.StopLabels); } /** * 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(Op.JumpUnless, 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(MachineOp.Jump, labelOperand("FINALLY")), op(HighLevelBuilderOpcodes.Label, "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(); }); } // {{component}} // <Component> // chokepoint function InvokeComponent(op, component, _elementBlock, positional, named, _blocks) { let { compilable: compilable, capabilities: capabilities, handle: handle } = component, elementBlock = _elementBlock ? [_elementBlock, []] : null, blocks = Array.isArray(_blocks) || null === _blocks ? namedBlocks(_blocks) : _blocks; compilable ? (op(Op.PushComponentDefinition, handle), function (op, { capabilities: capabilities, layout: layout, elementBlock: elementBlock, positional: positional, named: named, blocks: blocks }) { let { symbolTable: symbolTable } = layout; if (symbolTable.hasEval || hasCapability(capabilities, InternalComponentCapabilities.prepareArgs)) return void InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock: elementBlock, positional: positional, named: named, atNames: !0, blocks: blocks, layout: layout }); op(Op.Fetch, $s0), op(Op.Dup, $sp, 1), op(Op.Load, $s0), op(MachineOp.PushFrame); // 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(unwrap(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(Op.PushArgs, 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 = unwrap(names[i]), symbol = symbols.indexOf(name); -1 !== symbol && (expr(op, val[i]), argSymbols.push(symbol), argNames.push(name)); } } op(Op.BeginComponentTransaction, $s0), hasCapability(capabilities, InternalComponentCapabilities.dynamicScope) && op(Op.PushDynamicScope), hasCapability(capabilities, InternalComponentCapabilities.createInstance) && op(Op.CreateComponent, 0 | blocks.has("default"), $s0), op(Op.RegisterComponentDestructor, $s0), hasCapability(capabilities, InternalComponentCapabilities.createArgs) ? op(Op.GetComponentSelf, $s0) : op(Op.GetComponentSelf, $s0, argNames), // Setup the new root scope for the component op(Op.RootScope, 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(Op.SetVariable, 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(Op.Pop, 1) : op(Op.SetVariable, symbol + 1); // if any positional params exist, pop them off the stack as well null !== positional && op(Op.Pop, positional.length); // Finish up by popping off and assigning blocks for (const symbol of reverse(blockSymbols)) op(Op.SetBlock, symbol + 1); op(Op.Constant, layoutOperand(layout)), op(Op.CompileBlock), op(MachineOp.InvokeVirtual), op(Op.DidRenderLayout, $s0), op(MachineOp.PopFrame), op(Op.PopScope), hasCapability(capabilities, InternalComponentCapabilities.dynamicScope) && op(Op.PopDynamicScope), op(Op.CommitComponentTransaction), op(Op.Load, $s0); }(op, { capabilities: capabilities, layout: compilable, elementBlock: elementBlock, positional: positional, named: named, blocks: blocks })) : (op(Op.PushComponentDefinition, handle), InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock: elementBlock, positional: positional, named: named, atNames: !0, blocks: blocks })); } function InvokeDynamicComponent(op, definition, _elementBlock, positional, named, _blocks, atNames, curried) { let elementBlock = _elementBlock ? [_elementBlock, []] : null, blocks = Array.isArray(_blocks) || null === _blocks ? namedBlocks(_blocks) : _blocks; Replayable(op, () => (expr(op, definition), op(Op.Dup, $sp, 0), 2), () => { op(Op.JumpUnless, labelOperand("ELSE")), curried ? op(Op.ResolveCurriedComponent) : op(Op.ResolveDynamicComponent, isStrictMode()), op(Op.PushDynamicComponentInstance), InvokeNonStaticComponent(op, { capabilities: !0, elementBlock: elementBlock, positional: positional, named: named, atNames: atNames, blocks: blocks }), op(HighLevelBuilderOpcodes.Label, "ELSE"); }); } function InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock: elementBlock, positional: positional, named: named, atNames: atNames, blocks: namedBlocks, layout: layout }) { let bindableBlocks = !!namedBlocks, bindableAtNames = !0 === capabilities || hasCapability(capabilities, InternalComponentCapabilities.prepareArgs) || !(!named || 0 === named[0].length), blocks = namedBlocks.with("attrs", elementBlock); op(Op.Fetch, $s0), op(Op.Dup, $sp, 1), op(Op.Load, $s0), op(MachineOp.PushFrame), 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 && (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(Op.PushArgs, names, blockNames, flags); }(op, positional, named, blocks, atNames), op(Op.PrepareArgs, $s0), invokePreparedComponent(op, blocks.has("default"), bindableBlocks, bindableAtNames, () => { layout ? (op(Op.PushSymbolTable, symbolTableOperand(layout.symbolTable)), op(Op.Constant, layoutOperand(layout)), op(Op.CompileBlock)) : op(Op.GetComponentLayout, $s0), op(Op.PopulateLayout, $s0); }), op(Op.Load, $s0); } function invokePreparedComponent(op, hasBlock, bindableBlocks, bindableAtNames, populateLayout = null) { op(Op.BeginComponentTransaction, $s0), op(Op.PushDynamicScope), op(Op.CreateComponent, 0 | hasBlock, $s0), // 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(Op.RegisterComponentDestructor, $s0), op(Op.GetComponentSelf, $s0), op(Op.VirtualRootScope, $s0), op(Op.SetVariable, 0), op(Op.SetupForEval, $s0), bindableAtNames && op(Op.SetNamedVariables, $s0), bindableBlocks && op(Op.SetBlocks, $s0), op(Op.Pop, 1), op(Op.InvokeComponentLayout, $s0), op(Op.DidRenderLayout, $s0), op(MachineOp.PopFrame), op(Op.PopScope), op(Op.PopDynamicScope), op(Op.CommitComponentTransaction); } /** * Append content to the DOM. This standard function triages content and does the * right thing based upon whether it's a string, safe string, component, fragment * or node. * * @param trusting whether to interpolate a string as raw HTML (corresponds to * triple curlies) */ function StdAppend(op, trusting, nonDynamicAppend) { SwitchCases(op, () => op(Op.ContentType), when => { when(ContentType.String, () => { trusting ? (op(Op.AssertSame), op(Op.AppendHTML)) : op(Op.AppendText); }), "number" == typeof nonDynamicAppend ? (when(ContentType.Component, () => { op(Op.ResolveCurriedComponent), op(Op.PushDynamicComponentInstance), function (op) { op(Op.Fetch, $s0), op(Op.Dup, $sp, 1), op(Op.Load, $s0), op(MachineOp.PushFrame), op(Op.PushEmptyArgs), op(Op.PrepareArgs, $s0), invokePreparedComponent(op, !1, !1, !0, () => { op(Op.GetComponentLayout, $s0), op(Op.PopulateLayout, $s0); }), op(Op.Load, $s0); }(op); }), when(ContentType.Helper, () => { CallDynamic(op, null, null, () => { op(MachineOp.InvokeStatic, nonDynamicAppend); }); })) : ( // when non-dynamic, we can no longer call the value (potentially because we've already called it) // this prevents infinite loops. We instead coerce the value, whatever it is, into the DOM. when(ContentType.Component, () => { op(Op.AppendText); }), when(ContentType.Helper, () => { op(Op.AppendText); })), when(ContentType.SafeString, () => { op(Op.AssertSame), op(Op.AppendSafeHTML); }), when(ContentType.Fragment, () => { op(Op.AssertSame), op(Op.AppendDocumentFragment); }), when(ContentType.Node, () => { op(Op.AssertSame), op(Op.AppendNode); }); }); } function compileStd(context) { let mainHandle = build(context, op => function (op) { op(Op.Main, $s0), invokePreparedComponent(op, !1, !1, !0); }(op)), trustingGuardedNonDynamicAppend = build(context, op => StdAppend(op, !0, null)), cautiousGuardedNonDynamicAppend = build(context, op => StdAppend(op, !1, null)), trustingGuardedDynamicAppend = build(context, op => StdAppend(op, !0, trustingGuardedNonDynamicAppend)), cautiousGuardedDynamicAppend = build(context, op => StdAppend(op, !1, cautiousGuardedNonDynamicAppend)); return new StdLib(mainHandle, trustingGuardedDynamicAppend, cautiousGuardedDynamicAppend, trustingGuardedNonDynamicAppend, cautiousGuardedNonDynamicAppend); } EXPRESSIONS.add(opcodes.Concat, (op, [, parts]) => { for (let part of parts) expr(op, part); op(Op.Concat, parts.length); }), EXPRESSIONS.add(opcodes.Call, (op, [, expression, positional, named]) => { isGetFreeHelper(expression) ? op(HighLevelResolutionOpcodes.Helper, 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(Op.GetVariable, sym), withPath(op, path); }), EXPRESSIONS.add(opcodes.GetLexicalSymbol, (op, [, sym, path]) => { op(HighLevelResolutionOpcodes.TemplateLocal, sym, handle => { op(Op.ConstantReference, handle), withPath(op, path); }); }), EXPRESSIONS.add(opcodes.GetStrictKeyword, (op, expr) => { op(HighLevelResolutionOpcodes.Local, expr[1], _name => { op(HighLevelResolutionOpcodes.Helper, expr, handle => { Call(op, handle, null, null); }); }); }), EXPRESSIONS.add(opcodes.GetFreeAsHelperHead, (op, expr) => { op(HighLevelResolutionOpcodes.Local, expr[1], _name => { op(HighLevelResolutionOpcodes.Helper, 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(Op.HasBlock); }), EXPRESSIONS.add(opcodes.HasBlockParams, (op, [, block]) => { expr(op, block), op(Op.SpreadBlock), op(Op.CompileBlock), op(Op.HasBlockParams); }), EXPRESSIONS.add(opcodes.IfInline, (op, [, condition, truthy, falsy]) => { // Push in reverse order expr(op, falsy), expr(op, truthy), expr(op, condition), op(Op.IfInline); }), EXPRESSIONS.add(opcodes.Not, (op, [, value]) => { expr(op, value), op(Op.Not); }), EXPRESSIONS.add(opcodes.GetDynamicVar, (op, [, expression]) => { expr(op, expression), op(Op.GetDynamicVar); }), EXPRESSIONS.add(opcodes.Log, (op, [, positional]) => { op(MachineOp.PushFrame), SimpleArgs(op, positional, null, !1), op(Op.Log), op(MachineOp.PopFrame), op(Op.Fetch, $v0); }); const STDLIB_META = { evalSymbols: null, upvars: null, moduleName: "stdlib", // TODO: ?? scopeValues: null, isStrictMode: !0, owner: null, size: 0 }; function build(program, builder) { let { constants: constants, heap: heap, resolver: resolver } = program, encoder = new EncoderImpl(heap, STDLIB_META); builder(function (...op) { encodeOp(encoder, constants, resolver, STDLIB_META, op); }); let result = encoder.commit(0); if ("number" != typeof result) // This shouldn't be possible throw new Error("Unexpected errors compiling std"); return result; } class CompileTimeCompilationContextImpl { constants; heap; stdlib; constructor({ constants: constants, heap: heap }, resolver, createOp) { this.resolver = resolver, this.createOp = createOp, this.constants = constants, this.heap = heap, this.stdlib = compileStd(this); } } function programCompilationContext(artifacts, resolver, createOp) { return new CompileTimeCompilationContextImpl(artifacts, resolver, createOp); } function templateCompilationContext(program, meta) { return { program: program, encoder: new EncoderImpl(program.heap, meta, program.stdlib), meta: meta }; } 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(Op.Comment, sexp[1])), STATEMENTS.add(opcodes.CloseElement, op => op(Op.CloseElement)), STATEMENTS.add(opcodes.FlushElement, op => op(Op.FlushElement)), STATEMENTS.add(opcodes.Modifier, (op, [, expression, positional, named]) => { isGetFreeModifier(expression) ? op(HighLevelResolutionOpcodes.Modifier, expression, handle => { op(MachineOp.PushFrame), SimpleArgs(op, positional, named, !1), op(Op.Modifier, handle), op(MachineOp.PopFrame); }) : (expr(op, expression), op(MachineOp.PushFrame), SimpleArgs(op, positional, named, !1), op(Op.Dup, $fp, 1), op(Op.DynamicModifier), op(MachineOp.PopFrame)); }), STATEMENTS.add(opcodes.StaticAttr, (op, [, name, value, namespace]) => { op(Op.StaticAttr, inflateAttrName(name), value, namespace ?? null); }), STATEMENTS.add(opcodes.StaticComponentAttr, (op, [, name, value, namespace]) => { op(Op.StaticComponentAttr, inflateAttrName(name), value, namespace ?? null); }), STATEMENTS.add(opcodes.DynamicAttr, (op, [, name, value, namespace]) => { expr(op, value), op(Op.DynamicAttr, inflateAttrName(name), !1, namespace ?? null); }), STATEMENTS.add(opcodes.TrustingDynamicAttr, (op, [, name, value, namespace]) => { expr(op, value), op(Op.DynamicAttr, inflateAttrName(name), !0, namespace ?? null); }), STATEMENTS.add(opcodes.ComponentAttr, (op, [, name, value, namespace]) => { expr(op, value), op(Op.ComponentAttr, inflateAttrName(name), !1, namespace ?? null); }), STATEMENTS.add(opcodes.TrustingComponentAttr, (op, [, name, value, namespace]) => { expr(op, value), op(Op.ComponentAttr, inflateAttrName(name), !0, namespace ?? null); }), STATEMENTS.add(opcodes.OpenElement, (op, [, tag]) => { op(Op.OpenElement, inflateTagName(tag)); }), STATEMENTS.add(opcodes.OpenElementWithSplat, (op, [, tag]) => { op(Op.PutComponentOperations), op(Op.OpenElement, inflateTagName(tag)); }), STATEMENTS.add(opcodes.Component, (op, [, expr, elementBlock, named, blocks]) => { isGetFreeComp