| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- /** Local Pascal symbol model for Outline, completion and hover.
- *
- * Conservative by design: only declaration shapes recognised with full
- * confidence are emitted (misses are acceptable, wrong entries are not).
- * Bodies are skipped with depth counting, so statement-level noise can
- * never leak into the symbol table. Scopes: unit/program globals plus one
- * routine level (params and locals); deeper nesting folds into its owner.
- */
- export interface PasSymbol {
- name: string;
- kind: 'procedure' | 'function' | 'constant' | 'type' | 'variable' | 'parameter' | 'field' | 'module';
- detail: string;
- line: number; ch: number; endLine: number; endCh: number;
- children: PasSymbol[];
- /** Last line covered by this scope (routines/modules), if known. */
- scopeEndLine?: number;
- scopeEndCh?: number;
- }
- export interface Tok {
- text: string;
- upper: string;
- line: number; ch: number; endLine: number; endCh: number;
- }
- const WS = /\s/;
- /** Strip comments and string literals (length-preserving) so the
- * declaration scan never sees keywords inside them. Exported for the
- * resolver's occurrence scan, which needs identical masking. */
- export function mask(text: string): string {
- const out = text.split('');
- const n = text.length;
- let i = 0;
- const blank = (a: number, b: number) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
- while (i < n) {
- const c = text[i];
- const nx = i + 1 < n ? text[i + 1] : '';
- if (c === '(' && nx === '*') {
- let depth = 1;
- let j = i + 2;
- while (j < n && depth > 0) {
- if (text[j] === '(' && text[j + 1] === '*') { depth++; j += 2; }
- else if (text[j] === '*' && text[j + 1] === ')') { depth--; j += 2; }
- else j++;
- }
- blank(i, j);
- i = j;
- } else if (c === '{') {
- let j = i + 1;
- while (j < n && text[j] !== '}') j++;
- blank(i, Math.min(j + 1, n));
- i = Math.min(j + 1, n);
- } else if (c === "'") {
- let j = i + 1;
- while (j < n) {
- if (text[j] === "'") {
- if (text[j + 1] === "'") j += 2;
- else { j++; break; }
- } else j++;
- }
- blank(i, j);
- i = j;
- } else {
- i++;
- }
- }
- return out.join('');
- }
- export function lex(masked: string): Tok[] {
- const toks: Tok[] = [];
- const re = /[A-Za-z_][A-Za-z0-9_]*|\d+(?:\.\d+)?(?:[Ee][+-]?\d+)?|:=|\.\.|<=|>=|<>|[^\sA-Za-z0-9_]/g;
- let m: RegExpExecArray | null;
- // Incremental line tracking: the previous version re-sliced from the
- // file start per token (O(n^2)), which stalls on ~800KB sources.
- let line = 0;
- let prevEnd = 0;
- while ((m = re.exec(masked)) !== null) {
- for (let k = prevEnd; k < m.index; k++) {
- if (masked[k] === '\n') line++;
- }
- const ch = m.index - (masked.lastIndexOf('\n', m.index - 1) + 1);
- const mid = m[0].split('\n');
- toks.push({
- text: m[0], upper: m[0].toUpperCase(),
- line, ch,
- endLine: line + mid.length - 1,
- endCh: mid.length > 1 ? mid[mid.length - 1].length : ch + m[0].length
- });
- line += mid.length - 1;
- prevEnd = m.index + m[0].length;
- }
- return toks;
- }
- export const isIdent = (t: Tok) => /^[A-Za-z_]/.test(t.text);
- class Parser {
- pos = 0;
- constructor(readonly toks: Tok[]) {}
- peek(off = 0): Tok | null {
- return this.pos + off < this.toks.length ? this.toks[this.pos + off] : null;
- }
- at(word: string): boolean {
- const t = this.peek();
- return !!t && t.upper === word;
- }
- next(): Tok {
- return this.toks[this.pos++];
- }
- expect(word: string): boolean {
- if (this.at(word)) { this.next(); return true; }
- return false;
- }
- /** Skip a parenthesis/bracket group, returns true when balanced. */
- skipGroup(open: string, close: string): boolean {
- if (!this.at(open)) return false;
- let depth = 0;
- while (this.pos < this.toks.length) {
- const t = this.next();
- if (t.upper === open) depth++;
- else if (t.upper === close) {
- depth--;
- if (depth === 0) return true;
- }
- }
- return false;
- }
- /** Skip a routine body: declarations then an optional compound.
- * Only section words skipSection() accepts are delegated to it;
- * anything else (notably a nested PROCEDURE/FUNCTION, which the
- * caller parses itself) ends the skip so the loop always progresses. */
- skipBody(name: string): void {
- for (;;) {
- const t = this.peek();
- if (!t) return;
- if (t.upper === 'BEGIN') {
- this.skipCompound();
- // trailing name after END (Oberon families)
- if (this.at('END')) { this.next(); if (this.peek()?.upper !== '.' && /^[A-Za-z_]/.test(this.peek()?.text ?? '')) this.next(); }
- return;
- }
- if (['CONST', 'TYPE', 'VAR', 'LABEL'].includes(t.upper)) {
- this.skipSection();
- continue;
- }
- return;
- }
- }
- skipCompound(): void {
- if (!this.at('BEGIN')) return;
- this.next();
- let depth = 1;
- while (this.pos < this.toks.length && depth > 0) {
- const t = this.next();
- if (t.upper === 'BEGIN' || t.upper === 'CASE' || t.upper === 'RECORD') depth++;
- else if (t.upper === 'END') depth--;
- }
- }
- /** Skip one declaration section (const/type/var/label). */
- skipSection(): void {
- const kw = this.peek()?.upper;
- if (kw !== 'CONST' && kw !== 'TYPE' && kw !== 'VAR' && kw !== 'LABEL') return;
- this.next();
- // items separated by ';' until a section starter, routine, BEGIN or END
- for (;;) {
- const t = this.peek();
- if (!t) return;
- if (['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END', 'IMPLEMENTATION', 'INTERFACE', 'FINALIZATION', 'INITIALIZATION'].includes(t.upper)) return;
- this.skipDeclItem();
- if (this.at(';')) this.next();
- else return;
- }
- }
- /** Skip one `name [, ...] ...` declaration item up to `;` at depth 0. */
- skipDeclItem(): void {
- let depth = 0;
- while (this.pos < this.toks.length) {
- const t = this.peek()!;
- if (t.upper === '(' || t.upper === '[') depth++;
- else if (t.upper === ')' || t.upper === ']') depth--;
- else if (t.upper === 'RECORD') depth++;
- else if (t.upper === 'END' && depth > 0) depth--;
- else if (depth === 0 && (t.upper === ';' || ['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(t.upper))) return;
- this.next();
- }
- }
- /** Parse an identifier list, returning the name tokens. */
- nameList(): Tok[] {
- const out: Tok[] = [];
- for (;;) {
- const t = this.peek();
- if (!t || !isIdent(t)) return out;
- out.push(this.next());
- if (this.at(',')) { this.next(); continue; }
- return out;
- }
- }
- }
- /** Parse a `RECORD ... END` field list; RECORD already consumed.
- *
- * Only plain `name [, ...] : type;` items at depth 1 become fields.
- * Variant parts (CASE), nested records and anything unrecognised are
- * skipped without emitting (misses acceptable, wrong entries are not).
- * The closing END is consumed. */
- export function parseRecordFields(p: Parser): PasSymbol[] {
- const fields: PasSymbol[] = [];
- let depth = 1;
- for (;;) {
- const t = p.peek();
- if (!t) return fields;
- if (t.upper === 'RECORD') { depth++; p.next(); continue; }
- if (t.upper === 'END') {
- depth--;
- p.next();
- if (depth <= 0) return fields;
- continue;
- }
- if (depth !== 1) { p.next(); continue; }
- if (!isIdent(t)) {
- p.skipDeclItem();
- if (p.at(';')) p.next();
- else if (p.peek() && p.peek()!.upper !== 'END') p.next();
- continue;
- }
- if (['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN'].includes(t.upper)) {
- // Lost sync: skip to the matching END so the caller stays aligned.
- while (p.pos < p.toks.length && depth > 0) {
- const k = p.next();
- if (k.upper === 'RECORD') depth++;
- else if (k.upper === 'END') depth--;
- }
- return fields;
- }
- const names = p.nameList();
- if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
- p.next();
- const typeToks: string[] = [];
- let dd = 0;
- while (p.pos < p.toks.length) {
- const k = p.peek()!;
- if (k.upper === '(' || k.upper === '[') dd++;
- else if (k.upper === ')' || k.upper === ']') dd--;
- else if (k.upper === 'RECORD') dd++;
- else if (k.upper === 'END' && dd > 0) dd--;
- else if (dd === 0 && (k.upper === ';' || k.upper === 'END')) break;
- else if (dd === 0 && ['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'FUNCTION', 'BEGIN'].includes(k.upper)) break;
- typeToks.push(k.text);
- p.next();
- }
- const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
- for (const nm of names) {
- fields.push({
- name: nm.text, kind: 'field', detail: `${nm.text}: ${typeText}`,
- line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: []
- });
- }
- if (p.at(';')) p.next();
- }
- }
- /** Parse an enumeration literal list `(A, B, ...)`; `(` already consumed.
- * Returns the literal names with their tokens. Stops at `)` (consumed)
- * or on anything unparseable (partial list kept, never throws). */
- export function parseEnumLiterals(p: Parser): Tok[] {
- const out: Tok[] = [];
- for (;;) {
- const e = p.peek();
- if (!e) return out;
- if (e.upper === ')') { p.next(); return out; }
- if (e.upper === ';') return out;
- if (isIdent(e)) {
- out.push(p.next());
- const nx = p.peek();
- if (nx && (nx.upper === '=' || nx.upper === ':=')) {
- // Explicit ordinal (some dialects): skip value to ',' or ')'.
- p.next();
- let dd = 0;
- while (p.pos < p.toks.length) {
- const q = p.peek()!;
- if (q.upper === '(') dd++;
- else if (q.upper === ')') {
- if (dd === 0) break;
- dd--;
- }
- else if (dd === 0 && (q.upper === ',' || q.upper === ';')) break;
- p.next();
- }
- }
- if (p.at(',')) { p.next(); continue; }
- continue;
- }
- p.next();
- }
- }
- /** A `USES` clause unit name with its position (for go-to-unit). */
- export interface PasUse {
- name: string;
- line: number; ch: number; endCh: number;
- }
- /** All `USES a, b, ...;` unit names at paren-depth 0.
- *
- * `IN 'file'` segments are skipped (string contents are masked away,
- * so IN is followed by `,`/`;`/ident — everything up to the next `,`
- * or `;` is ignored). A USES not followed by an identifier, or one
- * without a terminating `;`, yields nothing. */
- export function parseUses(text: string): PasUse[] {
- const toks = lex(mask(text));
- const out: PasUse[] = [];
- let depth = 0;
- for (let k = 0; k < toks.length; k++) {
- const t = toks[k];
- if (t.upper === '(') { depth++; continue; }
- if (t.upper === ')') { depth--; continue; }
- if (depth !== 0 || t.upper !== 'USES') continue;
- const nx = toks[k + 1];
- if (!nx || !isIdent(nx)) continue;
- let j = k + 1;
- let ok = false;
- for (;;) {
- const u = toks[j];
- if (!u) break;
- if (u.upper === ';') { ok = true; break; }
- if (u.upper === 'BEGIN' || u.upper === 'END' || u.upper === 'IMPLEMENTATION' ||
- u.upper === 'PROCEDURE' || u.upper === 'FUNCTION') break;
- if (u.upper === 'IN') {
- j++;
- while (j < toks.length && toks[j].upper !== ',' && toks[j].upper !== ';') j++;
- continue;
- }
- if (isIdent(u)) {
- // Must be `name`, `name,`, or `name;` — anything else aborts.
- const after = toks[j + 1];
- if (!after || (after.upper !== ',' && after.upper !== ';' && after.upper !== 'IN')) break;
- out.push({ name: u.text, line: u.line, ch: u.ch, endCh: u.endCh });
- } else if (u.upper !== ',') {
- break;
- }
- j++;
- }
- if (ok) k = j;
- }
- return out;
- }
- /** Parse the declarations of one scope; stops before BEGIN/END/EOF. */
- function parseScope(p: Parser, out: PasSymbol[], stopAtBegin: boolean): void {
- for (;;) {
- const t = p.peek();
- if (!t) return;
- const u = t.upper;
- if (u === 'BEGIN') {
- if (stopAtBegin) return;
- p.skipBody('');
- return;
- }
- if (u === 'END') return;
- if (u === 'INTERFACE' || u === 'IMPLEMENTATION') { p.next(); continue; }
- if (u === 'INITIALIZATION' || u === 'FINALIZATION') {
- // Statement section: skip to its END so no statement noise leaks in.
- p.next();
- let depth = 0;
- while (p.pos < p.toks.length) {
- const k = p.next();
- if (k.upper === 'BEGIN' || k.upper === 'CASE' || k.upper === 'RECORD') depth++;
- else if (k.upper === 'END') {
- if (depth === 0) break;
- depth--;
- }
- }
- continue;
- }
- if (u === 'LABEL') { p.skipSection(); continue; }
- if (u === 'CONST') {
- p.next();
- for (;;) {
- const n = p.peek();
- if (!n || !isIdent(n)) break;
- if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
- const nm = p.next();
- if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
- p.next();
- // value: skip to ';' at depth 0
- p.skipDeclItem();
- out.push({ name: nm.text, kind: 'constant', detail: `CONST ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
- if (p.at(';')) p.next();
- else break;
- }
- continue;
- }
- if (u === 'TYPE') {
- p.next();
- for (;;) {
- const n = p.peek();
- if (!n || !isIdent(n)) break;
- if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
- const nm = p.next();
- if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
- p.next();
- const typeChildren: PasSymbol[] = [];
- const packedRec = p.at('PACKED') && p.peek(1)?.upper === 'RECORD';
- if (packedRec) p.next();
- if (p.at('RECORD')) {
- p.next();
- typeChildren.push(...parseRecordFields(p));
- } else if (p.at('(')) {
- p.next();
- for (const lit of parseEnumLiterals(p)) {
- typeChildren.push({
- name: lit.text, kind: 'constant', detail: `${nm.text}.${lit.text}`,
- line: lit.line, ch: lit.ch, endLine: lit.endLine, endCh: lit.endCh, children: []
- });
- }
- } else {
- p.skipDeclItem();
- }
- out.push({ name: nm.text, kind: 'type', detail: `TYPE ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: typeChildren });
- if (p.at(';')) p.next();
- else break;
- }
- continue;
- }
- if (u === 'VAR' || u === 'THREADVAR') {
- p.next();
- for (;;) {
- const n = p.peek();
- if (!n || !isIdent(n)) break;
- if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
- const names = p.nameList();
- if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
- p.next();
- // Inline `RECORD ... END` type: fields become the variable's children.
- const packedInline = p.at('PACKED') && p.peek(1)?.upper === 'RECORD';
- if (packedInline) p.next();
- if (p.at('RECORD')) {
- p.next();
- const fields = parseRecordFields(p);
- for (const nm of names) {
- out.push({ name: nm.text, kind: 'variable', detail: `VAR ${nm.text}: RECORD`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: fields });
- }
- if (p.at(';')) p.next();
- else break;
- continue;
- }
- // capture type text up to ';' or '=' (initializer) at depth 0
- const typeToks: string[] = [];
- let depth = 0;
- while (p.pos < p.toks.length) {
- const k = p.peek()!;
- if (k.upper === '(' || k.upper === '[') depth++;
- else if (k.upper === ')' || k.upper === ']') depth--;
- else if (k.upper === 'RECORD') depth++;
- else if (k.upper === 'END' && depth > 0) depth--;
- else if (depth === 0 && (k.upper === ';' || k.upper === '=')) break;
- else if (depth === 0 && ['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(k.upper)) break;
- typeToks.push(k.text);
- p.next();
- }
- const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
- if (p.at('=')) { p.next(); p.skipDeclItem(); }
- for (const nm of names) {
- out.push({ name: nm.text, kind: 'variable', detail: `VAR ${nm.text}: ${typeText}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
- }
- if (p.at(';')) p.next();
- else break;
- }
- continue;
- }
- if (u === 'PROCEDURE' || u === 'FUNCTION') {
- const isFunc = u === 'FUNCTION';
- const kw = p.next();
- // optional generic prefix / class / receiver: find the routine name.
- // Name = last ident before the formal list, result type or ';'.
- // Stops at '(' (formals parsed separately below); '<...>' type
- // parameter lists are skipped via angle depth.
- let depth = 0;
- let adepth = 0;
- let nameTok: Tok | null = null;
- let lastIdent: Tok | null = null;
- while (p.pos < p.toks.length) {
- const k = p.peek()!;
- if (k.upper === '<') adepth++;
- else if (k.upper === '>') adepth--;
- else if (k.upper === '(' && adepth === 0) break;
- else if (k.upper === '(') depth++;
- else if (k.upper === ')') depth--;
- else if (depth === 0 && adepth === 0 && (k.upper === ';' || k.upper === ':')) break;
- else if (depth === 0 && adepth === 0 && k.upper === '.') { lastIdent = null; }
- else if (depth === 0 && adepth === 0 && isIdent(k)) lastIdent = k;
- if (depth === 0 && adepth === 0 && ['BEGIN', 'END', 'CONST', 'TYPE', 'VAR', 'PROCEDURE', 'FUNCTION'].includes(k.upper)) break;
- p.next();
- }
- nameTok = lastIdent;
- // formal params
- const params: PasSymbol[] = [];
- if (p.at('(')) {
- p.next();
- let pd = 1;
- const pnames: Tok[] = [];
- let ptype: string[] = [];
- let inType = false;
- const flush = () => {
- const tt = ptype.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
- for (const nm of pnames) {
- params.push({ name: nm.text, kind: 'parameter', detail: `${nm.text}: ${tt}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
- }
- pnames.length = 0; ptype = []; inType = false;
- };
- while (p.pos < p.toks.length && pd > 0) {
- const k = p.next();
- if (k.upper === '(') { pd++; if (inType) ptype.push(k.text); continue; }
- if (k.upper === ')') { pd--; if (pd === 0) { if (pnames.length || ptype.length) flush(); break; } if (inType) ptype.push(k.text); continue; }
- if (pd !== 1) { if (inType) ptype.push(k.text); continue; }
- if (k.upper === ';') { flush(); continue; }
- if (k.upper === ':' && !inType) { inType = true; continue; }
- if (k.upper === ',') continue;
- if (k.upper === '=' && inType) {
- // default value: skip to ';' or ')' at depth 1
- let dd = 1;
- while (p.pos < p.toks.length && dd > 0) {
- const q = p.next();
- if (q.upper === '(') dd++;
- else if (q.upper === ')') { dd--; if (dd === 0) { pd--; if (pnames.length || ptype.length) flush(); break; } }
- else if (q.upper === ';' && dd === 1) { flush(); break; }
- }
- continue;
- }
- if (['VAR', 'OUT', 'CONST'].includes(k.upper) && !inType && pnames.length === 0) continue;
- if (isIdent(k)) { if (inType) ptype.push(k.text); else pnames.push(k); }
- else if (inType) ptype.push(k.text);
- }
- }
- // result type
- let restype = '';
- if (p.at(':')) {
- p.next();
- const rt: string[] = [];
- while (p.pos < p.toks.length) {
- const k = p.peek()!;
- if (k.upper === ';') break;
- rt.push(k.text);
- p.next();
- }
- restype = rt.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
- }
- if (!nameTok) {
- // unrecognised header: skip one item and continue (never emit)
- p.skipDeclItem();
- if (p.at(';')) p.next();
- continue;
- }
- const sym: PasSymbol = {
- name: nameTok.text,
- kind: isFunc ? 'function' : 'procedure',
- detail: `${isFunc ? 'FUNCTION' : 'PROCEDURE'} ${nameTok.text}${restype ? ': ' + restype : ''}`,
- line: kw.line, ch: kw.ch, endLine: nameTok.endLine, endCh: nameTok.endCh,
- children: params
- };
- out.push(sym);
- if (p.at(';')) p.next();
- // directives or body: skip directives (words + ';') then body
- for (;;) {
- const k = p.peek();
- if (!k) break;
- if (k.upper === 'INTERFACE' || k.upper === 'IMPLEMENTATION') { p.next(); continue; }
- if (k.upper === 'BEGIN') { p.skipBody(nameTok.text); break; }
- if (k.upper === 'FORWARD' || k.upper === 'EXTERNAL') {
- // bodyless: consume through ';'
- while (p.pos < p.toks.length && !p.at(';') && !['BEGIN', 'END', 'PROCEDURE', 'FUNCTION', 'CONST', 'TYPE', 'VAR'].includes(p.peek()!.upper)) p.next();
- if (p.at(';')) p.next();
- break;
- }
- if (k.upper === 'END' || k.upper === 'PROCEDURE' || k.upper === 'FUNCTION') break;
- // nested declarations belong to this routine: parse one level, then body
- if (['CONST', 'TYPE', 'VAR', 'LABEL'].includes(k.upper)) {
- parseScope(p, sym.children, true);
- continue;
- }
- // unknown directive word: skip to ';' (covers calling conventions)
- if (isIdent(k)) {
- p.next();
- if (p.at(';')) { p.next(); continue; }
- // not a lone directive: treat rest as body
- p.skipBody(nameTok.text);
- break;
- }
- break;
- }
- const tail = p.toks[p.pos - 1];
- if (tail) { sym.scopeEndLine = tail.endLine; sym.scopeEndCh = tail.endCh; }
- continue;
- }
- // anything else at scope level: skip one item to stay in sync
- p.skipDeclItem();
- if (p.at(';')) p.next();
- else if (p.pos < p.toks.length) p.next();
- }
- }
- /** Entry: top-level symbols of a Pascal unit/program. */
- export function parsePascal(text: string): PasSymbol[] {
- const toks = lex(mask(text));
- const p = new Parser(toks);
- // skip program/unit/library header up to the first ';' at depth 0
- const first = p.peek();
- if (first && ['PROGRAM', 'UNIT', 'LIBRARY'].includes(first.upper)) {
- p.next();
- let depth = 0;
- while (p.pos < p.toks.length) {
- const t = p.next();
- if (t.upper === '(') depth++;
- else if (t.upper === ')') depth--;
- else if (t.upper === ';' && depth === 0) break;
- }
- }
- const out: PasSymbol[] = [];
- parseScope(p, out, false);
- // module wrapper for outline friendliness when a header was seen
- if (first && ['PROGRAM', 'UNIT', 'LIBRARY'].includes(first.upper)) {
- const nm = p.toks.length > 1 ? p.toks[1] : null;
- if (nm && /^[A-Za-z_]/.test(nm.text)) {
- const last = p.toks[p.toks.length - 1];
- const mod: PasSymbol = { name: nm.text, kind: 'module', detail: `${first.text.toUpperCase()} ${nm.text}`, line: first.line, ch: first.ch, endLine: nm.endLine, endCh: nm.endCh, children: out };
- if (last) { mod.scopeEndLine = last.endLine; mod.scopeEndCh = last.endCh; }
- return [mod];
- }
- }
- return out;
- }
- /** True when (line, ch) lies within the symbol's scope, if known. */
- function inScope(s: PasSymbol, line: number, ch: number): boolean {
- if (s.scopeEndLine === undefined) return true;
- if (line < s.line) return false;
- if (line > s.scopeEndLine) return false;
- if (line === s.scopeEndLine && s.scopeEndCh !== undefined && ch > s.scopeEndCh) return false;
- return true;
- }
- /** Symbols visible at a position: globals plus the innermost enclosing
- * routine's params and locals. Forgiving by design (completion-only). */
- export function visibleAt(symbols: PasSymbol[], line: number, ch: number): PasSymbol[] {
- const top: PasSymbol[] = [];
- for (const s of symbols) {
- if (s.kind === 'module') top.push(...s.children);
- else top.push(s);
- }
- const result: PasSymbol[] = [];
- for (const s of top) {
- if (s.kind === 'constant' || s.kind === 'type' || s.kind === 'variable') result.push(s);
- }
- // innermost enclosing routine first
- const routines = top.filter(s => (s.kind === 'procedure' || s.kind === 'function') && inScope(s, line, ch));
- routines.sort((a, b) => (b.line - a.line) || ((b.ch ?? 0) - (a.ch ?? 0)));
- for (const r of routines) {
- result.push(...r.children);
- result.push(r);
- }
- // top-level routines are callable throughout the unit
- for (const s of top) {
- if ((s.kind === 'procedure' || s.kind === 'function') && !result.includes(s)) result.push(s);
- }
- return result;
- }
|