diff --git a/.changeset/easy-points-tan.md b/.changeset/easy-points-tan.md new file mode 100644 index 0000000000..60edbf94eb --- /dev/null +++ b/.changeset/easy-points-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: speed up parser interactions with Acorn or avoid them where possible diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 3c3ad2bc8a..add61ea19f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -142,17 +142,24 @@ let last_template = ''; let lf_only = true; /** - * Without `startLocation`, acorn counts the lines before `index` on every call. - * It also breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't, so those templates are left to acorn + * Without `startLocation`, acorn counts the lines before `index` on every call * @param {Parser} parser * @param {number} index */ function start_location(parser, index) { + return has_lf_line_breaks_only(parser) ? locator(index) : undefined; +} + +/** + * acorn breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't + * @param {Parser} parser + */ +export function has_lf_line_breaks_only(parser) { if (parser.template !== last_template) { last_template = parser.template; lf_only = !regex_non_lf_line_break.test(last_template); } - return lf_only ? locator(index) : undefined; + return lf_only; } const regex_position_indicator = / \(\d+:\d+\)$/; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index 1c8f097c2f..b63ad443a8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -1,9 +1,13 @@ -/** @import { Expression } from 'estree' */ +/** @import { Expression, Identifier } from 'estree' */ /** @import { Parser } from '../index.js' */ -import { parse_expression_at, remove_parens } from '../acorn.js'; +// @ts-expect-error acorn type definitions are borked in the release we use +import { isIdentifierStart, isIdentifierChar } from 'acorn'; +import { has_lf_line_breaks_only, parse_expression_at, remove_parens } from '../acorn.js'; import { regex_whitespace } from '../../patterns.js'; import * as e from '../../../errors.js'; import { find_matching_bracket } from '../utils/bracket.js'; +import { is_reserved } from '../../../../utils.js'; +import { locator } from '../../../state.js'; /** * @param {Parser} parser @@ -33,6 +37,9 @@ export function get_loose_identifier(parser, opening_token) { * @returns {Expression} */ export default function read_expression(parser, opening_token, disallow_loose) { + const simple = read_simple_expression(parser); + if (simple) return simple; + try { const node = parse_expression_at(parser, parser.template, parser.index); @@ -57,3 +64,104 @@ export default function read_expression(parser, opening_token, disallow_loose) { throw err; } } + +/** + * Most template expressions are an identifier or a `a.b.c` member chain followed by `}`. + * Those are built directly for better parse performance, with the same shape acorn would produce; anything else goes to acorn + * @param {Parser} parser + * @returns {Expression | null} + */ +function read_simple_expression(parser) { + if (!has_lf_line_breaks_only(parser)) return null; + + const template = parser.template; + const index = parser.index; + + parser.allow_whitespace(); + const start = parser.index; + + let end = read_word(template, start); + if (end === -1 || is_reserved(template.slice(start, end))) { + parser.index = index; + return null; + } + + /** @type {Expression} */ + let node = identifier(template, start, end); + + while (template[end] === '.') { + const property_end = read_word(template, end + 1); + if (property_end === -1) { + parser.index = index; + return null; + } + + node = { + type: 'MemberExpression', + start, + end: property_end, + loc: { start: position(start), end: position(property_end) }, + object: node, + property: identifier(template, end + 1, property_end), + computed: false, + optional: false + }; + + end = property_end; + } + + parser.index = end; + parser.allow_whitespace(); + + if (!parser.match('}')) { + parser.index = index; + return null; + } + + parser.index = end; + return node; +} + +/** + * @param {string} template + * @param {number} start + * @returns {number} the end of the identifier starting at `start`, or -1 + */ +function read_word(template, start) { + if (start >= template.length) return -1; + + const code = /** @type {number} */ (template.codePointAt(start)); + if (!isIdentifierStart(code, true)) return -1; + + let end = start + (code <= 0xffff ? 1 : 2); + + while (end < template.length) { + const code = /** @type {number} */ (template.codePointAt(end)); + if (!isIdentifierChar(code, true)) break; + end += code <= 0xffff ? 1 : 2; + } + + return end; +} + +/** + * @param {string} template + * @param {number} start + * @param {number} end + * @returns {Identifier} + */ +function identifier(template, start, end) { + return { + type: 'Identifier', + start, + end, + loc: { start: position(start), end: position(end) }, + name: template.slice(start, end) + }; +} + +/** @param {number} index */ +function position(index) { + const { line, column } = locator(index); + return { line, column }; +}