Skip to content

Commit de0b47d

Browse files
committed
Fix word parts, lazy compute
1 parent 3b37b2e commit de0b47d

2 files changed

Lines changed: 183 additions & 4 deletions

File tree

bench/lazy-word.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { parse } from "../src/parser.ts";
2+
import type { Node, Statement, Word } from "../src/types.ts";
3+
import { bench, group, run, summary } from "mitata";
4+
import { short, advanced, specialized, installers, large } from "./fixtures.ts";
5+
6+
function visitWords(node: Node | Statement): number {
7+
let count = 0;
8+
switch (node.type) {
9+
case "Command":
10+
if (node.name) count += touchWord(node.name);
11+
for (const w of node.suffix) count += touchWord(w);
12+
for (const a of node.prefix) {
13+
if (a.value) count += touchWord(a.value);
14+
if (a.array) for (const w of a.array) count += touchWord(w);
15+
}
16+
for (const r of node.redirects) count += visitRedirect(r);
17+
break;
18+
case "Pipeline":
19+
for (const c of node.commands) count += visitWords(c);
20+
break;
21+
case "AndOr":
22+
for (const c of node.commands) count += visitWords(c);
23+
break;
24+
case "If":
25+
count += visitNode(node.clause);
26+
count += visitNode(node.then);
27+
if (node.else) count += visitNode(node.else);
28+
break;
29+
case "For":
30+
count += touchWord(node.name);
31+
for (const w of node.wordlist) count += touchWord(w);
32+
count += visitCompoundList(node.body);
33+
break;
34+
case "While":
35+
count += visitNode(node.clause);
36+
count += visitCompoundList(node.body);
37+
break;
38+
case "Case":
39+
count += touchWord(node.word);
40+
for (const item of node.items) {
41+
for (const w of item.pattern) count += touchWord(w);
42+
count += visitCompoundList(item.body);
43+
}
44+
break;
45+
case "Function":
46+
count += touchWord(node.name);
47+
count += visitNode(node.body);
48+
for (const r of node.redirects) count += visitRedirect(r);
49+
break;
50+
case "Select":
51+
count += touchWord(node.name);
52+
for (const w of node.wordlist) count += touchWord(w);
53+
count += visitCompoundList(node.body);
54+
break;
55+
case "Subshell":
56+
case "BraceGroup":
57+
count += visitCompoundList(node.body);
58+
break;
59+
case "CompoundList":
60+
count += visitCompoundList(node);
61+
break;
62+
case "Statement":
63+
count += visitWords(node.command);
64+
for (const r of node.redirects) count += visitRedirect(r);
65+
break;
66+
case "Coproc":
67+
if (node.name) count += touchWord(node.name);
68+
count += visitNode(node.body);
69+
for (const r of node.redirects) count += visitRedirect(r);
70+
break;
71+
case "TestCommand":
72+
count += visitTestExpr(node.expression);
73+
break;
74+
case "ArithmeticCommand":
75+
case "ArithmeticFor":
76+
break;
77+
}
78+
return count;
79+
}
80+
81+
function visitNode(node: Node): number {
82+
return visitWords(node);
83+
}
84+
85+
function visitCompoundList(cl: { commands: Statement[] }): number {
86+
let count = 0;
87+
for (const s of cl.commands) count += visitWords(s);
88+
return count;
89+
}
90+
91+
function visitRedirect(r: { target?: Word; body?: Word }): number {
92+
let count = 0;
93+
if (r.target) count += touchWord(r.target);
94+
if (r.body) count += touchWord(r.body);
95+
return count;
96+
}
97+
98+
function visitTestExpr(expr: import("../src/types.ts").TestExpression): number {
99+
let count = 0;
100+
switch (expr.type) {
101+
case "TestUnary":
102+
count += touchWord(expr.operand);
103+
break;
104+
case "TestBinary":
105+
count += touchWord(expr.left);
106+
count += touchWord(expr.right);
107+
break;
108+
case "TestLogical":
109+
count += visitTestExpr(expr.left);
110+
count += visitTestExpr(expr.right);
111+
break;
112+
case "TestNot":
113+
count += visitTestExpr(expr.operand);
114+
break;
115+
case "TestGroup":
116+
count += visitTestExpr(expr.expression);
117+
break;
118+
}
119+
return count;
120+
}
121+
122+
function touchWord(w: Word): number {
123+
w.parts; // trigger lazy computation
124+
return 1;
125+
}
126+
127+
for (const [label, scripts] of [
128+
["short", short],
129+
["advanced", advanced],
130+
...specialized,
131+
["medium", installers],
132+
["large", large],
133+
] as const) {
134+
group(label, () => {
135+
summary(() => {
136+
bench("parse only", () => {
137+
for (const s of scripts) parse(s);
138+
}).baseline();
139+
140+
bench("parse + parts", () => {
141+
for (const s of scripts) {
142+
const ast = parse(s);
143+
for (const stmt of ast.commands) visitWords(stmt);
144+
}
145+
});
146+
});
147+
});
148+
}
149+
150+
await run();

src/parser.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,36 @@ import type {
3737
TestUnaryExpression,
3838
While,
3939
Word,
40+
WordPart,
4041
} from "./types.ts";
4142
import { LexContext, Token, Lexer, TokenValue } from "./lexer.ts";
4243
import { parseArithmeticExpression } from "./arithmetic.ts";
44+
import { computeWordParts } from "./parts.ts";
45+
46+
class WordImpl implements Word {
47+
text: string;
48+
pos: number;
49+
end: number;
50+
#source: string;
51+
#parts: WordPart[] | undefined | null = null; // null = not yet computed
52+
53+
constructor(text: string, pos: number, end: number, source: string) {
54+
this.text = text;
55+
this.pos = pos;
56+
this.end = end;
57+
this.#source = source;
58+
}
59+
60+
get parts(): WordPart[] | undefined {
61+
if (this.#parts === null) {
62+
this.#parts = computeWordParts(this.#source, this) ?? undefined;
63+
}
64+
return this.#parts;
65+
}
66+
set parts(v: WordPart[] | undefined) {
67+
this.#parts = v ?? undefined;
68+
}
69+
}
4370

4471
const CASE_TERMINATORS: Record<number, CaseTerminator> = {
4572
[Token.DoubleSemi]: ";;",
@@ -175,11 +202,13 @@ export function parse(source: string): Script & { errors?: ParseError[] } {
175202

176203
class Parser {
177204
private tok: Lexer;
205+
private source: string;
178206
private errors: ParseError[] = [];
179207
private _redirects: Redirect[] = [];
180208

181209
constructor(source: string) {
182210
this.tok = new Lexer(source);
211+
this.source = source;
183212
}
184213

185214
parse(sourceLen: number): Script & { errors?: ParseError[] } {
@@ -961,7 +990,7 @@ class Parser {
961990
body: undefined,
962991
};
963992
if (t.content != null) {
964-
r.target = { text: t.content, pos: t.targetPos, end: t.targetEnd, parts: undefined };
993+
r.target = new WordImpl(t.content, t.targetPos, t.targetEnd, this.source);
965994
}
966995
if (t.value === "<<" || t.value === "<<-") this.tok.registerHereDocTarget(r);
967996
redirects.push(r);
@@ -982,11 +1011,11 @@ class Parser {
9821011
}
9831012

9841013
private toWord(tok: TokenValue): Word {
985-
return { text: tok.value, pos: tok.pos, end: tok.end, parts: undefined };
1014+
return new WordImpl(tok.value, tok.pos, tok.end, this.source);
9861015
}
9871016

9881017
private toWordFromPosEnd(tok: TokenValue, pos: number, end: number): Word {
989-
return { text: tok.value, pos, end, parts: undefined };
1018+
return new WordImpl(tok.value, pos, end, this.source);
9901019
}
9911020

9921021
private parseAssignment(tok: TokenValue): AssignmentPrefix {
@@ -1045,7 +1074,7 @@ class Parser {
10451074
const elements = this.parseArrayElements(inner, arrayOffset);
10461075
result.array = elements;
10471076
} else {
1048-
result.value = { text: valText, pos: tokPos + valStart, end: tokEnd, parts: undefined };
1077+
result.value = new WordImpl(valText, tokPos + valStart, tokEnd, this.source);
10491078
}
10501079

10511080
return result;

0 commit comments

Comments
 (0)