diff --git a/lang/el-compiler/src/parser.el b/lang/el-compiler/src/parser.el index e4de3b9..d93fcba 100644 --- a/lang/el-compiler/src/parser.el +++ b/lang/el-compiler/src/parser.el @@ -23,10 +23,29 @@ fn tok_at(tokens: [Any], pos: Int) -> Map { } fn tok_kind(tokens: [Any], pos: Int) -> String { + // Out-of-range reads must report the Eof sentinel so every `== "Eof"` + // termination guard in the parser fires. Without this, reading past the + // single trailing Eof token returns runtime null (el_list_get OOB -> 0), + // which matches no delimiter, letting inner parse loops append AST nodes + // forever on malformed input -> unbounded allocation -> OOM. + let n: Int = native_list_len(tokens) / 2 + if pos < 0 { + return "Eof" + } + if pos >= n { + return "Eof" + } native_list_get(tokens, pos * 2) } fn tok_value(tokens: [Any], pos: Int) -> String { + let n: Int = native_list_len(tokens) / 2 + if pos < 0 { + return "" + } + if pos >= n { + return "" + } native_list_get(tokens, pos * 2 + 1) } @@ -35,7 +54,12 @@ fn expect(tokens: [Any], pos: Int, kind: String) -> Int { if k == kind { return pos + 1 } - // On mismatch just advance; error recovery is best-effort + // On mismatch, error recovery is best-effort. But never step PAST the Eof + // sentinel: once at Eof a mismatch means the input ended early, and + // advancing would run the cursor off the token list. + if k == "Eof" { + return pos + } pos + 1 }