diff --git a/src/compiler/compile/render_dom/Renderer.js b/src/compiler/compile/render_dom/Renderer.js index 73d1c9878e..2ca22a0e58 100644 --- a/src/compiler/compile/render_dom/Renderer.js +++ b/src/compiler/compile/render_dom/Renderer.js @@ -5,287 +5,295 @@ import flatten_reference from '../utils/flatten_reference.js'; import { reserved_keywords } from '../utils/reserved_keywords.js'; import { renderer_invalidate } from './invalidate.js'; export default class Renderer { + /** @type {import('../Component.js').default} */ + component; // TODO Maybe Renderer shouldn't know about Component? - /** @type {import('../Component.js').default} */ - component; // TODO Maybe Renderer shouldn't know about Component? + /** @type {import('../../interfaces.js').CompileOptions} */ + options; - /** @type {import('../../interfaces.js').CompileOptions} */ - options; + /** @type {ContextMember[]} */ + context = []; - /** @type {ContextMember[]} */ - context = []; + /** @type {ContextMember[]} */ + initial_context = []; - /** @type {ContextMember[]} */ - initial_context = []; + /** @type {Map} */ + context_lookup = new Map(); - /** @type {Map} */ - context_lookup = new Map(); + /** @type {boolean} */ + context_overflow; - /** @type {boolean} */ - context_overflow; + /** @type {Array} */ + blocks = []; - /** @type {Array} */ - blocks = []; + /** @type {Set} */ + readonly = new Set(); - /** @type {Set} */ - readonly = new Set(); + /** @type {Array} */ + meta_bindings = []; // initial values for e.g. window.innerWidth, if there's a meta tag - /** @type {Array} */ - meta_bindings = []; // initial values for e.g. window.innerWidth, if there's a meta tag + /** @type {Map} */ + binding_groups = new Map(); - /** @type {Map} */ - binding_groups = new Map(); + /** @type {import('./Block.js').default} */ + block; - /** @type {import('./Block.js').default} */ - block; + /** @type {import('./wrappers/Fragment.js').default} */ + fragment; - /** @type {import('./wrappers/Fragment.js').default} */ - fragment; + /** @type {import('estree').Identifier} */ + file_var; - /** @type {import('estree').Identifier} */ - file_var; + /** @type {(c: number) => { line: number; column: number }} */ + locate; - /** @type {(c: number) => { line: number; column: number }} */ - locate; + /** + * @param {import('../Component.js').default} component + * @param {import('../../interfaces.js').CompileOptions} options + */ + constructor(component, options) { + this.component = component; + this.options = options; + this.locate = component.locate; // TODO messy + this.file_var = options.dev && this.component.get_unique_name('file'); + component.vars + .filter((v) => !v.hoistable || (v.export_name && !v.module)) + .forEach((v) => this.add_to_context(v.name)); + // ensure store values are included in context + component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`)); + reserved_keywords.forEach((keyword) => { + if (component.var_lookup.has(keyword)) { + this.add_to_context(keyword); + } + }); + if (component.slots.size > 0) { + this.add_to_context('$$scope'); + this.add_to_context('#slots'); + } + // main block + this.block = new Block({ + renderer: this, + name: null, + type: 'component', + key: null, + bindings: new Map(), + dependencies: new Set() + }); + this.block.has_update_method = true; + this.fragment = new FragmentWrapper( + this, + this.block, + component.fragment.children, + null, + true, + null + ); + // TODO messy + this.blocks.forEach((block) => { + if (block instanceof Block) { + block.assign_variable_names(); + } + }); + this.block.assign_variable_names(); + this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`)); + this.context_overflow = this.context.length > 31; + this.context.forEach((member) => { + const { variable } = member; + if (variable) { + member.priority += 2; + if (variable.mutated || variable.reassigned) member.priority += 4; + // these determine whether variable is included in initial context + // array, so must have the highest priority + if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) + member.priority += 16; + if (variable.export_name) member.priority += 32; + if (variable.referenced) member.priority += 64; + } else if (member.is_non_contextual) { + // determine whether variable is included in initial context + // array, so must have the highest priority + member.priority += 8; + } + if (!member.is_contextual) { + member.priority += 1; + } + }); + this.context.sort( + (a, b) => + b.priority - a.priority || + /** @type {number} */ (a.index.value) - /** @type {number} */ (b.index.value) + ); + this.context.forEach((member, i) => (member.index.value = i)); + let i = this.context.length; + while (i--) { + const member = this.context[i]; + if (member.variable) { + if ( + member.variable.referenced || + member.variable.export_name || + (member.variable.is_reactive_dependency && + (member.variable.mutated || member.variable.reassigned)) + ) + break; + } else if (member.is_non_contextual) { + break; + } + } + this.initial_context = this.context.slice(0, i + 1); + } - /** - * @param {import('../Component.js').default} component - * @param {import('../../interfaces.js').CompileOptions} options - */ - constructor(component, options) { - this.component = component; - this.options = options; - this.locate = component.locate; // TODO messy - this.file_var = options.dev && this.component.get_unique_name('file'); - component.vars - .filter((v) => !v.hoistable || (v.export_name && !v.module)) - .forEach((v) => this.add_to_context(v.name)); - // ensure store values are included in context - component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`)); - reserved_keywords.forEach((keyword) => { - if (component.var_lookup.has(keyword)) { - this.add_to_context(keyword); - } - }); - if (component.slots.size > 0) { - this.add_to_context('$$scope'); - this.add_to_context('#slots'); - } - // main block - this.block = new Block({ - renderer: this, - name: null, - type: 'component', - key: null, - bindings: new Map(), - dependencies: new Set() - }); - this.block.has_update_method = true; - this.fragment = new FragmentWrapper(this, this.block, component.fragment.children, null, true, null); - // TODO messy - this.blocks.forEach((block) => { - if (block instanceof Block) { - block.assign_variable_names(); - } - }); - this.block.assign_variable_names(); - this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x `#nodes`)); - this.context_overflow = this.context.length > 31; - this.context.forEach((member) => { - const { variable } = member; - if (variable) { - member.priority += 2; - if (variable.mutated || variable.reassigned) - member.priority += 4; - // these determine whether variable is included in initial context - // array, so must have the highest priority - if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) - member.priority += 16; - if (variable.export_name) - member.priority += 32; - if (variable.referenced) - member.priority += 64; - } - else if (member.is_non_contextual) { - // determine whether variable is included in initial context - // array, so must have the highest priority - member.priority += 8; - } - if (!member.is_contextual) { - member.priority += 1; - } - }); - this.context.sort((a, b) => b.priority - a.priority || ( /** @type {number} */(a.index.value)) - ( /** @type {number} */(b.index.value))); - this.context.forEach((member, i) => (member.index.value = i)); - let i = this.context.length; - while (i--) { - const member = this.context[i]; - if (member.variable) { - if (member.variable.referenced || - member.variable.export_name || - (member.variable.is_reactive_dependency && - (member.variable.mutated || member.variable.reassigned))) - break; - } - else if (member.is_non_contextual) { - break; - } - } - this.initial_context = this.context.slice(0, i + 1); - } + /** + * @param {string} name + * @param {any} contextual + */ + add_to_context(name, contextual = false) { + if (!this.context_lookup.has(name)) { + /** @type {ContextMember} */ + const member = { + name, + index: { type: 'Literal', value: this.context.length }, + is_contextual: false, + is_non_contextual: false, + variable: null, + priority: 0 + }; + this.context_lookup.set(name, member); + this.context.push(member); + } + const member = this.context_lookup.get(name); + if (contextual) { + member.is_contextual = true; + } else { + member.is_non_contextual = true; + member.variable = this.component.var_lookup.get(name); + } + return member; + } - /** - * @param {string} name - * @param {any} contextual - */ - add_to_context(name, contextual = false) { - if (!this.context_lookup.has(name)) { + /** + * @param {string} name + * @param {unknown} [value] + * @param {boolean} main_execution_context + */ + invalidate(name, value, main_execution_context = false) { + return renderer_invalidate(this, name, value, main_execution_context); + } - /** @type {ContextMember} */ - const member = { - name, - index: { type: 'Literal', value: this.context.length }, - is_contextual: false, - is_non_contextual: false, - variable: null, - priority: 0 - }; - this.context_lookup.set(name, member); - this.context.push(member); - } - const member = this.context_lookup.get(name); - if (contextual) { - member.is_contextual = true; - } - else { - member.is_non_contextual = true; - member.variable = this.component.var_lookup.get(name); - } - return member; - } + /** + * @param {string[]} names + * @param {any} is_reactive_declaration + * @returns {import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').Expression} + */ + dirty(names, is_reactive_declaration = false) { + const renderer = this; + const dirty = /** @type {| import('estree').Identifier + | import('estree').MemberExpression} */ ( + is_reactive_declaration ? x`$$self.$$.dirty` : x`#dirty` + ); + const get_bitmask = () => { + /** @type {BitMasks} */ + const bitmask = []; + names.forEach((name) => { + const member = renderer.context_lookup.get(name); + if (!member) return; + if (member.index.value === -1) { + throw new Error('unset index'); + } + const value = /** @type {number} */ (member.index.value); + const i = (value / 31) | 0; + const n = 1 << value % 31; + if (!bitmask[i]) bitmask[i] = { n: 0, names: [] }; + bitmask[i].n |= n; + bitmask[i].names.push(name); + }); + return bitmask; + }; + // TODO: context-overflow make it less gross + return /** @type {any} */ ({ + // Using a ParenthesizedExpression allows us to create + // the expression lazily. TODO would be better if + // context was determined before rendering, so that + // this indirection was unnecessary + type: 'ParenthesizedExpression', + get expression() { + const bitmask = get_bitmask(); + if (!bitmask.length) { + return /** @type {import('estree').BinaryExpression} */ ( + x`${dirty} & /*${names.join(', ')}*/ 0` + ); + } + if (renderer.context_overflow) { + return bitmask + .map((b, i) => ({ b, i })) + .filter(({ b }) => b) + .map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) + .reduce((lhs, rhs) => x`${lhs} | ${rhs}`); + } + return /** @type {import('estree').BinaryExpression} */ ( + x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` + ); + } + }); + } + // NOTE: this method may be called before this.context_overflow / this.context is fully defined + // therefore, they can only be evaluated later in a getter function - /** - * @param {string} name - * @param {unknown} [value] - * @param {boolean} main_execution_context - */ - invalidate(name, value, main_execution_context = false) { - return renderer_invalidate(this, name, value, main_execution_context); - } + /** @returns {import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').UnaryExpression | import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').ArrayExpression} */ + get_initial_dirty() { + const _this = this; + // TODO: context-overflow make it less gross - /** - * @param {string[]} names - * @param {any} is_reactive_declaration - * @returns {import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').Expression} - */ - dirty(names, is_reactive_declaration = false) { - const renderer = this; - const dirty = /** @type {| import('estree').Identifier - | import('estree').MemberExpression} */ ((is_reactive_declaration ? x `$$self.$$.dirty` : x `#dirty`)); - const get_bitmask = () => { + /** @type {import('estree').UnaryExpression} */ + const val = /** @type {import('estree').UnaryExpression} */ (x`-1`); + return { + get type() { + return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression'; + }, + // as [-1] + get elements() { + const elements = []; + for (let i = 0; i < _this.context.length; i += 31) { + elements.push(val); + } + return elements; + }, + // as -1 + operator: val.operator, + prefix: val.prefix, + argument: val.argument + }; + } - /** @type {BitMasks} */ - const bitmask = []; - names.forEach((name) => { - const member = renderer.context_lookup.get(name); - if (!member) - return; - if (member.index.value === -1) { - throw new Error('unset index'); - } - const value = /** @type {number} */ (member.index.value); - const i = (value / 31) | 0; - const n = 1 << value % 31; - if (!bitmask[i]) - bitmask[i] = { n: 0, names: [] }; - bitmask[i].n |= n; - bitmask[i].names.push(name); - }); - return bitmask; - }; - // TODO: context-overflow make it less gross - return /** @type {any} */ ({ - // Using a ParenthesizedExpression allows us to create - // the expression lazily. TODO would be better if - // context was determined before rendering, so that - // this indirection was unnecessary - type: 'ParenthesizedExpression', - get expression() { - const bitmask = get_bitmask(); - if (!bitmask.length) { - return /** @type {import('estree').BinaryExpression} */ (x `${dirty} & /*${names.join(', ')}*/ 0`); - } - if (renderer.context_overflow) { - return bitmask - .map((b, i) => ({ b, i })) - .filter(({ b }) => b) - .map(({ b, i }) => x `${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) - .reduce((lhs, rhs) => x `${lhs} | ${rhs}`); - } - return /** @type {import('estree').BinaryExpression} */ (x `${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}`); - } - }); - } - // NOTE: this method may be called before this.context_overflow / this.context is fully defined - // therefore, they can only be evaluated later in a getter function + /** + * @param {string | import('estree').Identifier | import('estree').MemberExpression} node + * @param {string | void} ctx + */ + reference(node, ctx = '#ctx') { + if (typeof node === 'string') { + node = { type: 'Identifier', name: node }; + } + const { name, nodes } = flatten_reference(node); + const member = this.context_lookup.get(name); + // TODO is this correct? + if (this.component.var_lookup.get(name)) { + this.component.add_reference(node, name); + } + if (member !== undefined) { + const replacement = /** @type {import('estree').MemberExpression} */ ( + x`/*${member.name}*/ ${ctx}[${member.index}]` + ); + if (nodes[0].loc) replacement.object.loc = nodes[0].loc; + nodes[0] = replacement; + return nodes.reduce((lhs, rhs) => x`${lhs}.${rhs}`); + } + return node; + } - /** @returns {import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').UnaryExpression | import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').ArrayExpression} */ - get_initial_dirty() { - const _this = this; - // TODO: context-overflow make it less gross - - /** @type {import('estree').UnaryExpression} */ - const val = /** @type {import('estree').UnaryExpression} */ (x `-1`); - return { - get type() { - return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression'; - }, - // as [-1] - get elements() { - const elements = []; - for (let i = 0; i < _this.context.length; i += 31) { - elements.push(val); - } - return elements; - }, - // as -1 - operator: val.operator, - prefix: val.prefix, - argument: val.argument - }; - } - - /** - * @param {string | import('estree').Identifier | import('estree').MemberExpression} node - * @param {string | void} ctx - */ - reference(node, ctx = '#ctx') { - if (typeof node === 'string') { - node = { type: 'Identifier', name: node }; - } - const { name, nodes } = flatten_reference(node); - const member = this.context_lookup.get(name); - // TODO is this correct? - if (this.component.var_lookup.get(name)) { - this.component.add_reference(node, name); - } - if (member !== undefined) { - const replacement = /** @type {import('estree').MemberExpression} */ (x `/*${member.name}*/ ${ctx}[${member.index}]`); - if (nodes[0].loc) - replacement.object.loc = nodes[0].loc; - nodes[0] = replacement; - return nodes.reduce((lhs, rhs) => x `${lhs}.${rhs}`); - } - return node; - } - - /** @param {import('./Block.js').default | import('estree').Node | import('estree').Node[]} block */ - remove_block(block) { - this.blocks.splice(this.blocks.indexOf(block), 1); - } + /** @param {import('./Block.js').default | import('estree').Node | import('estree').Node[]} block */ + remove_block(block) { + this.blocks.splice(this.blocks.indexOf(block), 1); + } } - /** * @typedef {Array<{ * n: number; @@ -299,7 +307,7 @@ export default class Renderer { * @property {boolean} is_contextual * @property {boolean} is_non_contextual * @property {Var} variable - * @property {number} priority + * @property {number} priority */ /** @typedef {Object} BindingGroup * @property {(to_reference?:boolean)=>Node} binding_group @@ -307,5 +315,5 @@ export default class Renderer { * @property {Set} list_dependencies * @property {string} keypath * @property {(block:Block,element:Identifier)=>void} add_element - * @property {(block:Block)=>void} render - */ \ No newline at end of file + * @property {(block:Block)=>void} render + */