Skip to content

Commit 010daf6

Browse files
committed
perf(parser): speed up getLocation
- cache line start indices per input string (small LRU)\n- deno.json: bump version to 0.2.7
1 parent 3d5d307 commit 010daf6

2 files changed

Lines changed: 44 additions & 7 deletions

File tree

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@claudiu-ceia/combine",
3-
"version": "0.2.6",
3+
"version": "0.2.7",
44
"exports": "./mod.ts",
55
"publish": {
66
"exclude": ["bench/", "tests/", "npm/"]

src/Parser.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ export const success = <T>(ctx: Context, value: T): Success<T> => {
7777
};
7878
};
7979

80+
const LINE_CACHE_LIMIT = 8;
81+
// Cache per input string. `null` means the string has no '\n' (single line).
82+
const lineStartsCache = new Map<string, number[] | null>();
83+
84+
const getLineStarts = (text: string): number[] | null => {
85+
if (lineStartsCache.has(text)) {
86+
const cached = lineStartsCache.get(text) ?? null;
87+
// Basic LRU bump: preserve small cache without unbounded growth.
88+
lineStartsCache.delete(text);
89+
lineStartsCache.set(text, cached);
90+
return cached;
91+
}
92+
93+
const starts: number[] = [0];
94+
for (let i = 0; i < text.length; i++) {
95+
if (text.charCodeAt(i) === 10 /* '\n' */) starts.push(i + 1);
96+
}
97+
98+
const value = starts.length === 1 ? null : starts;
99+
lineStartsCache.set(text, value);
100+
if (lineStartsCache.size > LINE_CACHE_LIMIT) {
101+
const oldest = lineStartsCache.keys().next().value as string | undefined;
102+
if (oldest !== undefined) lineStartsCache.delete(oldest);
103+
}
104+
105+
return value;
106+
};
107+
80108
/**
81109
* Compute line and column from context
82110
*/
@@ -88,14 +116,23 @@ export const getLocation = (ctx: Context): { line: number; column: number } => {
88116
if (index < 0) index = 0;
89117
if (index > textLength) index = textLength;
90118

91-
const parsedCtx = text.slice(0, index);
92-
const parsedLines = parsedCtx.split("\n");
93-
const line = parsedLines.length;
119+
if (index === 0) return { line: 1, column: 1 };
94120

95-
// `split` always returns at least one element, but keep a safe fallback.
96-
const lastLine = parsedLines[parsedLines.length - 1] ?? "";
97-
const column = lastLine.length + 1;
121+
const starts = getLineStarts(text);
122+
if (starts === null) return { line: 1, column: index + 1 };
123+
124+
// upper bound: number of line starts <= index => 1-based line number
125+
let lo = 0;
126+
let hi = starts.length;
127+
while (lo < hi) {
128+
const mid = (lo + hi) >> 1;
129+
if (starts[mid]! <= index) lo = mid + 1;
130+
else hi = mid;
131+
}
98132

133+
const line = lo;
134+
const lineStart = starts[line - 1] ?? 0;
135+
const column = index - lineStart + 1;
99136
return { line, column };
100137
};
101138

0 commit comments

Comments
 (0)