UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,597 lines (1,525 loc) 64.7 kB
import { EMPTY_STRING_ARRAY, isSmallInt, encodeImmediate, unwrap, reverse, assert, Stack, isPresentArray, encodeHandle, expect, assign, dict, enumerate, EMPTY_ARRAY, debugToString } from '@glimmer/util'; import '@glimmer/debug'; import { Op, MachineOp, $v0, $fp, $sp, InternalComponentCapabilities, $s0, ContentType, TYPE_SIZE, isMachineOp, MACHINE_MASK, ARG_SHIFT, $s1 } from '@glimmer/vm'; import { DEBUG } from '@glimmer/env'; import { InstructionEncoderImpl } from '@glimmer/encoder'; import { SexpOpcodes } from '@glimmer/wire-format'; import { hasCapability } from '@glimmer/manager'; let debugCompiler; function isGetLikeTuple(opcode) { return Array.isArray(opcode) && opcode.length === 2; } function makeResolutionTypeVerifier(typeToVerify) { return opcode => { if (!isGetLikeTuple(opcode)) return false; let type = opcode[0]; return type === SexpOpcodes.GetStrictKeyword || type === SexpOpcodes.GetLexicalSymbol || type === typeToVerify; }; } const isGetFreeComponent = makeResolutionTypeVerifier(SexpOpcodes.GetFreeAsComponentHead); const isGetFreeModifier = makeResolutionTypeVerifier(SexpOpcodes.GetFreeAsModifierHead); const isGetFreeHelper = makeResolutionTypeVerifier(SexpOpcodes.GetFreeAsHelperHead); const isGetFreeComponentOrHelper = makeResolutionTypeVerifier(SexpOpcodes.GetFreeAsComponentOrHelperHead); function assertResolverInvariants(meta) { if (DEBUG) { 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 resolveComponent(resolver, constants, meta, [, expr, then]) { assert(isGetFreeComponent(expr), 'Attempted to resolve a component with incorrect opcode'); let type = expr[0]; if (DEBUG && expr[0] === SexpOpcodes.GetStrictKeyword) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw 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 === SexpOpcodes.GetLexicalSymbol) { let { scopeValues, owner } = meta; let 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, owner } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let definition = resolver.lookupComponent(name, owner); if (DEBUG && (typeof definition !== 'object' || definition === null)) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw 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) */ function resolveHelper(resolver, constants, meta, [, expr, then]) { assert(isGetFreeHelper(expr), 'Attempted to resolve a helper with incorrect opcode'); let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { let { scopeValues } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[expr[1]]; then(constants.helper(definition)); } else if (type === SexpOpcodes.GetStrictKeyword) { then(lookupBuiltInHelper(expr, resolver, meta, constants, 'helper')); } else { let { upvars, owner } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let helper = resolver.lookupHelper(name, owner); if (DEBUG && helper === null) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw new Error(`Attempted to resolve \`${name}\`, which was expected to be a helper, but nothing was found.`); } then(constants.helper(helper, name)); } } /** * <div {{modifier}}/> * <div {{modifier arg}}/> * <Foo {{modifier}}/> */ function resolveModifier(resolver, constants, meta, [, expr, then]) { assert(isGetFreeModifier(expr), 'Attempted to resolve a modifier with incorrect opcode'); let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { let { scopeValues } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[expr[1]]; then(constants.modifier(definition)); } else if (type === SexpOpcodes.GetStrictKeyword) { let { upvars } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let modifier = resolver.lookupBuiltInModifier(name); if (DEBUG && modifier === null) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw 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, owner } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let modifier = resolver.lookupModifier(name, owner); if (DEBUG && modifier === null) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw 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}} */ function resolveComponentOrHelper(resolver, constants, meta, [, expr, { ifComponent, ifHelper }]) { assert(isGetFreeComponentOrHelper(expr), 'Attempted to resolve a component or helper with incorrect opcode'); let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { let { scopeValues, owner } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[expr[1]]; let component = constants.component(definition, expect(owner, 'BUG: expected owner when resolving component definition'), true); if (component !== null) { ifComponent(component); return; } let helper = constants.helper(definition, null, true); if (DEBUG && helper === null) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw 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(definition)}`); } ifHelper(expect(helper, 'BUG: helper must exist')); } else if (type === SexpOpcodes.GetStrictKeyword) { ifHelper(lookupBuiltInHelper(expr, resolver, meta, constants, 'component or helper')); } else { let { upvars, owner } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let definition = resolver.lookupComponent(name, owner); if (definition !== null) { ifComponent(constants.resolvedComponent(definition, name)); } else { let helper = resolver.lookupHelper(name, owner); if (DEBUG && helper === null) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw 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}} */ function resolveOptionalComponentOrHelper(resolver, constants, meta, [, expr, { ifComponent, ifHelper, ifValue }]) { assert(isGetFreeComponentOrHelper(expr), 'Attempted to resolve an optional component or helper with incorrect opcode'); let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { let { scopeValues, owner } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[expr[1]]; if (typeof definition !== 'function' && (typeof definition !== 'object' || definition === null)) { // The value is not an object, so it can't be a component or helper. ifValue(constants.value(definition)); return; } let component = constants.component(definition, expect(owner, 'BUG: expected owner when resolving component definition'), true); if (component !== null) { ifComponent(component); return; } let helper = constants.helper(definition, null, true); if (helper !== null) { ifHelper(helper); return; } ifValue(constants.value(definition)); } else if (type === SexpOpcodes.GetStrictKeyword) { ifHelper(lookupBuiltInHelper(expr, resolver, meta, constants, 'value')); } else { let { upvars, owner } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let definition = resolver.lookupComponent(name, owner); if (definition !== null) { ifComponent(constants.resolvedComponent(definition, name)); return; } let helper = resolver.lookupHelper(name, owner); if (helper !== null) { ifHelper(constants.helper(helper, name)); } } } function lookupBuiltInHelper(expr, resolver, meta, constants, type) { let { upvars } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let helper = resolver.lookupBuiltInHelper(name); if (DEBUG && helper === null) { assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); // Keyword helper did not exist, which means that we're attempting to use a // value of some kind that is not in scope throw 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 }; const HighLevelBuilderOpcodes = { Label: 1000, StartLabels: 1001, StopLabels: 1002, Start: 1000, End: 1002 }; const 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 }; } function debugSymbolsOperand() { return { type: HighLevelOperands.DebugSymbols, value: undefined }; } function isStrictMode() { return { type: HighLevelOperands.IsStrictMode, value: undefined }; } function blockOperand(value) { return { type: HighLevelOperands.Block, value }; } function stdlibOperand(value) { return { type: HighLevelOperands.StdLib, value }; } function nonSmallIntOperand(value) { assert(!isSmallInt(value), 'Attempted to make a operand for an int that was not a small int, you should encode this as an immediate'); return { type: HighLevelOperands.NonSmallInt, value }; } function symbolTableOperand(value) { return { type: HighLevelOperands.SymbolTable, value }; } function layoutOperand(value) { return { type: HighLevelOperands.Layout, value }; } class Labels { labels = dict(); targets = []; label(name, index) { this.labels[name] = index; } target(at, target) { this.targets.push({ at, target }); } patch(heap) { let { targets, labels } = this; for (const { at, target } of targets) { let address = labels[target] - at; assert(heap.getbyaddr(at) === -1, 'Expected heap to contain a placeholder, but it did not'); heap.setbyaddr(at, address); } } } function encodeOp(encoder, constants, resolver, meta, op) { if (isBuilderOpcode(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 resolveComponent(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Modifier: return resolveModifier(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Helper: return resolveHelper(resolver, constants, meta, op); case HighLevelResolutionOpcodes.ComponentOrHelper: return resolveComponentOrHelper(resolver, constants, meta, op); case HighLevelResolutionOpcodes.OptionalComponentOrHelper: return resolveOptionalComponentOrHelper(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Local: { let freeVar = op[1]; let name = expect(meta.upvars, 'BUG: attempted to resolve value but no upvars found')[freeVar]; let andThen = op[2]; andThen(name, meta.moduleName); break; } case HighLevelResolutionOpcodes.TemplateLocal: { let [, valueIndex, then] = op; let 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 Stack(); 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; this.heap.pushMachine(MachineOp.Return); this.heap.finishMalloc(handle, size); if (isPresentArray(this.errors)) { return { errors: this.errors, handle }; } else { return handle; } } push(constants, type, ...args) { let { heap } = this; if (DEBUG && type > TYPE_SIZE) { throw new Error(`Opcode type over 8-bits. Got ${type}.`); } let machine = isMachineOp(type) ? MACHINE_MASK : 0; let first = type | machine | 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 (typeof operand === 'number') { return operand; } if (typeof operand === 'object' && operand !== null) { if (Array.isArray(operand)) { return encodeHandle(constants.array(operand)); } else { switch (operand.type) { case HighLevelOperands.Label: this.currentLabels.target(this.heap.offset, operand.value); return -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(compilableBlock(operand.value, this.meta))); 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); } } } 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() { let label = expect(this.labelsStack.pop(), 'unbalanced push and pop labels'); label.patch(this.heap); } } function isBuilderOpcode(op) { return op < HighLevelBuilderOpcodes.Start; } 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) { if (!this.blocks) return null; return this.blocks[name] || null; } has(name) { let { blocks } = this; return blocks !== null && name in blocks; } with(name, block) { let { blocks } = this; if (blocks) { return new NamedBlocksImpl(assign({}, blocks, { [name]: block })); } else { return new NamedBlocksImpl({ [name]: block }); } } get hasAny() { return this.blocks !== null; } } const EMPTY_BLOCKS = new NamedBlocksImpl(null); function namedBlocks(blocks) { if (blocks === null) { return EMPTY_BLOCKS; } let out = dict(); let [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; if (typeof p === 'number') { p = isSmallInt(p) ? encodeImmediate(p) : nonSmallIntOperand(p); } 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, false); 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, false); op(Op.Dup, $fp, 1); op(Op.DynamicHelper); if (append) { op(Op.Fetch, $v0); append(); op(MachineOp.PopFrame); op(Op.Pop, 1); } else { 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 DynamicScope(op, names, block) { op(Op.PushDynamicScope); op(Op.BindDynamicScope, names); block(); op(Op.PopDynamicScope); } function Curry(op, type, definition, positional, named) { op(MachineOp.PushFrame); SimpleArgs(op, positional, named, false); 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]; let index = unwrap(this.names[name]); let func = this.funcs[index]; assert(!!func, `expected an implementation for ${sexp[0]}`); func(op, sexp); } } const EXPRESSIONS = new Compilers(); EXPRESSIONS.add(SexpOpcodes.Concat, (op, [, parts]) => { for (let part of parts) { expr(op, part); } op(Op.Concat, parts.length); }); EXPRESSIONS.add(SexpOpcodes.Call, (op, [, expression, positional, named]) => { if (isGetFreeHelper(expression)) { op(HighLevelResolutionOpcodes.Helper, expression, handle => { Call(op, handle, positional, named); }); } else { expr(op, expression); CallDynamic(op, positional, named); } }); EXPRESSIONS.add(SexpOpcodes.Curry, (op, [, expr, type, positional, named]) => { Curry(op, type, expr, positional, named); }); EXPRESSIONS.add(SexpOpcodes.GetSymbol, (op, [, sym, path]) => { op(Op.GetVariable, sym); withPath(op, path); }); EXPRESSIONS.add(SexpOpcodes.GetLexicalSymbol, (op, [, sym, path]) => { op(HighLevelResolutionOpcodes.TemplateLocal, sym, handle => { op(Op.ConstantReference, handle); withPath(op, path); }); }); EXPRESSIONS.add(SexpOpcodes.GetStrictKeyword, (op, expr) => { op(HighLevelResolutionOpcodes.Local, expr[1], _name => { op(HighLevelResolutionOpcodes.Helper, expr, handle => { Call(op, handle, null, null); }); }); }); EXPRESSIONS.add(SexpOpcodes.GetFreeAsHelperHead, (op, expr) => { op(HighLevelResolutionOpcodes.Local, expr[1], _name => { op(HighLevelResolutionOpcodes.Helper, expr, handle => { Call(op, handle, null, null); }); }); }); function withPath(op, path) { if (path === undefined || path.length === 0) return; for (let i = 0; i < path.length; i++) { op(Op.GetProperty, path[i]); } } EXPRESSIONS.add(SexpOpcodes.Undefined, op => PushPrimitiveReference(op, undefined)); EXPRESSIONS.add(SexpOpcodes.HasBlock, (op, [, block]) => { expr(op, block); op(Op.HasBlock); }); EXPRESSIONS.add(SexpOpcodes.HasBlockParams, (op, [, block]) => { expr(op, block); op(Op.SpreadBlock); op(Op.CompileBlock); op(Op.HasBlockParams); }); EXPRESSIONS.add(SexpOpcodes.IfInline, (op, [, condition, truthy, falsy]) => { // Push in reverse order expr(op, falsy); expr(op, truthy); expr(op, condition); op(Op.IfInline); }); EXPRESSIONS.add(SexpOpcodes.Not, (op, [, value]) => { expr(op, value); op(Op.Not); }); EXPRESSIONS.add(SexpOpcodes.GetDynamicVar, (op, [, expression]) => { expr(op, expression); op(Op.GetDynamicVar); }); EXPRESSIONS.add(SexpOpcodes.Log, (op, [, positional]) => { op(MachineOp.PushFrame); SimpleArgs(op, positional, null, false); op(Op.Log); op(MachineOp.PopFrame); op(Op.Fetch, $v0); }); function expr(op, expression) { if (Array.isArray(expression)) { EXPRESSIONS.compile(op, expression); } else { 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 CompileArgs(op, positional, named, blocks, atNames) { let blockNames = blocks.names; for (const name of blockNames) { PushYieldableBlock(op, blocks.get(name)); } let count = CompilePositional(op, positional); let flags = count << 4; if (atNames) flags |= 0b1000; if (blocks) { flags |= 0b111; } 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); } function SimpleArgs(op, positional, named, atNames) { if (positional === null && named === null) { op(Op.PushEmptyArgs); return; } let count = CompilePositional(op, positional); let flags = count << 4; if (atNames) flags |= 0b1000; 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 (positional === null) 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 } = layout; let [, 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, true); 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) { PushSymbolTable(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]; let calleeCount = parameters.length; let count = Math.min(callerCount, calleeCount); if (count === 0) { InvokeStaticBlock(op, block); return; } op(MachineOp.PushFrame); if (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); if (count) { op(Op.PopScope); } op(MachineOp.PopFrame); } function PushSymbolTable(op, parameters) { if (parameters !== null) { op(Op.PushSymbolTable, symbolTableOperand({ parameters })); } else { PushPrimitive(op, null); } } function PushCompilable(op, _block) { if (_block === null) { PushPrimitive(op, null); } else { op(Op.Constant, blockOperand(_block)); } } function SwitchCases(op, bootstrap, matcher) { // Setup the switch DSL let clauses = []; let count = 0; function when(match, callback) { clauses.push({ match, callback, label: `CLAUSE${count++}` }); } // Call the callback matcher(when); // 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. if (i !== 0) { 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. if (ifFalse !== undefined) { ifFalse(); } }); } const ATTRS_BLOCK = '&attrs'; // {{component}} // <Component> // chokepoint function InvokeComponent(op, component, _elementBlock, positional, named, _blocks) { let { compilable, capabilities, handle } = component; let elementBlock = _elementBlock ? [_elementBlock, []] : null; let blocks = Array.isArray(_blocks) || _blocks === null ? namedBlocks(_blocks) : _blocks; if (compilable) { op(Op.PushComponentDefinition, handle); InvokeStaticComponent(op, { capabilities: capabilities, layout: compilable, elementBlock, positional, named, blocks }); } else { op(Op.PushComponentDefinition, handle); InvokeNonStaticComponent(op, { capabilities: capabilities, elementBlock, positional, named, atNames: true, blocks }); } } function InvokeDynamicComponent(op, definition, _elementBlock, positional, named, _blocks, atNames, curried) { let elementBlock = _elementBlock ? [_elementBlock, []] : null; let blocks = Array.isArray(_blocks) || _blocks === null ? namedBlocks(_blocks) : _blocks; Replayable(op, () => { expr(op, definition); op(Op.Dup, $sp, 0); return 2; }, () => { op(Op.JumpUnless, labelOperand('ELSE')); if (curried) { op(Op.ResolveCurriedComponent); } else { op(Op.ResolveDynamicComponent, isStrictMode()); } op(Op.PushDynamicComponentInstance); InvokeNonStaticComponent(op, { capabilities: true, elementBlock, positional, named, atNames, blocks }); op(HighLevelBuilderOpcodes.Label, 'ELSE'); }); } function InvokeStaticComponent(op, { capabilities, layout, elementBlock, positional, named, blocks }) { let { symbolTable } = layout; let bailOut = symbolTable.hasEval || hasCapability(capabilities, InternalComponentCapabilities.prepareArgs); if (bailOut) { InvokeNonStaticComponent(op, { capabilities, elementBlock, positional, named, atNames: true, blocks, layout }); return; } op(Op.Fetch, $s0); op(Op.Dup, $sp, 1); op(Op.Load, $s0); op(MachineOp.PushFrame); // Setup arguments let { symbols } = symbolTable; // 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 let blockSymbols = []; let argSymbols = []; let argNames = []; // First we push the blocks onto the stack let blockNames = blocks.names; // Starting with the attrs block, if it exists and is referenced in the component if (elementBlock !== null) { let symbol = symbols.indexOf(ATTRS_BLOCK); if (symbol !== -1) { 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}`); if (symbol !== -1) { 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 count = CompilePositional(op, positional); // setup the flags with the count of positionals, and to indicate that atNames // are used let flags = count << 4; flags |= 0b1000; 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 (named !== null) { 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 (named !== null) { // 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]; let val = named[1]; for (let i = 0; i < val.length; i++) { let name = unwrap(names[i]); let symbol = symbols.indexOf(name); if (symbol !== -1) { expr(op, val[i]); argSymbols.push(symbol); argNames.push(name); } } } op(Op.BeginComponentTransaction, $s0); if (hasCapability(capabilities, InternalComponentCapabilities.dynamicScope)) { op(Op.PushDynamicScope); } if (hasCapability(capabilities, InternalComponentCapabilities.createInstance)) { op(Op.CreateComponent, blocks.has('default') | 0, $s0); } op(Op.RegisterComponentDestructor, $s0); if (hasCapability(capabilities, InternalComponentCapabilities.createArgs)) { op(Op.GetComponentSelf, $s0); } else { 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]; if (symbol === -1) { // 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); } else { op(Op.SetVariable, symbol + 1); } } // if any positional params exist, pop them off the stack as well if (positional !== null) { 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); if (hasCapability(capabilities, InternalComponentCapabilities.dynamicScope)) { op(Op.PopDynamicScope); } op(Op.CommitComponentTransaction); op(Op.Load, $s0); } function InvokeNonStaticComponent(op, { capabilities, elementBlock, positional, named, atNames, blocks: namedBlocks, layout }) { let bindableBlocks = !!namedBlocks; let bindableAtNames = capabilities === true || hasCapability(capabilities, InternalComponentCapabilities.prepareArgs) || !!(named && named[0].length !== 0); let blocks = namedBlocks.with('attrs', elementBlock); op(Op.Fetch, $s0); op(Op.Dup, $sp, 1); op(Op.Load, $s0); op(MachineOp.PushFrame); CompileArgs(op, positional, named, blocks, atNames); op(Op.PrepareArgs, $s0); invokePreparedComponent(op, blocks.has('default'), bindableBlocks, bindableAtNames, () => { if (layout) { op(Op.PushSymbolTable, symbolTableOperand(layout.symbolTable)); op(Op.Constant, layoutOperand(layout)); op(Op.CompileBlock); } else { op(Op.GetComponentLayout, $s0); } op(Op.PopulateLayout, $s0); }); op(Op.Load, $s0); } function WrappedComponent(op, layout, attrsBlockNumber) { op(HighLevelBuilderOpcodes.StartLabels); WithSavedRegister(op, $s1, () => { op(Op.GetComponentTagName, $s0); op(Op.PrimitiveReference); op(Op.Dup, $sp, 0); }); op(Op.JumpUnless, labelOperand('BODY')); op(Op.Fetch, $s1); op(Op.PutComponentOperations); op(Op.OpenDynamicElement); op(Op.DidCreateElement, $s0); YieldBlock(op, attrsBlockNumber, null); op(Op.FlushElement); op(HighLevelBuilderOpcodes.Label, 'BODY'); InvokeStaticBlock(op, [layout.block[0], []]); op(Op.Fetch, $s1); op(Op.JumpUnless, labelOperand('END')); op(Op.CloseElement); op(HighLevelBuilderOpcodes.Label, 'END'); op(Op.Load, $s1); op(HighLevelBuilderOpcodes.StopLabels); } function invokePreparedComponent(op, hasBlock, bindableBlocks, bindableAtNames, populateLayout = null) { op(Op.BeginComponentTransaction, $s0); op(Op.PushDynamicScope); op(Op.CreateComponent, hasBlock | 0, $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. if (populateLayout) { populateLayout(); } op(Op.RegisterComponentDestructor, $s0); op(Op.GetComponentSelf, $s0); op(Op.VirtualRootScope, $s0); op(Op.SetVariable, 0); op(Op.SetupForEval, $s0); if (bindableAtNames) op(Op.SetNamedVariables, $s0); if (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); } function InvokeBareComponent(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, false, false, true, () => { op(Op.GetComponentLayout, $s0); op(Op.PopulateLayout, $s0); }); op(Op.Load, $s0); } function WithSavedRegister(op, register, block) { op(Op.Fetch, register); block(); op(Op.Load, register); } function main(op) { op(Op.Main, $s0); invokePreparedComponent(op, false, false, true); } /** * 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, () => { if (trusting) { op(Op.AssertSame); op(Op.AppendHTML); } else { op(Op.AppendText); } }); if (typeof nonDynamicAppend === 'number') { when(ContentType.Component, () => { op(Op.ResolveCurriedComponent); op(Op.PushDynamicComponentInstance); InvokeBareComponent(op); }); when(ContentType.Helper, () => { CallDynamic(op, null, null, () => { op(MachineOp.InvokeStatic, nonDynamicAppend); }); }); } else { // 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 => main(op)); let trustingGuardedNonDynamicAppend = build(context, op => StdAppend(op, true, null)); let cautiousGuardedNonDynamicAppend = build(context, op => StdAppend(op, false, null)); let trustingGuardedDynamicAppend = build(context, op => StdAppend(op, true, trustingGuardedNonDynamicAppend)); let cautiousGuardedDynamicAppend = build(context, op => StdAppend(op, false, cautiousGuardedNonDynamicAppend)); return new StdLib(mainHandle, trustingGuardedDynamicAppend, cautiousGuardedDynamicAppend, trustingGuardedNonDynamicAppend, cautiousGuardedNonDynamicAppend); } const STDLIB_META = { evalSymbols: null, upvars: null, moduleName: 'stdlib', // TODO: ?? scopeValues: null, isStrictMode: true, owner: null, size: 0 }; function build(program, builder) { let { constants, heap, resolver } = program; let encoder = new EncoderImpl(heap, STDLIB_META); function pushOp(...op) { encodeOp(encoder, constants, resolver, STDLIB_META, op); } builder(pushOp); let result = encoder.commit(0); if (typeof result !== 'number') { // This shouldn't be possible throw new Error(`Unexpected errors compiling std`); } else { return result; } } class CompileTimeCompilationContextImpl { constants; heap; stdlib; constructor({ constants, 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) { let encoder = new EncoderImpl(program.heap, meta, program.stdlib); return { program, encoder, meta }; } const STATEMENTS = new Compilers(); const INFLATE_ATTR_TABLE = ['class', 'id', 'value', 'name', 'type', 'style', 'href']; const INFLATE_TAG_TABLE = ['div', 'span', 'p', 'a']; function inflateTagName(tagName) { return typeof tagName === 'string' ? tagName : INFLATE_TAG_TABLE[tagName]; } function inflateAttrName(attrName) { return typeof attrName === 'string' ? attrName : INFLATE_ATTR_TABLE[attrName]; } STATEMENTS.add(SexpOpcodes.Comment, (op, sexp) => op(Op.Comment, sexp[1])); STATEMENTS.add(SexpOpcodes.CloseElement, op => op(Op.CloseElement)); STATEMENTS.add(SexpOpcodes.FlushElement, op => op(Op.FlushElement)); STATEMENTS.add(SexpOpcodes.Modifier, (op, [, expression, positional, named]) => { if (isGetFreeModifier(expression)) { op(HighLevelResolutionOpcodes.Modifier, expression, handle => { op(MachineOp.PushFrame); Si