Skip to content

Commit 75803fd

Browse files
webproclaude
andcommitted
Collect arithmetic command expansions during parse, drop tree walk
parseArithmeticExpression now pushes ArithmeticCommandExpansion nodes into a module-level pending list as they're created. Callers drain the list and parse inner scripts directly instead of walking the arith tree. Eliminates resolveArithmeticExpansions and the per-word tree-walk loops in parts.ts. Arith-heavy fixtures see ~9% faster `parse + parts`; other fixtures within noise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 880c12b commit 75803fd

4 files changed

Lines changed: 54 additions & 62 deletions

File tree

src/arithmetic.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,16 @@ function opRightAssoc(op: string): boolean {
106106
}
107107
}
108108

109+
let pendingArithCmdExps: ArithmeticCommandExpansion[] | null = null;
110+
111+
export function drainArithCmdExps(): ArithmeticCommandExpansion[] | null {
112+
const out = pendingArithCmdExps;
113+
pendingArithCmdExps = null;
114+
return out;
115+
}
116+
109117
export function parseArithmeticExpression(src: string, offset: number = 0): ArithmeticExpression | null {
118+
pendingArithCmdExps = null;
110119
let pos = 0;
111120
const len = src.length;
112121

@@ -410,14 +419,16 @@ export function parseArithmeticExpression(src: string, offset: number = 0): Arit
410419
}
411420
const text = src.slice(start, pos);
412421
const inner = text.slice(2, -1); // remove "$(" and ")"
413-
return {
422+
const node: ArithmeticCommandExpansion = {
414423
type: "ArithmeticCommandExpansion",
415424
pos: start + offset,
416425
end: pos + offset,
417426
text,
418427
inner,
419428
script: undefined,
420-
} satisfies ArithmeticCommandExpansion;
429+
};
430+
(pendingArithCmdExps ??= []).push(node);
431+
return node;
421432
}
422433
} else if (c === CH_LBRACE) {
423434
// ${ parameter expansion }

src/lexer.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// oxlint-disable unicorn/no-thenable
22
import type {
3+
ArithmeticCommandExpansion,
34
DeferredCommandExpansion,
45
DoubleQuotedChild,
56
ExtGlobOperator,
@@ -8,7 +9,7 @@ import type {
89
Word,
910
WordPart,
1011
} from "./types.ts";
11-
import { parseArithmeticExpression } from "./arithmetic.ts";
12+
import { parseArithmeticExpression, drainArithCmdExps } from "./arithmetic.ts";
1213
import { WordImpl } from "./word.ts";
1314
import {
1415
CH_TAB,
@@ -325,6 +326,7 @@ export class Lexer {
325326
private hasPeek: boolean;
326327
private pendingHereDocs: PendingHereDoc[];
327328
private collectedExpansions: DeferredCommandExpansion[];
329+
private collectedArithCmdExps: ArithmeticCommandExpansion[] | null = null;
328330
_errors: ParseError[] | null = null;
329331
_buildParts = false;
330332

@@ -351,6 +353,10 @@ export class Lexer {
351353
return this.collectedExpansions;
352354
}
353355

356+
getCollectedArithCmdExps(): ArithmeticCommandExpansion[] | null {
357+
return this.collectedArithCmdExps;
358+
}
359+
354360
getPos(): number {
355361
return this.pos;
356362
}
@@ -1738,6 +1744,11 @@ export class Lexer {
17381744
this._resultHasExpansion = false;
17391745
if (this._buildParts) {
17401746
const expr = parseArithmeticExpression(body) ?? undefined;
1747+
const drained = drainArithCmdExps();
1748+
if (drained) {
1749+
if (this.collectedArithCmdExps) this.collectedArithCmdExps.push(...drained);
1750+
else this.collectedArithCmdExps = drained;
1751+
}
17411752
this._resultPart = { type: "ArithmeticExpansion", text, expression: expr };
17421753
} else {
17431754
this._resultPart = undefined;

src/parser.ts

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import type {
3939
Word,
4040
} from "./types.ts";
4141
import { LexContext, Token, Lexer, TokenValue } from "./lexer.ts";
42-
import { parseArithmeticExpression } from "./arithmetic.ts";
42+
import { parseArithmeticExpression, drainArithCmdExps } from "./arithmetic.ts";
4343
import { computeWordParts, computeHereDocBodyParts } from "./parts.ts";
4444
import { WordImpl } from "./word.ts";
4545

@@ -62,7 +62,7 @@ class ArithmeticCommandImpl implements ArithmeticCommand {
6262
get expression(): ArithmeticExpression | undefined {
6363
if (this.#expression === null) {
6464
this.#expression = parseArithmeticExpression(this.body, this.pos + 2) ?? undefined;
65-
if (this.#expression) resolveArithmeticExpansions(this.#expression);
65+
resolveDrainedArithCmdExps();
6666
}
6767
return this.#expression;
6868
}
@@ -112,10 +112,8 @@ class ArithmeticForImpl implements ArithmeticFor {
112112
if (this.#initialize === null) {
113113
if (this.#initStr) {
114114
const expr = parseArithmeticExpression(this.#initStr);
115-
if (expr) {
116-
offsetArith(expr, this.#initPos);
117-
resolveArithmeticExpansions(expr);
118-
}
115+
if (expr) offsetArith(expr, this.#initPos);
116+
resolveDrainedArithCmdExps();
119117
this.#initialize = expr ?? undefined;
120118
} else {
121119
this.#initialize = undefined;
@@ -131,10 +129,8 @@ class ArithmeticForImpl implements ArithmeticFor {
131129
if (this.#test === null) {
132130
if (this.#testStr) {
133131
const expr = parseArithmeticExpression(this.#testStr);
134-
if (expr) {
135-
offsetArith(expr, this.#testPos);
136-
resolveArithmeticExpansions(expr);
137-
}
132+
if (expr) offsetArith(expr, this.#testPos);
133+
resolveDrainedArithCmdExps();
138134
this.#test = expr ?? undefined;
139135
} else {
140136
this.#test = undefined;
@@ -150,10 +146,8 @@ class ArithmeticForImpl implements ArithmeticFor {
150146
if (this.#update === null) {
151147
if (this.#updateStr) {
152148
const expr = parseArithmeticExpression(this.#updateStr);
153-
if (expr) {
154-
offsetArith(expr, this.#updatePos);
155-
resolveArithmeticExpansions(expr);
156-
}
149+
if (expr) offsetArith(expr, this.#updatePos);
150+
resolveDrainedArithCmdExps();
157151
this.#update = expr ?? undefined;
158152
} else {
159153
this.#update = undefined;
@@ -209,29 +203,14 @@ function offsetArith(node: ArithmeticExpression, base: number): void {
209203
}
210204
}
211205

212-
export function resolveArithmeticExpansions(expr: ArithmeticExpression): void {
213-
switch (expr.type) {
214-
case "ArithmeticBinary":
215-
resolveArithmeticExpansions(expr.left);
216-
resolveArithmeticExpansions(expr.right);
217-
break;
218-
case "ArithmeticUnary":
219-
resolveArithmeticExpansions(expr.operand);
220-
break;
221-
case "ArithmeticTernary":
222-
resolveArithmeticExpansions(expr.test);
223-
resolveArithmeticExpansions(expr.consequent);
224-
resolveArithmeticExpansions(expr.alternate);
225-
break;
226-
case "ArithmeticGroup":
227-
resolveArithmeticExpansions(expr.expression);
228-
break;
229-
case "ArithmeticCommandExpansion":
230-
if (expr.inner !== undefined) {
231-
expr.script = parse(expr.inner);
232-
expr.inner = undefined;
233-
}
234-
break;
206+
function resolveDrainedArithCmdExps(): void {
207+
const list = drainArithCmdExps();
208+
if (!list) return;
209+
for (const node of list) {
210+
if (node.inner !== undefined) {
211+
node.script = parse(node.inner);
212+
node.inner = undefined;
213+
}
235214
}
236215
}
237216

src/parts.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DeferredCommandExpansion, Word, WordPart } from "./types.ts";
22
import { Lexer } from "./lexer.ts";
3-
import { parse, resolveArithmeticExpansions } from "./parser.ts";
3+
import { parse } from "./parser.ts";
44

55
/**
66
* Compute the structural parts of a word by re-scanning the source.
@@ -12,19 +12,7 @@ export function computeWordParts(source: string, word: Word): WordPart[] | undef
1212
const lexer = new Lexer(source);
1313
const parts = lexer.buildWordParts(word.pos);
1414
if (!parts) return undefined;
15-
16-
// Resolve command expansions: parse inner scripts
17-
for (const exp of lexer.getCollectedExpansions()) {
18-
resolveExpansion(exp);
19-
}
20-
21-
// Resolve command substitutions inside arithmetic expressions
22-
for (const part of parts) {
23-
if (part.type === "ArithmeticExpansion" && part.expression) {
24-
resolveArithmeticExpansions(part.expression);
25-
}
26-
}
27-
15+
resolveCollected(lexer);
2816
return parts;
2917
}
3018

@@ -37,20 +25,23 @@ export function computeHereDocBodyParts(source: string, word: Word): WordPart[]
3725
const lexer = new Lexer(source);
3826
const parts = lexer.buildHereDocParts(word.pos, word.end);
3927
if (!parts) return undefined;
28+
resolveCollected(lexer);
29+
return parts;
30+
}
4031

41-
// Resolve command expansions: parse inner scripts
32+
function resolveCollected(lexer: Lexer): void {
4233
for (const exp of lexer.getCollectedExpansions()) {
4334
resolveExpansion(exp);
4435
}
45-
46-
// Resolve command substitutions inside arithmetic expressions
47-
for (const part of parts) {
48-
if (part.type === "ArithmeticExpansion" && part.expression) {
49-
resolveArithmeticExpansions(part.expression);
36+
const arithCmdExps = lexer.getCollectedArithCmdExps();
37+
if (arithCmdExps) {
38+
for (const node of arithCmdExps) {
39+
if (node.inner !== undefined) {
40+
node.script = parse(node.inner);
41+
node.inner = undefined;
42+
}
5043
}
5144
}
52-
53-
return parts;
5445
}
5546

5647
function resolveExpansion(e: DeferredCommandExpansion) {

0 commit comments

Comments
 (0)