perf: build identifier and member chain expressions without acorn (#18740)

After #18738 the biggest thing left in the parser profile was acorn's
constructor, `getOptions` and `wordsRegexp` alone were around 12% of
parse time. `parseExpressionAt` is `new Parser()` followed by
`parseExpression()`, and we call it for every expression in the
template. For something like `{name}` the constructor (normalizing
options, four keyword regexes, scope state) is about 2 µs while the
actual parse is under 1 µs, and components have dozens of these.

The fix seemed to belong in acorn, cache the options and regexes so
repeated `parseExpressionAt` calls stop redoing them. I lasted about
twenty minutes in acorn's constructor... I was way in over my head. The
Svelte alternative was one parser per component, reset between
expressions, but that means re-initializing acorn's and
acorn-typescript's internals by hand and breaking whenever either adds a
field.

I realized most of the expressions in the test corpus are an identifier
or an `a.b.c` chain, and you don't need a JS parser for those. So that's
this fix. `read_expression` now scans for that shape, builds the nodes
itself in the form acorn would, and hands everything else to acorn like
before.No behavior change, verified byte-for-byte against acorn on the
test corpus.

About 18% faster parsing on typical components on top of #18738.

I guess this should be a fix for acorn, rather than here, so I'm not
against closing this.

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/18751/head
Nic Polumeyv 2 days ago committed by GitHub
parent 504a7536c6
commit 34142af3a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: speed up parser interactions with Acorn or avoid them where possible

@ -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+\)$/;

@ -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 };
}

Loading…
Cancel
Save