From 79bea307ca0d327035c831607b6247eabdcf291c Mon Sep 17 00:00:00 2001 From: Nic <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:26:31 -0400 Subject: [PATCH] perf: build identifier and member chain expressions without acorn --- .../src/compiler/phases/1-parse/acorn.js | 13 +- .../phases/1-parse/read/expression.js | 112 +++++++++++++++++- 2 files changed, 120 insertions(+), 5 deletions(-) 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..2a754f92a4 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, 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 }; +}