ember-source
Version:
A JavaScript framework for creating ambitious web applications
435 lines (422 loc) • 13.6 kB
JavaScript
import { getInternalHelperManager, getInternalModifierManager, getInternalComponentManager, capabilityFlagsFrom, getComponentTemplate, managerHasCapability } from '@glimmer/manager';
import { templateFactory } from '@glimmer/opcode-compiler';
import { constants, enumerate, assert, unwrapTemplate, expect, unwrap } from '@glimmer/util';
import { InternalComponentCapabilities, OPERAND_LEN_MASK, ARG_SHIFT, MACHINE_MASK, TYPE_MASK } from '@glimmer/vm';
import { SexpOpcodes } from '@glimmer/wire-format';
/**
* Default component template, which is a plain yield
*/
const DEFAULT_TEMPLATE_BLOCK = [[[SexpOpcodes.Yield, 1, null]], ['&default'], false, []];
const DEFAULT_TEMPLATE = {
// random uuid
id: '1b32f5c2-7623-43d6-a0ad-9672898920a1',
moduleName: '__default__.hbs',
block: JSON.stringify(DEFAULT_TEMPLATE_BLOCK),
scope: null,
isStrictMode: true
};
const WELL_KNOWN_EMPTY_ARRAY = Object.freeze([]);
const STARTER_CONSTANTS = constants(WELL_KNOWN_EMPTY_ARRAY);
const WELL_KNOWN_EMPTY_ARRAY_POSITION = STARTER_CONSTANTS.indexOf(WELL_KNOWN_EMPTY_ARRAY);
class CompileTimeConstantImpl {
// `0` means NULL
values = STARTER_CONSTANTS.slice();
indexMap = new Map(this.values.map((value, index) => [value, index]));
value(value) {
let indexMap = this.indexMap;
let index = indexMap.get(value);
if (index === undefined) {
index = this.values.push(value) - 1;
indexMap.set(value, index);
}
return index;
}
array(values) {
if (values.length === 0) {
return WELL_KNOWN_EMPTY_ARRAY_POSITION;
}
let handles = new Array(values.length);
for (let i = 0; i < values.length; i++) {
handles[i] = this.value(values[i]);
}
return this.value(handles);
}
toPool() {
return this.values;
}
}
class RuntimeConstantsImpl {
values;
constructor(pool) {
this.values = pool;
}
getValue(handle) {
return this.values[handle];
}
getArray(value) {
let handles = this.getValue(value);
let reified = new Array(handles.length);
for (const [i, n] of enumerate(handles)) {
reified[i] = this.getValue(n);
}
return reified;
}
}
class ConstantsImpl extends CompileTimeConstantImpl {
reifiedArrs = {
[WELL_KNOWN_EMPTY_ARRAY_POSITION]: WELL_KNOWN_EMPTY_ARRAY
};
defaultTemplate = templateFactory(DEFAULT_TEMPLATE)();
// Used for tests and debugging purposes, and to be able to analyze large apps
// This is why it's enabled even in production
helperDefinitionCount = 0;
modifierDefinitionCount = 0;
componentDefinitionCount = 0;
helperDefinitionCache = new WeakMap();
modifierDefinitionCache = new WeakMap();
componentDefinitionCache = new WeakMap();
helper(definitionState,
// TODO: Add a way to expose resolved name for debugging
_resolvedName = null, isOptional) {
let handle = this.helperDefinitionCache.get(definitionState);
if (handle === undefined) {
let managerOrHelper = getInternalHelperManager(definitionState, isOptional);
if (managerOrHelper === null) {
this.helperDefinitionCache.set(definitionState, null);
return null;
}
assert(managerOrHelper, 'BUG: expected manager or helper');
let helper = typeof managerOrHelper === 'function' ? managerOrHelper : managerOrHelper.getHelper(definitionState);
handle = this.value(helper);
this.helperDefinitionCache.set(definitionState, handle);
this.helperDefinitionCount++;
}
return handle;
}
modifier(definitionState, resolvedName = null, isOptional) {
let handle = this.modifierDefinitionCache.get(definitionState);
if (handle === undefined) {
let manager = getInternalModifierManager(definitionState, isOptional);
if (manager === null) {
this.modifierDefinitionCache.set(definitionState, null);
return null;
}
let definition = {
resolvedName,
manager,
state: definitionState
};
handle = this.value(definition);
this.modifierDefinitionCache.set(definitionState, handle);
this.modifierDefinitionCount++;
}
return handle;
}
component(definitionState, owner, isOptional) {
let definition = this.componentDefinitionCache.get(definitionState);
if (definition === undefined) {
let manager = getInternalComponentManager(definitionState, isOptional);
if (manager === null) {
this.componentDefinitionCache.set(definitionState, null);
return null;
}
assert(manager, 'BUG: expected manager');
let capabilities = capabilityFlagsFrom(manager.getCapabilities(definitionState));
let templateFactory = getComponentTemplate(definitionState);
let compilable = null;
let template;
if (!managerHasCapability(manager, capabilities, InternalComponentCapabilities.dynamicLayout)) {
template = templateFactory?.(owner) ?? this.defaultTemplate;
} else {
template = templateFactory?.(owner);
}
if (template !== undefined) {
template = unwrapTemplate(template);
compilable = managerHasCapability(manager, capabilities, InternalComponentCapabilities.wrapped) ? template.asWrappedLayout() : template.asLayout();
}
definition = {
resolvedName: null,
handle: -1,
// replaced momentarily
manager,
capabilities,
state: definitionState,
compilable
};
definition.handle = this.value(definition);
this.componentDefinitionCache.set(definitionState, definition);
this.componentDefinitionCount++;
}
return definition;
}
resolvedComponent(resolvedDefinition, resolvedName) {
let definition = this.componentDefinitionCache.get(resolvedDefinition);
if (definition === undefined) {
let {
manager,
state,
template
} = resolvedDefinition;
let capabilities = capabilityFlagsFrom(manager.getCapabilities(resolvedDefinition));
let compilable = null;
if (!managerHasCapability(manager, capabilities, InternalComponentCapabilities.dynamicLayout)) {
template = template ?? this.defaultTemplate;
}
if (template !== null) {
template = unwrapTemplate(template);
compilable = managerHasCapability(manager, capabilities, InternalComponentCapabilities.wrapped) ? template.asWrappedLayout() : template.asLayout();
}
definition = {
resolvedName,
handle: -1,
// replaced momentarily
manager,
capabilities,
state,
compilable
};
definition.handle = this.value(definition);
this.componentDefinitionCache.set(resolvedDefinition, definition);
this.componentDefinitionCount++;
}
return expect(definition, 'BUG: resolved component definitions cannot be null');
}
getValue(index) {
assert(index >= 0, `cannot get value for handle: ${index}`);
return this.values[index];
}
getArray(index) {
let reifiedArrs = this.reifiedArrs;
let reified = reifiedArrs[index];
if (reified === undefined) {
let names = this.getValue(index);
reified = new Array(names.length);
for (const [i, name] of enumerate(names)) {
reified[i] = this.getValue(name);
}
reifiedArrs[index] = reified;
}
return reified;
}
}
class RuntimeOpImpl {
offset = 0;
constructor(heap) {
this.heap = heap;
}
get size() {
let rawType = this.heap.getbyaddr(this.offset);
return ((rawType & OPERAND_LEN_MASK) >> ARG_SHIFT) + 1;
}
get isMachine() {
let rawType = this.heap.getbyaddr(this.offset);
return rawType & MACHINE_MASK ? 1 : 0;
}
get type() {
return this.heap.getbyaddr(this.offset) & TYPE_MASK;
}
get op1() {
return this.heap.getbyaddr(this.offset + 1);
}
get op2() {
return this.heap.getbyaddr(this.offset + 2);
}
get op3() {
return this.heap.getbyaddr(this.offset + 3);
}
}
var TableSlotState = /*#__PURE__*/function (TableSlotState) {
TableSlotState[TableSlotState["Allocated"] = 0] = "Allocated";
TableSlotState[TableSlotState["Freed"] = 1] = "Freed";
TableSlotState[TableSlotState["Purged"] = 2] = "Purged";
TableSlotState[TableSlotState["Pointer"] = 3] = "Pointer";
return TableSlotState;
}(TableSlotState || {});
const PAGE_SIZE = 0x100000;
class RuntimeHeapImpl {
heap;
table;
constructor(serializedHeap) {
let {
buffer,
table
} = serializedHeap;
this.heap = new Int32Array(buffer);
this.table = table;
}
// It is illegal to close over this address, as compaction
// may move it. However, it is legal to use this address
// multiple times between compactions.
getaddr(handle) {
return unwrap(this.table[handle]);
}
getbyaddr(address) {
return expect(this.heap[address], 'Access memory out of bounds of the heap');
}
sizeof(handle) {
return sizeof(this.table);
}
}
function hydrateHeap(serializedHeap) {
return new RuntimeHeapImpl(serializedHeap);
}
/**
* The Heap is responsible for dynamically allocating
* memory in which we read/write the VM's instructions
* from/to. When we malloc we pass out a VMHandle, which
* is used as an indirect way of accessing the memory during
* execution of the VM. Internally we track the different
* regions of the memory in an int array known as the table.
*
* The table 32-bit aligned and has the following layout:
*
* | ... | hp (u32) | info (u32) | size (u32) |
* | ... | Handle | Scope Size | State | Size |
* | ... | 32bits | 30bits | 2bits | 32bit |
*
* With this information we effectively have the ability to
* control when we want to free memory. That being said you
* can not free during execution as raw address are only
* valid during the execution. This means you cannot close
* over them as you will have a bad memory access exception.
*/
class HeapImpl {
offset = 0;
heap;
handleTable;
handleState;
handle = 0;
constructor() {
this.heap = new Int32Array(PAGE_SIZE);
this.handleTable = [];
this.handleState = [];
}
pushRaw(value) {
this.sizeCheck();
this.heap[this.offset++] = value;
}
pushOp(item) {
this.pushRaw(item);
}
pushMachine(item) {
this.pushRaw(item | MACHINE_MASK);
}
sizeCheck() {
let {
heap
} = this;
if (this.offset === this.heap.length) {
let newHeap = new Int32Array(heap.length + PAGE_SIZE);
newHeap.set(heap, 0);
this.heap = newHeap;
}
}
getbyaddr(address) {
return unwrap(this.heap[address]);
}
setbyaddr(address, value) {
this.heap[address] = value;
}
malloc() {
// push offset, info, size
this.handleTable.push(this.offset);
return this.handleTable.length - 1;
}
finishMalloc(handle) {
}
size() {
return this.offset;
}
// It is illegal to close over this address, as compaction
// may move it. However, it is legal to use this address
// multiple times between compactions.
getaddr(handle) {
return unwrap(this.handleTable[handle]);
}
sizeof(handle) {
return sizeof(this.handleTable);
}
free(handle) {
this.handleState[handle] = TableSlotState.Freed;
}
/**
* The heap uses the [Mark-Compact Algorithm](https://en.wikipedia.org/wiki/Mark-compact_algorithm) to shift
* reachable memory to the bottom of the heap and freeable
* memory to the top of the heap. When we have shifted all
* the reachable memory to the top of the heap, we move the
* offset to the next free position.
*/
compact() {
let compactedSize = 0;
let {
handleTable,
handleState,
heap
} = this;
for (let i = 0; i < length; i++) {
let offset = unwrap(handleTable[i]);
let size = unwrap(handleTable[i + 1]) - unwrap(offset);
let state = handleState[i];
if (state === TableSlotState.Purged) {
continue;
} else if (state === TableSlotState.Freed) {
// transition to "already freed" aka "purged"
// a good improvement would be to reuse
// these slots
handleState[i] = TableSlotState.Purged;
compactedSize += size;
} else if (state === TableSlotState.Allocated) {
for (let j = offset; j <= i + size; j++) {
heap[j - compactedSize] = unwrap(heap[j]);
}
handleTable[i] = offset - compactedSize;
} else if (state === TableSlotState.Pointer) {
handleTable[i] = offset - compactedSize;
}
}
this.offset = this.offset - compactedSize;
}
capture(offset = this.offset) {
// Only called in eager mode
let buffer = slice(this.heap, 0, offset).buffer;
return {
handle: this.handle,
table: this.handleTable,
buffer: buffer
};
}
}
class RuntimeProgramImpl {
_opcode;
constructor(constants, heap) {
this.constants = constants;
this.heap = heap;
this._opcode = new RuntimeOpImpl(this.heap);
}
opcode(offset) {
this._opcode.offset = offset;
return this._opcode;
}
}
function slice(arr, start, end) {
if (arr.slice !== undefined) {
return arr.slice(start, end);
}
let ret = new Int32Array(end);
for (; start < end; start++) {
ret[start] = unwrap(arr[start]);
}
return ret;
}
function sizeof(table, handle) {
{
return -1;
}
}
function artifacts() {
return {
constants: new ConstantsImpl(),
heap: new HeapImpl()
};
}
export { CompileTimeConstantImpl, ConstantsImpl, HeapImpl, RuntimeConstantsImpl, RuntimeHeapImpl, RuntimeOpImpl, RuntimeProgramImpl, artifacts, hydrateHeap };