Skip to content

Commit

Permalink
Added anti-loop caching
Browse files Browse the repository at this point in the history
  • Loading branch information
james-pre committed Dec 15, 2024
1 parent 8d1dd88 commit 9f1f367
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 14 deletions.
20 changes: 12 additions & 8 deletions scripts/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ try {
process.exit(1);
}

function dump_info() {
if (!options.info) return;

const info = parseInfo.get(input);

for (const [k, v] of Object.entries(info)) {
console.error(k + ': ', v);
}
}

let ast;
try {
ast = parse({
Expand All @@ -54,16 +64,10 @@ try {
},
id: input,
});
if (options.info) {
const info = parseInfo.get(input);
console.error('parseNode calls:', info.parseNodeCalls);
}
dump_info();
} catch (e) {
console.error('Error: parsing failed:', e);
if (options.info) {
const info = parseInfo.get(input);
console.error('parseNode calls:', info.parseNodeCalls);
}
dump_info();
process.exit(1);
}

Expand Down
31 changes: 25 additions & 6 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function find_loop(strings: string[], counts: number): string[] | null {

interface ParseInfo {
parseNodeCalls: number;
nodesParsed: number;
}

export const parseInfo = new Map<string, ParseInfo>();
Expand All @@ -76,7 +77,9 @@ export function parse(options: ParseOptions): Node[] {
const tokens = 'tokens' in options ? options.tokens : tokenize(options.source, options.literals);
const literals = 'tokens' in options ? options.literals : [...options.literals].map(literal => literal.name);

if (id) parseInfo.set(id, { parseNodeCalls: 0 });
const attempts = new Map<string, Node | null>();

if (id) parseInfo.set(id, { parseNodeCalls: 0, nodesParsed: 0 });

function parseNode(kind: string, parents: string[] = []): Node | null {
if (id) parseInfo.get(id)!.parseNodeCalls++;
Expand All @@ -85,14 +88,30 @@ export function parse(options: ParseOptions): Node[] {

if (depth >= max_depth) throw 'Max depth exceeded when parsing ' + kind;

const log = (level: number, message: string): void => options.log?.(level, message, depth);

const attempt = kind + position + (depth == 0 ? '' : parents.at(-1));

const _cache = (node: Node | null) => {
attempts.set(attempt, node);
return node;
};

if (attempts.has(attempt) && find_loop(parents, 3)) {
log(3, `Already parsed ${kind} at ${position}`);
return attempts.get(attempt) ?? null;
}

_cache(null);

const loop = find_loop(parents, max_cycles);

if (loop) {
const node = tokens[position];
throw `Possible infinite loop: ${loop.join(' -> ')} -> ... at ${node.line}:${node.column}`;
}

const log = (level: number, message: string): void => options.log?.(level, message, depth);
if (id) parseInfo.get(id)!.nodesParsed++;

if (literals.includes(kind)) {
while (options.ignoreLiterals.includes(tokens[position]?.kind)) {
Expand All @@ -112,7 +131,7 @@ export function parse(options: ParseOptions): Node[] {
}

position++;
return { ...token, children: [] };
return _cache({ ...token, children: [] });
}

const definition = options.definitions.find(def => def.name === kind);
Expand All @@ -131,7 +150,7 @@ export function parse(options: ParseOptions): Node[] {
const node = parseNode(option.kind, [...parents, kind]);
if (node) {
log(3, `Resolved ${kind} to ${node.kind}`);
return node;
return _cache(node);
}
}
log(1, 'Warning: No matches for alternation');
Expand Down Expand Up @@ -171,14 +190,14 @@ export function parse(options: ParseOptions): Node[] {
}

log(1, `"${token.kind}" at ${token.line}:${token.column}`);
return {
return _cache({
kind,
text: token.text,
position: token.position,
line: token.line,
column: token.column,
children,
};
});
}
default:
console.warn('Invalid node kind:', kind);
Expand Down

0 comments on commit 9f1f367

Please sign in to comment.