|
@@ -0,0 +1,584 @@
|
|
|
|
|
+/** Lenient Modula-2 declaration parser for IDE services (Phase 2).
|
|
|
|
|
+ *
|
|
|
|
|
+ * Parses module structure, imports, CONST/TYPE/VAR sections, procedures
|
|
|
|
|
+ * (including nested scopes, parameters and RECORD fields) and builds a
|
|
|
|
|
+ * symbol tree with source ranges. Statement and expression bodies are
|
|
|
|
|
+ * skipped with bracket/END matching. Recovery is best-effort: an
|
|
|
|
|
+ * unexpected token skips to the next synchronization point instead of
|
|
|
|
|
+ * aborting the whole file.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+import { Token, lex } from './lexer';
|
|
|
|
|
+
|
|
|
|
|
+export type M2SymbolKind =
|
|
|
|
|
+ | 'module' | 'procedure' | 'variable' | 'field'
|
|
|
|
|
+ | 'parameter' | 'constant' | 'type';
|
|
|
|
|
+
|
|
|
|
|
+export interface M2Range {
|
|
|
|
|
+ startLine: number; startCh: number; endLine: number; endCh: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface M2Symbol {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ kind: M2SymbolKind;
|
|
|
|
|
+ /** Short declaration text, e.g. `a : ARRAY [0..10] OF CHAR`. */
|
|
|
|
|
+ detail: string;
|
|
|
|
|
+ /** Range of the declared name. */
|
|
|
|
|
+ nameRange: M2Range;
|
|
|
|
|
+ /** Full extent (for containers: up to the closing END). */
|
|
|
|
|
+ extent: M2Range;
|
|
|
|
|
+ children: M2Symbol[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface M2Import {
|
|
|
|
|
+ /** Module the names come from. */
|
|
|
|
|
+ module: string;
|
|
|
|
|
+ /** Imported identifiers (FROM-imports). Empty when plain `IMPORT m`. */
|
|
|
|
|
+ names: string[];
|
|
|
|
|
+ /** True for plain `IMPORT m` (all exports visible as m.X). */
|
|
|
|
|
+ all: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface M2Unit {
|
|
|
|
|
+ filePath: string;
|
|
|
|
|
+ moduleName: string;
|
|
|
|
|
+ definition: boolean;
|
|
|
|
|
+ symbols: M2Symbol[];
|
|
|
|
|
+ imports: M2Import[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const SECTION = new Set(['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'MODULE', 'BEGIN', 'END', 'FINALLY', 'EXCEPT']);
|
|
|
|
|
+const OPENERS = new Set(['BEGIN', 'IF', 'CASE', 'LOOP', 'WHILE', 'FOR', 'WITH', 'RECORD', 'MODULE']);
|
|
|
|
|
+
|
|
|
|
|
+function rangeOf(t: Token): M2Range {
|
|
|
|
|
+ return { startLine: t.line, startCh: t.ch, endLine: t.endLine, endCh: t.endCh };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function posLe(aL: number, aC: number, bL: number, bC: number): boolean {
|
|
|
|
|
+ return aL < bL || (aL === bL && aC <= bC);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function rangeContains(r: M2Range, line: number, ch: number): boolean {
|
|
|
|
|
+ return posLe(r.startLine, r.startCh, line, ch) && posLe(line, ch, r.endLine, r.endCh);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class Parser {
|
|
|
|
|
+ private pos = 0;
|
|
|
|
|
+ constructor(private tokens: Token[], private text: string) {}
|
|
|
|
|
+
|
|
|
|
|
+ private peek(off = 0): Token {
|
|
|
|
|
+ return this.tokens[Math.min(this.pos + off, this.tokens.length - 1)];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private next(): Token {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ if (this.pos < this.tokens.length - 1) this.pos++;
|
|
|
|
|
+ return t;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private atKw(word: string): boolean {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ return t.kind === 'keyword' && t.text === word;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private atSym(sym: string): boolean {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ return t.kind === 'symbol' && t.text === sym;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private eatKw(word: string): boolean {
|
|
|
|
|
+ if (this.atKw(word)) { this.next(); return true; }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private eatSym(sym: string): boolean {
|
|
|
|
|
+ if (this.atSym(sym)) { this.next(); return true; }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private expectIdent(): Token | null {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ if (t.kind === 'ident') return this.next();
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Raw source between two tokens (for detail/type text). */
|
|
|
|
|
+ private slice(a: Token, b: Token): string {
|
|
|
|
|
+ return this.text.slice(a.offset, b.endOffset);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ parseUnit(filePath: string): M2Unit {
|
|
|
|
|
+ const definition = this.atKw('DEFINITION');
|
|
|
|
|
+ if (definition) this.next();
|
|
|
|
|
+ this.eatKw('IMPLEMENTATION');
|
|
|
|
|
+ let moduleName = '';
|
|
|
|
|
+ let startTok: Token = this.peek();
|
|
|
|
|
+ if (this.eatKw('MODULE')) {
|
|
|
|
|
+ const nm = this.expectIdent();
|
|
|
|
|
+ if (nm) { moduleName = nm.text; startTok = nm; }
|
|
|
|
|
+ }
|
|
|
|
|
+ // FOR "..." in definition modules, priority [..], then ';'
|
|
|
|
|
+ if (this.atKw('FOR')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ if (this.peek().kind === 'string') this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.atSym('[')) this.skipBalanced('[', ']');
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+
|
|
|
|
|
+ const unit: M2Unit = { filePath, moduleName, definition, symbols: [], imports: [] };
|
|
|
|
|
+ const modSym: M2Symbol = {
|
|
|
|
|
+ name: moduleName, kind: 'module', detail: (definition ? 'DEFINITION MODULE ' : 'MODULE ') + moduleName,
|
|
|
|
|
+ nameRange: rangeOf(startTok), extent: rangeOf(startTok), children: [],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.parseImportSeq(unit);
|
|
|
|
|
+ if (!definition) {
|
|
|
|
|
+ if (this.atKw('EXPORT')) this.skipToSym(';');
|
|
|
|
|
+ this.parseBlock(modSym, unit, true);
|
|
|
|
|
+ this.eatSym('.');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (this.atKw('EXPORT')) this.skipToSym(';');
|
|
|
|
|
+ this.parseDeclarations(modSym, unit, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ // Module extent runs to end of file.
|
|
|
|
|
+ const last = this.tokens[this.tokens.length - 1];
|
|
|
|
|
+ modSym.extent = {
|
|
|
|
|
+ startLine: modSym.extent.startLine, startCh: modSym.extent.startCh,
|
|
|
|
|
+ endLine: last.endLine, endCh: last.endCh,
|
|
|
|
|
+ };
|
|
|
|
|
+ unit.symbols.push(modSym);
|
|
|
|
|
+ return unit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseImportSeq(unit: M2Unit): void {
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ if (this.atKw('FROM')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ const m = this.expectIdent();
|
|
|
|
|
+ if (!m || !this.eatKw('IMPORT')) { this.syncDecl(); continue; }
|
|
|
|
|
+ const names = this.parseIdentList();
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+ if (m) unit.imports.push({ module: m.text, names, all: false });
|
|
|
|
|
+ } else if (this.atKw('IMPORT')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ const names = this.parseIdentList();
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+ for (const n of names) unit.imports.push({ module: n, names: [], all: true });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseIdentList(): string[] {
|
|
|
|
|
+ const names: string[] = [];
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ const t = this.expectIdent();
|
|
|
|
|
+ if (!t) break;
|
|
|
|
|
+ names.push(t.text);
|
|
|
|
|
+ if (!this.eatSym(',')) break;
|
|
|
|
|
+ }
|
|
|
|
|
+ return names;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Declarations up to BEGIN / END / FINALLY / EOF.
|
|
|
|
|
+ * In definition modules, procedures are headings without bodies. */
|
|
|
|
|
+ private parseDeclarations(parent: M2Symbol, unit: M2Unit, defOnly = false): void {
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ if (this.atKw('CONST')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.parseConstSection(parent);
|
|
|
|
|
+ } else if (this.atKw('TYPE')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.parseTypeSection(parent);
|
|
|
|
|
+ } else if (this.atKw('VAR')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.parseVarSection(parent, 'variable');
|
|
|
|
|
+ } else if (this.atKw('PROCEDURE')) {
|
|
|
|
|
+ const p = this.parseProcedure(unit, !defOnly);
|
|
|
|
|
+ if (p) parent.children.push(p);
|
|
|
|
|
+ else this.syncDecl();
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+ } else if (this.atKw('MODULE')) {
|
|
|
|
|
+ if (defOnly) { this.syncDecl(); continue; }
|
|
|
|
|
+ const m = this.parseLocalModule(unit);
|
|
|
|
|
+ if (m) parent.children.push(m);
|
|
|
|
|
+ else this.syncDecl();
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseConstSection(parent: M2Symbol): void {
|
|
|
|
|
+ while (this.peek().kind === 'ident') {
|
|
|
|
|
+ const nm = this.next();
|
|
|
|
|
+ if (!this.eatSym('=')) { this.syncDecl(); continue; }
|
|
|
|
|
+ const start = this.peek();
|
|
|
|
|
+ this.skipToSymAtDepth0(';');
|
|
|
|
|
+ const detail = `${nm.text} = ${this.rawText(start, this.prev()).trim()}`;
|
|
|
|
|
+ parent.children.push({
|
|
|
|
|
+ name: nm.text, kind: 'constant', detail,
|
|
|
|
|
+ nameRange: rangeOf(nm), extent: rangeOf(nm), children: [],
|
|
|
|
|
+ });
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseTypeSection(parent: M2Symbol): void {
|
|
|
|
|
+ while (this.peek().kind === 'ident') {
|
|
|
|
|
+ const nm = this.next();
|
|
|
|
|
+ if (this.eatSym('=')) {
|
|
|
|
|
+ const { text, fields } = this.captureType();
|
|
|
|
|
+ this.skipAlignment();
|
|
|
|
|
+ parent.children.push({
|
|
|
|
|
+ name: nm.text, kind: 'type', detail: `${nm.text} = ${text}`,
|
|
|
|
|
+ nameRange: rangeOf(nm), extent: rangeOf(nm), children: fields,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.eatSym(';')) this.syncDecl();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseVarSection(parent: M2Symbol, kind: 'variable' | 'field'): void {
|
|
|
|
|
+ while (this.peek().kind === 'ident') {
|
|
|
|
|
+ const names: Token[] = [];
|
|
|
|
|
+ const first = this.expectIdent();
|
|
|
|
|
+ if (!first) break;
|
|
|
|
|
+ names.push(first);
|
|
|
|
|
+ while (this.eatSym(',')) {
|
|
|
|
|
+ const n = this.expectIdent();
|
|
|
|
|
+ if (!n) break;
|
|
|
|
|
+ names.push(n);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.eatSym(':')) { this.syncDecl(); continue; }
|
|
|
|
|
+ const { text, fields } = this.captureType();
|
|
|
|
|
+ this.skipAlignment();
|
|
|
|
|
+ for (const n of names) {
|
|
|
|
|
+ const sym: M2Symbol = {
|
|
|
|
|
+ name: n.text, kind, detail: `${n.text} : ${text}`,
|
|
|
|
|
+ nameRange: rangeOf(n), extent: rangeOf(n), children: [],
|
|
|
|
|
+ };
|
|
|
|
|
+ // Attach record fields so `var.field` completion works best-effort.
|
|
|
|
|
+ for (const f of fields) sym.children.push({ ...f, children: [] });
|
|
|
|
|
+ parent.children.push(sym);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.eatSym(';')) { this.syncDecl(); break; }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Capture a type expression up to ';' at depth 0; collect RECORD fields. */
|
|
|
|
|
+ private captureType(): { text: string; fields: M2Symbol[] } {
|
|
|
|
|
+ const startIdx = this.pos;
|
|
|
|
|
+ const fields: M2Symbol[] = [];
|
|
|
|
|
+ let depth = 0; // ( [ {
|
|
|
|
|
+ let recDepth = 0; // RECORD..END nesting
|
|
|
|
|
+ let prevSignificant: Token | null = null;
|
|
|
|
|
+ const fieldCands: Token[] = [];
|
|
|
|
|
+ while (!this.atEof()) {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ if (t.kind === 'symbol') {
|
|
|
|
|
+ if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
|
|
|
|
|
+ else if (t.text === ')' || t.text === ']' || t.text === '}') depth = Math.max(0, depth - 1);
|
|
|
|
|
+ else if (t.text === ';' && depth === 0 && recDepth === 0) break;
|
|
|
|
|
+ } else if (t.kind === 'keyword') {
|
|
|
|
|
+ if (t.text === 'RECORD' && depth === 0) recDepth++;
|
|
|
|
|
+ else if (t.text === 'END' && depth === 0 && recDepth > 0) recDepth--;
|
|
|
|
|
+ else if ((t.text === 'CONST' || t.text === 'TYPE' || t.text === 'VAR' ||
|
|
|
|
|
+ t.text === 'PROCEDURE' || t.text === 'MODULE' || t.text === 'BEGIN' ||
|
|
|
|
|
+ t.text === 'FINALLY') && depth === 0 && recDepth === 0) break;
|
|
|
|
|
+ } else if (t.kind === 'ident' && recDepth > 0 && depth === 0) {
|
|
|
|
|
+ // Candidate record field: ident followed by ',' or ':' at field level.
|
|
|
|
|
+ const nx = this.peek(1);
|
|
|
|
|
+ const afterCase = prevSignificant !== null && prevSignificant.kind === 'keyword' &&
|
|
|
|
|
+ (prevSignificant.text === 'CASE' || prevSignificant.text === 'OF');
|
|
|
|
|
+ if (!afterCase && nx.kind === 'symbol' && (nx.text === ',' || nx.text === ':')) {
|
|
|
|
|
+ fieldCands.push(t);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (t.kind !== 'eof') prevSignificant = t;
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ // Promote candidates that precede ':' (name lists) to fields.
|
|
|
|
|
+ for (let k = 0; k < fieldCands.length; k++) {
|
|
|
|
|
+ const t = fieldCands[k];
|
|
|
|
|
+ fields.push({
|
|
|
|
|
+ name: t.text, kind: 'field', detail: t.text,
|
|
|
|
|
+ nameRange: rangeOf(t), extent: rangeOf(t), children: [],
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ const endTok = this.peek();
|
|
|
|
|
+ const text = this.rawRange(startIdx, this.pos).trim().replace(/\s+/g, ' ');
|
|
|
|
|
+ void endTok;
|
|
|
|
|
+ return { text, fields };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private skipAlignment(): void {
|
|
|
|
|
+ if (this.atSym('<*')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ let depth = 1;
|
|
|
|
|
+ while (!this.atEof() && depth > 0) {
|
|
|
|
|
+ if (this.atSym('<*')) depth++;
|
|
|
|
|
+ else if (this.atSym('*>')) depth--;
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseProcedure(unit: M2Unit, withBody = true): M2Symbol | null {
|
|
|
|
|
+ const kw = this.peek();
|
|
|
|
|
+ this.next(); // PROCEDURE
|
|
|
|
|
+ // GNU builtin attributes: __ATTRIBUTE__ __BUILTIN__ (( ident )) | __INLINE__
|
|
|
|
|
+ if (this.peek().text === '__ATTRIBUTE__') {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ if (this.peek().text === '__BUILTIN__') {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ if (this.atSym('(')) this.skipBalanced('(', ')');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (this.peek().text === '__INLINE__') {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ const nm = this.expectIdent();
|
|
|
|
|
+ if (!nm) return null;
|
|
|
|
|
+ const proc: M2Symbol = {
|
|
|
|
|
+ name: nm.text, kind: 'procedure', detail: `PROCEDURE ${nm.text}`,
|
|
|
|
|
+ nameRange: rangeOf(nm), extent: rangeOf(kw), children: [],
|
|
|
|
|
+ };
|
|
|
|
|
+ if (this.atSym('(')) this.parseFormalParams(proc, false);
|
|
|
|
|
+ else if (this.atSym('[')) this.parseFormalParams(proc, true);
|
|
|
|
|
+ if (this.atSym(':')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.eatSym('[');
|
|
|
|
|
+ const rt: string[] = [];
|
|
|
|
|
+ while (this.peek().kind === 'ident') {
|
|
|
|
|
+ rt.push(this.next().text);
|
|
|
|
|
+ if (!this.eatSym('.')) break;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.eatSym(']');
|
|
|
|
|
+ if (rt.length > 0) proc.detail += `: ${rt.join('.')}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.skipAlignment();
|
|
|
|
|
+ if (this.atSym(';')) this.next();
|
|
|
|
|
+ else { this.syncDecl(); }
|
|
|
|
|
+ if (!withBody) {
|
|
|
|
|
+ const endTok = this.prevTok();
|
|
|
|
|
+ proc.extent = {
|
|
|
|
|
+ startLine: proc.extent.startLine, startCh: proc.extent.startCh,
|
|
|
|
|
+ endLine: endTok.endLine, endCh: endTok.endCh,
|
|
|
|
|
+ };
|
|
|
|
|
+ return proc;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.parseBlock(proc, unit, false);
|
|
|
|
|
+ if (this.peek().kind === 'ident') this.next(); // closing name
|
|
|
|
|
+ const endTok = this.prevTok();
|
|
|
|
|
+ proc.extent = {
|
|
|
|
|
+ startLine: proc.extent.startLine, startCh: proc.extent.startCh,
|
|
|
|
|
+ endLine: endTok.endLine, endCh: endTok.endCh,
|
|
|
|
|
+ };
|
|
|
|
|
+ return proc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Formal parameter list; bracketed form holds GNU optional args. */
|
|
|
|
|
+ private parseFormalParams(proc: M2Symbol, bracket: boolean): void {
|
|
|
|
|
+ const open = bracket ? '[' : '(';
|
|
|
|
|
+ const close = bracket ? ']' : ')';
|
|
|
|
|
+ this.next(); // open
|
|
|
|
|
+ if (bracket && this.atSym(']')) { this.next(); return; }
|
|
|
|
|
+ if (!bracket && this.atSym(')')) { this.next(); return; }
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ if (this.atSym(close) || this.atEof()) { this.eatSym(close); return; }
|
|
|
|
|
+ if (this.atSym('...')) { this.next(); }
|
|
|
|
|
+ else if (this.peek().text === '...') { this.next(); }
|
|
|
|
|
+ else if (this.atKw('VAR')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.parseParamNames(proc, 'parameter');
|
|
|
|
|
+ } else if (this.atSym('[')) {
|
|
|
|
|
+ // GNU optional argument: [ name : type [= expr] ]
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ const n = this.expectIdent();
|
|
|
|
|
+ let dtype = '';
|
|
|
|
|
+ if (n && this.eatSym(':')) dtype = this.captureType().text;
|
|
|
|
|
+ if (this.atSym('=')) { this.next(); this.skipBalancedUpTo([']', ';']); }
|
|
|
|
|
+ if (n) {
|
|
|
|
|
+ proc.children.push({
|
|
|
|
|
+ name: n.text, kind: 'parameter',
|
|
|
|
|
+ detail: dtype ? `${n.text} : ${dtype} (optional)` : `${n.text} (optional)`,
|
|
|
|
|
+ nameRange: rangeOf(n), extent: rangeOf(n), children: [],
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ this.eatSym(']');
|
|
|
|
|
+ } else if (this.peek().kind === 'ident') {
|
|
|
|
|
+ this.parseParamNames(proc, 'parameter');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.atSym(';')) { this.next(); continue; }
|
|
|
|
|
+ if (this.atSym(close)) { this.next(); return; }
|
|
|
|
|
+ if (this.atSym(',')) { this.next(); continue; }
|
|
|
|
|
+ // Recovery inside parameter lists.
|
|
|
|
|
+ if (this.atKw('CONST') || this.atKw('TYPE') || this.atKw('VAR') ||
|
|
|
|
|
+ this.atKw('BEGIN') || this.atKw('END') || this.atEof()) return;
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseParamNames(proc: M2Symbol, kind: 'parameter'): void {
|
|
|
|
|
+ const names: Token[] = [];
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ const n = this.expectIdent();
|
|
|
|
|
+ if (!n) return;
|
|
|
|
|
+ names.push(n);
|
|
|
|
|
+ if (!this.eatSym(',')) break;
|
|
|
|
|
+ }
|
|
|
|
|
+ let dtype = '';
|
|
|
|
|
+ if (this.eatSym(':')) {
|
|
|
|
|
+ // FormalType: { ARRAY OF } Qualident
|
|
|
|
|
+ const parts: string[] = [];
|
|
|
|
|
+ while (this.atKw('ARRAY')) { parts.push('ARRAY'); this.next(); this.eatKw('OF'); parts.push('OF'); }
|
|
|
|
|
+ while (this.peek().kind === 'ident') {
|
|
|
|
|
+ parts.push(this.next().text);
|
|
|
|
|
+ if (!this.eatSym('.')) break;
|
|
|
|
|
+ }
|
|
|
|
|
+ dtype = parts.join(' ');
|
|
|
|
|
+ }
|
|
|
|
|
+ this.skipAlignment();
|
|
|
|
|
+ for (const n of names) {
|
|
|
|
|
+ proc.children.push({
|
|
|
|
|
+ name: n.text, kind, detail: dtype ? `${n.text} : ${dtype}` : n.text,
|
|
|
|
|
+ nameRange: rangeOf(n), extent: rangeOf(n), children: [],
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseLocalModule(unit: M2Unit): M2Symbol | null {
|
|
|
|
|
+ const kw = this.peek();
|
|
|
|
|
+ this.next(); // MODULE
|
|
|
|
|
+ const nm = this.expectIdent();
|
|
|
|
|
+ if (!nm) return null;
|
|
|
|
|
+ const mod: M2Symbol = {
|
|
|
|
|
+ name: nm.text, kind: 'module', detail: `MODULE ${nm.text}`,
|
|
|
|
|
+ nameRange: rangeOf(nm), extent: rangeOf(kw), children: [],
|
|
|
|
|
+ };
|
|
|
|
|
+ if (this.atSym('[')) this.skipBalanced('[', ']');
|
|
|
|
|
+ this.eatSym(';');
|
|
|
|
|
+ this.parseImportSeq(unit);
|
|
|
|
|
+ if (this.atKw('EXPORT')) this.skipToSym(';');
|
|
|
|
|
+ this.parseBlock(mod, unit, true);
|
|
|
|
|
+ if (this.peek().kind === 'ident') this.next();
|
|
|
|
|
+ const endTok = this.prevTok();
|
|
|
|
|
+ mod.extent = {
|
|
|
|
|
+ startLine: mod.extent.startLine, startCh: mod.extent.startCh,
|
|
|
|
|
+ endLine: endTok.endLine, endCh: endTok.endCh,
|
|
|
|
|
+ };
|
|
|
|
|
+ return mod;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Block = { declarations } [BEGIN body] [FINALLY body] END. */
|
|
|
|
|
+ private parseBlock(parent: M2Symbol, unit: M2Unit, _isModule: boolean): void {
|
|
|
|
|
+ this.parseDeclarations(parent, unit);
|
|
|
|
|
+ if (this.atKw('BEGIN')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.skipToMatchingEnd();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.atKw('FINALLY')) {
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.skipToMatchingEnd();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.atKw('EXCEPT')) { // tolerate EXCEPT part inside body skipping
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ this.skipToMatchingEnd();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.eatKw('END');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Skip until the END matching an already-consumed opener. */
|
|
|
|
|
+ private skipToMatchingEnd(): void {
|
|
|
|
|
+ let depth = 1;
|
|
|
|
|
+ while (!this.atEof() && depth > 0) {
|
|
|
|
|
+ const t = this.next();
|
|
|
|
|
+ if (t.kind === 'keyword' && OPENERS.has(t.text)) depth++;
|
|
|
|
|
+ else if (t.kind === 'keyword' && t.text === 'END') depth--;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private skipBalanced(open: string, close: string): void {
|
|
|
|
|
+ if (!this.eatSym(open)) return;
|
|
|
|
|
+ let depth = 1;
|
|
|
|
|
+ while (!this.atEof() && depth > 0) {
|
|
|
|
|
+ if (this.atSym(open)) depth++;
|
|
|
|
|
+ else if (this.atSym(close)) depth--;
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private skipBalancedUpTo(stops: string[]): void {
|
|
|
|
|
+ let depth = 0;
|
|
|
|
|
+ while (!this.atEof()) {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ if (t.kind === 'symbol') {
|
|
|
|
|
+ if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
|
|
|
|
|
+ else if (t.text === ')' || t.text === ']' || t.text === '}') {
|
|
|
|
|
+ if (depth === 0 && stops.includes(t.text)) return;
|
|
|
|
|
+ depth = Math.max(0, depth - 1);
|
|
|
|
|
+ } else if (depth === 0 && stops.includes(t.text)) return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private skipToSym(sym: string): void {
|
|
|
|
|
+ while (!this.atEof() && !this.atSym(sym)) this.next();
|
|
|
|
|
+ this.eatSym(sym);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private skipToSymAtDepth0(sym: string): void {
|
|
|
|
|
+ let depth = 0;
|
|
|
|
|
+ while (!this.atEof()) {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ if (t.kind === 'symbol') {
|
|
|
|
|
+ if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
|
|
|
|
|
+ else if (t.text === ')' || t.text === ']' || t.text === '}') depth = Math.max(0, depth - 1);
|
|
|
|
|
+ else if (t.text === sym && depth === 0) return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Skip to a plausible declaration boundary. */
|
|
|
|
|
+ private syncDecl(): void {
|
|
|
|
|
+ while (!this.atEof()) {
|
|
|
|
|
+ const t = this.peek();
|
|
|
|
|
+ if (t.kind === 'symbol' && t.text === ';') { this.next(); return; }
|
|
|
|
|
+ if (t.kind === 'keyword' && SECTION.has(t.text)) return;
|
|
|
|
|
+ this.next();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private atEof(): boolean {
|
|
|
|
|
+ return this.peek().kind === 'eof';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private prevTok(): Token {
|
|
|
|
|
+ return this.tokens[Math.max(0, Math.min(this.pos - 1, this.tokens.length - 1))];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private prev(): Token {
|
|
|
|
|
+ return this.prevTok();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private rawRange(from: number, to: number): string {
|
|
|
|
|
+ const toks = this.tokens.slice(from, to).filter(t => t.kind !== 'eof');
|
|
|
|
|
+ if (toks.length === 0) return '';
|
|
|
|
|
+ return this.text.slice(toks[0].offset, toks[toks.length - 1].endOffset);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private rawText(_a: Token, _b: Token): string {
|
|
|
|
|
+ return this.text.slice(_a.offset, _b.endOffset);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function parseUnitText(text: string, filePath: string): M2Unit {
|
|
|
|
|
+ const parser = new Parser(lex(text), text);
|
|
|
|
|
+ return parser.parseUnit(filePath);
|
|
|
|
|
+}
|