UNPKG

ember-material-icons

Version:

Google Material icons for your ember-cli app

468 lines (386 loc) 16 kB
import Upsert, { Insertion, CautiousInsertion, TrustingInsertion, isSafeString, isNode, isString, cautiousInsert, trustingInsert } from '../../upsert'; import { isComponentDefinition } from '../../component/interfaces'; import { DOMTreeConstruction } from '../../dom/helper'; import { OpcodeJSON, UpdatingOpcode } from '../../opcodes'; import { CompiledExpression, CompiledArgs } from '../expressions'; import { VM, UpdatingVM } from '../../vm'; import { TryOpcode, VMState } from '../../vm/update'; import { Reference, ReferenceCache, UpdatableTag, isModified, isConst, map } from '@glimmer/reference'; import { FIXME, Option, Opaque, LinkedList, expect } from '@glimmer/util'; import { Cursor, clear } from '../../bounds'; import { Fragment } from '../../builder'; import OpcodeBuilderDSL from './builder'; import { ConditionalReference } from '../../references'; import { Environment } from '../../environment'; import { UpdatableBlockTracker } from '../../builder'; import { SymbolTable } from '@glimmer/interfaces'; import { APPEND_OPCODES, OpcodeName as Op } from '../../opcodes'; APPEND_OPCODES.add(Op.DynamicContent, (vm, { op1: append }) => { let opcode = vm.constants.getOther(append) as AppendDynamicOpcode<Insertion>; opcode.evaluate(vm); }); function isEmpty(value: Opaque): boolean { return value === null || value === undefined || typeof value['toString'] !== 'function'; } export function normalizeTextValue(value: Opaque): string { if (isEmpty(value)) { return ''; } return String(value); } function normalizeTrustedValue(value: Opaque): TrustingInsertion { if (isEmpty(value)) { return ''; } if (isString(value)) { return value; } if (isSafeString(value)) { return value.toHTML(); } if (isNode(value)) { return value; } return String(value); } function normalizeValue(value: Opaque): CautiousInsertion { if (isEmpty(value)) { return ''; } if (isString(value)) { return value; } if (isSafeString(value) || isNode(value)) { return value; } return String(value); } export type AppendDynamicOpcodeConstructor = typeof OptimizedCautiousAppendOpcode | typeof OptimizedTrustingAppendOpcode; export abstract class AppendDynamicOpcode<T extends Insertion> { protected abstract normalize(reference: Reference<Opaque>): Reference<T>; protected abstract insert(dom: DOMTreeConstruction, cursor: Cursor, value: T): Upsert; protected abstract updateWith(vm: VM, reference: Reference<Opaque>, cache: ReferenceCache<T>, bounds: Fragment, upsert: Upsert): UpdatingOpcode; evaluate(vm: VM) { let reference = vm.frame.getOperand(); let normalized = this.normalize(reference); let value, cache; if (isConst(reference)) { value = normalized.value(); } else { cache = new ReferenceCache(normalized); value = cache.peek(); } let stack = vm.stack(); let upsert = this.insert(vm.env.getAppendOperations(), stack, value); let bounds = new Fragment(upsert.bounds); stack.newBounds(bounds); if (cache /* i.e. !isConst(reference) */) { vm.updateWith(this.updateWith(vm, reference, cache, bounds, upsert)); } } } export abstract class GuardedAppendOpcode<T extends Insertion> extends AppendDynamicOpcode<T> { protected abstract AppendOpcode: typeof OptimizedCautiousAppendOpcode | typeof OptimizedTrustingAppendOpcode; private start = -1; private end = -1; constructor(private expression: CompiledExpression<any>, private symbolTable: SymbolTable) { super(); } evaluate(vm: VM) { if (this.start === -1) { vm.evaluateOperand(this.expression); let value = vm.frame.getOperand().value(); if(isComponentDefinition(value)) { this.deopt(vm.env); vm.pushEvalFrame(this.start, this.end); } else { super.evaluate(vm); } } else { vm.pushEvalFrame(this.start, this.end); } } public deopt(env: Environment): number { // Public because it's used in the lazy deopt // At compile time, we determined that this append callsite might refer // to a local variable/property lookup that resolves to a component // definition at runtime. // // We could have eagerly compiled this callsite into something like this: // // {{#if (is-component-definition foo)}} // {{component foo}} // {{else}} // {{foo}} // {{/if}} // // However, in practice, there might be a large amout of these callsites // and most of them would resolve to a simple value lookup. Therefore, we // tried to be optimistic and assumed that the callsite will resolve to // appending a simple value. // // However, we have reached here because at runtime, the guard conditional // have detected that this callsite is indeed referring to a component // definition object. Since this is likely going to be true for other // instances of the same callsite, it is now appropiate to deopt into the // expanded version that handles both cases. The compilation would look // like this: // // PutValue(expression) // Test(is-component-definition) // Enter(BEGIN, END) // BEGIN: Noop // JumpUnless(VALUE) // PutDynamicComponentDefinitionOpcode // OpenComponent // CloseComponent // Jump(END) // VALUE: Noop // OptimizedAppend // END: Noop // Exit // // Keep in mind that even if we *don't* reach here at initial render time, // it is still possible (although quite rare) that the simple value we // encounter during initial render could later change into a component // definition object at update time. That is handled by the "lazy deopt" // code on the update side (scroll down for the next big block of comment). let dsl = new OpcodeBuilderDSL(this.symbolTable, env); dsl.putValue(this.expression); dsl.test(IsComponentDefinitionReference.create); dsl.labelled(null, (dsl, _BEGIN, END) => { dsl.jumpUnless('VALUE'); dsl.putDynamicComponentDefinition(); dsl.openComponent(CompiledArgs.empty()); dsl.closeComponent(); dsl.jump(END); dsl.label('VALUE'); dsl.dynamicContent(new this.AppendOpcode()); }); this.start = dsl.start; this.end = dsl.end; // From this point on, we have essentially replaced ourselves with a new set // of opcodes. Since we will always be executing the new/deopted code, it's // a good idea (as a pattern) to null out any unneeded fields here to avoid // holding on to unneeded/stale objects: // QUESTION: Shouldn't this whole object be GCed? If not, why not? this.expression = null as FIXME<any, 'QUESTION'>; return dsl.start; } } class IsComponentDefinitionReference extends ConditionalReference { static create(inner: Reference<Opaque>): IsComponentDefinitionReference { return new IsComponentDefinitionReference(inner); } toBool(value: Opaque): boolean { return isComponentDefinition(value); } } abstract class UpdateOpcode<T extends Insertion> extends UpdatingOpcode { constructor( protected cache: ReferenceCache<T>, protected bounds: Fragment, protected upsert: Upsert ) { super(); this.tag = cache.tag; } protected abstract insert(dom: DOMTreeConstruction, cursor: Cursor, value: T): Upsert; evaluate(vm: UpdatingVM) { let value = this.cache.revalidate(); if (isModified(value)) { let { bounds, upsert } = this; let { dom } = vm; if(!this.upsert.update(dom, value)) { let cursor = new Cursor(bounds.parentElement(), clear(bounds)); upsert = this.upsert = this.insert(vm.env.getAppendOperations(), cursor, value as T); } bounds.update(upsert.bounds); } } toJSON(): OpcodeJSON { let { _guid: guid, type, cache } = this; return { guid, type, details: { lastValue: JSON.stringify(cache.peek()) } }; } } abstract class GuardedUpdateOpcode<T extends Insertion> extends UpdateOpcode<T> { private _tag: UpdatableTag; private deopted: Option<TryOpcode> = null; constructor( private reference: Reference<Opaque>, cache: ReferenceCache<T>, bounds: Fragment, upsert: Upsert, private appendOpcode: GuardedAppendOpcode<T>, private state: VMState ) { super(cache, bounds, upsert); this.tag = this._tag = new UpdatableTag(this.tag); } evaluate(vm: UpdatingVM) { if (this.deopted) { vm.evaluateOpcode(this.deopted); } else { if (isComponentDefinition(this.reference.value())) { this.lazyDeopt(vm); } else { super.evaluate(vm); } } } private lazyDeopt(vm: UpdatingVM) { // Durign initial render, we know that the reference does not contain a // component definition, so we optimistically assumed that this append // is just a normal append. However, at update time, we discovered that // the reference has switched into containing a component definition, so // we need to do a "lazy deopt", simulating what would have happened if // we had decided to perform the deopt in the first place during initial // render. // // More concretely, we would have expanded the curly into a if/else, and // based on whether the value is a component definition or not, we would // have entered either the dynamic component branch or the simple value // branch. // // Since we rendered a simple value during initial render (and all the // updates up until this point), we need to pretend that the result is // produced by the "VALUE" branch of the deopted append opcode: // // Try(BEGIN, END) // Assert(IsComponentDefinition, expected=false) // OptimizedUpdate // // In this case, because the reference has switched from being a simple // value into a component definition, what would have happened is that // the assert would throw, causing the Try opcode to teardown the bounds // and rerun the original append opcode. // // Since the Try opcode would have nuked the updating opcodes anyway, we // wouldn't have to worry about simulating those. All we have to do is to // execute the Try opcode and immediately throw. let { bounds, appendOpcode, state } = this; let env = vm.env; let deoptStart = appendOpcode.deopt(env); let enter = expect(env.program.opcode(deoptStart + 8), 'hardcoded deopt location'); let { op1: start, op2: end } = enter; let tracker = new UpdatableBlockTracker(bounds.parentElement()); tracker.newBounds(this.bounds); let children = new LinkedList<UpdatingOpcode>(); state.frame.condition = IsComponentDefinitionReference.create(expect(state.frame['operand'], 'operand should be populated')); let deopted = this.deopted = new TryOpcode(start, end, state, tracker, children); this._tag.update(deopted.tag); vm.evaluateOpcode(deopted); vm.throw(); // From this point on, we have essentially replaced ourselve with a new // opcode. Since we will always be executing the new/deopted code, it's a // good idea (as a pattern) to null out any unneeded fields here to avoid // holding on to unneeded/stale objects: // QUESTION: Shouldn't this whole object be GCed? If not, why not? this._tag = null as FIXME<any, 'QUESTION'>; this.reference = null as FIXME<any, 'QUESTION'>; this.cache = null as FIXME<any, 'QUESTION'>; this.bounds = null as FIXME<any, 'QUESTION'>; this.upsert = null as FIXME<any, 'QUESTION'>; this.appendOpcode = null as FIXME<any, 'QUESTION'>; this.state = null as FIXME<any, 'QUESTION'>; } toJSON(): OpcodeJSON { let { _guid: guid, type, deopted } = this; if (deopted) { return { guid, type, deopted: true, children: [deopted.toJSON()] }; } else { return super.toJSON(); } } } export class OptimizedCautiousAppendOpcode extends AppendDynamicOpcode<CautiousInsertion> { type = 'optimized-cautious-append'; protected normalize(reference: Reference<Opaque>): Reference<CautiousInsertion> { return map(reference, normalizeValue); } protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: CautiousInsertion): Upsert { return cautiousInsert(dom, cursor, value); } protected updateWith(_vm: VM, _reference: Reference<Opaque>, cache: ReferenceCache<CautiousInsertion>, bounds: Fragment, upsert: Upsert): UpdatingOpcode { return new OptimizedCautiousUpdateOpcode(cache, bounds, upsert); } } class OptimizedCautiousUpdateOpcode extends UpdateOpcode<CautiousInsertion> { type = 'optimized-cautious-update'; protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: CautiousInsertion): Upsert { return cautiousInsert(dom, cursor, value); } } export class GuardedCautiousAppendOpcode extends GuardedAppendOpcode<CautiousInsertion> { type = 'guarded-cautious-append'; protected AppendOpcode = OptimizedCautiousAppendOpcode; protected normalize(reference: Reference<Opaque>): Reference<CautiousInsertion> { return map(reference, normalizeValue); } protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: CautiousInsertion): Upsert { return cautiousInsert(dom, cursor, value); } protected updateWith(vm: VM, reference: Reference<Opaque>, cache: ReferenceCache<CautiousInsertion>, bounds: Fragment, upsert: Upsert): UpdatingOpcode { return new GuardedCautiousUpdateOpcode(reference, cache, bounds, upsert, this, vm.capture()); } } class GuardedCautiousUpdateOpcode extends GuardedUpdateOpcode<CautiousInsertion> { type = 'guarded-cautious-update'; protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: CautiousInsertion): Upsert { return cautiousInsert(dom, cursor, value); } } export class OptimizedTrustingAppendOpcode extends AppendDynamicOpcode<TrustingInsertion> { type = 'optimized-trusting-append'; protected normalize(reference: Reference<Opaque>): Reference<TrustingInsertion> { return map(reference, normalizeTrustedValue); } protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: TrustingInsertion): Upsert { return trustingInsert(dom, cursor, value); } protected updateWith(_vm: VM, _reference: Reference<Opaque>, cache: ReferenceCache<TrustingInsertion>, bounds: Fragment, upsert: Upsert): UpdatingOpcode { return new OptimizedTrustingUpdateOpcode(cache, bounds, upsert); } } class OptimizedTrustingUpdateOpcode extends UpdateOpcode<TrustingInsertion> { type = 'optimized-trusting-update'; protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: TrustingInsertion): Upsert { return trustingInsert(dom, cursor, value); } } export class GuardedTrustingAppendOpcode extends GuardedAppendOpcode<TrustingInsertion> { type = 'guarded-trusting-append'; protected AppendOpcode = OptimizedTrustingAppendOpcode; protected normalize(reference: Reference<Opaque>): Reference<TrustingInsertion> { return map(reference, normalizeTrustedValue); } protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: TrustingInsertion): Upsert { return trustingInsert(dom, cursor, value); } protected updateWith(vm: VM, reference: Reference<Opaque>, cache: ReferenceCache<TrustingInsertion>, bounds: Fragment, upsert: Upsert): UpdatingOpcode { return new GuardedTrustingUpdateOpcode(reference, cache, bounds, upsert, this, vm.capture()); } } class GuardedTrustingUpdateOpcode extends GuardedUpdateOpcode<TrustingInsertion> { type = 'trusting-update'; protected insert(dom: DOMTreeConstruction, cursor: Cursor, value: TrustingInsertion): Upsert { return trustingInsert(dom, cursor, value); } }