From 66d9e2fba288b679956f45232fb3fe45c55b05df Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 3 Jun 2026 09:34:38 +1200 Subject: [PATCH] fix: guarantee parser advance to avoid infinite recursion Signed-off-by: Josh Wulf --- compiler/src/parser_base.ts | 5 +++++ compiler/src/parser_recovery.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/compiler/src/parser_base.ts b/compiler/src/parser_base.ts index 5bd9099..3e5870c 100644 --- a/compiler/src/parser_base.ts +++ b/compiler/src/parser_base.ts @@ -38,6 +38,11 @@ export class TokenStream { this.loopGuard = 0; } + /** Current position in the token stream (used to detect lack of progress during error recovery) */ + position(): number { + return this.pos; + } + peek(offset: number = 0): Token { return this.tokens[this.pos + offset] ?? this.tokens[this.tokens.length - 1]; } diff --git a/compiler/src/parser_recovery.ts b/compiler/src/parser_recovery.ts index 4c6f8e9..00e6a8b 100644 --- a/compiler/src/parser_recovery.ts +++ b/compiler/src/parser_recovery.ts @@ -57,6 +57,7 @@ export class RecoveringParser { const systems: AST.SystemDeclNode[] = []; while (!this.s.check(TokenKind.EOF)) { + const before = this.s.position(); try { systems.push(this.parseSystemDecl()); } catch (e) { @@ -67,6 +68,11 @@ export class RecoveringParser { throw e; } } + // Guarantee forward progress: if neither parsing nor synchronization + // consumed a token, skip one to avoid an infinite recovery loop. + if (this.s.position() === before && !this.s.check(TokenKind.EOF)) { + this.s.advance(); + } } if (systems.length === 0 && this.errors.length === 0) { @@ -107,6 +113,7 @@ export class RecoveringParser { const declarations: AST.DeclarationNode[] = []; while (!this.s.check(TokenKind.RBrace) && !this.s.check(TokenKind.EOF)) { + const before = this.s.position(); try { declarations.push(this.parseDeclaration()); } catch (e) { @@ -117,6 +124,15 @@ export class RecoveringParser { throw e; } } + // Guarantee forward progress to avoid an infinite recovery loop when + // synchronization lands on a token the body loop cannot consume. + if ( + this.s.position() === before && + !this.s.check(TokenKind.RBrace) && + !this.s.check(TokenKind.EOF) + ) { + this.s.advance(); + } } this.s.expect(TokenKind.RBrace, "system body close");