(P: ParseState)
| 706 | } |
| 707 | |
| 708 | function parseProgram(P: ParseState): TsNode { |
| 709 | const children: TsNode[] = [] |
| 710 | // Skip leading whitespace & newlines — program start is first content byte |
| 711 | skipBlanks(P.L) |
| 712 | while (true) { |
| 713 | const save = saveLex(P.L) |
| 714 | const t = nextToken(P.L, 'cmd') |
| 715 | if (t.type === 'NEWLINE') { |
| 716 | skipBlanks(P.L) |
| 717 | continue |
| 718 | } |
| 719 | restoreLex(P.L, save) |
| 720 | break |
| 721 | } |
| 722 | const progStart = P.L.b |
| 723 | while (P.L.i < P.L.len) { |
| 724 | const save = saveLex(P.L) |
| 725 | const t = nextToken(P.L, 'cmd') |
| 726 | if (t.type === 'EOF') break |
| 727 | if (t.type === 'NEWLINE') continue |
| 728 | if (t.type === 'COMMENT') { |
| 729 | children.push(leaf(P, 'comment', t)) |
| 730 | continue |
| 731 | } |
| 732 | restoreLex(P.L, save) |
| 733 | const stmts = parseStatements(P, null) |
| 734 | for (const s of stmts) children.push(s) |
| 735 | if (stmts.length === 0) { |
| 736 | // Couldn't parse — emit ERROR and skip one token |
| 737 | const errTok = nextToken(P.L, 'cmd') |
| 738 | if (errTok.type === 'EOF') break |
| 739 | // Stray `;;` at program level (e.g., `var=;;` outside case) — tree-sitter |
| 740 | // silently elides. Keep leading `;` as ERROR (security: paste artifact). |
| 741 | if ( |
| 742 | errTok.type === 'OP' && |
| 743 | errTok.value === ';;' && |
| 744 | children.length > 0 |
| 745 | ) { |
| 746 | continue |
| 747 | } |
| 748 | children.push(mk(P, 'ERROR', errTok.start, errTok.end, [])) |
| 749 | } |
| 750 | } |
| 751 | // tree-sitter includes trailing whitespace in program extent |
| 752 | const progEnd = children.length > 0 ? P.srcBytes : progStart |
| 753 | return mk(P, 'program', progStart, progEnd, children) |
| 754 | } |
| 755 | |
| 756 | /** Packed as (b << 16) | i — avoids heap alloc on every backtrack. */ |
| 757 | type LexSave = number |
no test coverage detected