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