pascalSymbols.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. /** Local Pascal symbol model for Outline, completion and hover.
  2. *
  3. * Conservative by design: only declaration shapes recognised with full
  4. * confidence are emitted (misses are acceptable, wrong entries are not).
  5. * Bodies are skipped with depth counting, so statement-level noise can
  6. * never leak into the symbol table. Scopes: unit/program globals plus one
  7. * routine level (params and locals); deeper nesting folds into its owner.
  8. */
  9. export interface PasSymbol {
  10. name: string;
  11. kind: 'procedure' | 'function' | 'constant' | 'type' | 'variable' | 'parameter' | 'field' | 'module';
  12. detail: string;
  13. line: number; ch: number; endLine: number; endCh: number;
  14. children: PasSymbol[];
  15. /** Last line covered by this scope (routines/modules), if known. */
  16. scopeEndLine?: number;
  17. scopeEndCh?: number;
  18. }
  19. export interface Tok {
  20. text: string;
  21. upper: string;
  22. line: number; ch: number; endLine: number; endCh: number;
  23. }
  24. const WS = /\s/;
  25. /** Strip comments and string literals (length-preserving) so the
  26. * declaration scan never sees keywords inside them. Exported for the
  27. * resolver's occurrence scan, which needs identical masking. */
  28. export function mask(text: string): string {
  29. const out = text.split('');
  30. const n = text.length;
  31. let i = 0;
  32. const blank = (a: number, b: number) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
  33. while (i < n) {
  34. const c = text[i];
  35. const nx = i + 1 < n ? text[i + 1] : '';
  36. if (c === '(' && nx === '*') {
  37. let depth = 1;
  38. let j = i + 2;
  39. while (j < n && depth > 0) {
  40. if (text[j] === '(' && text[j + 1] === '*') { depth++; j += 2; }
  41. else if (text[j] === '*' && text[j + 1] === ')') { depth--; j += 2; }
  42. else j++;
  43. }
  44. blank(i, j);
  45. i = j;
  46. } else if (c === '{') {
  47. let j = i + 1;
  48. while (j < n && text[j] !== '}') j++;
  49. blank(i, Math.min(j + 1, n));
  50. i = Math.min(j + 1, n);
  51. } else if (c === "'") {
  52. let j = i + 1;
  53. while (j < n) {
  54. if (text[j] === "'") {
  55. if (text[j + 1] === "'") j += 2;
  56. else { j++; break; }
  57. } else j++;
  58. }
  59. blank(i, j);
  60. i = j;
  61. } else {
  62. i++;
  63. }
  64. }
  65. return out.join('');
  66. }
  67. export function lex(masked: string): Tok[] {
  68. const toks: Tok[] = [];
  69. const re = /[A-Za-z_][A-Za-z0-9_]*|\d+(?:\.\d+)?(?:[Ee][+-]?\d+)?|:=|\.\.|<=|>=|<>|[^\sA-Za-z0-9_]/g;
  70. let m: RegExpExecArray | null;
  71. // Incremental line tracking: the previous version re-sliced from the
  72. // file start per token (O(n^2)), which stalls on ~800KB sources.
  73. let line = 0;
  74. let prevEnd = 0;
  75. while ((m = re.exec(masked)) !== null) {
  76. for (let k = prevEnd; k < m.index; k++) {
  77. if (masked[k] === '\n') line++;
  78. }
  79. const ch = m.index - (masked.lastIndexOf('\n', m.index - 1) + 1);
  80. const mid = m[0].split('\n');
  81. toks.push({
  82. text: m[0], upper: m[0].toUpperCase(),
  83. line, ch,
  84. endLine: line + mid.length - 1,
  85. endCh: mid.length > 1 ? mid[mid.length - 1].length : ch + m[0].length
  86. });
  87. line += mid.length - 1;
  88. prevEnd = m.index + m[0].length;
  89. }
  90. return toks;
  91. }
  92. export const isIdent = (t: Tok) => /^[A-Za-z_]/.test(t.text);
  93. class Parser {
  94. pos = 0;
  95. constructor(readonly toks: Tok[]) {}
  96. peek(off = 0): Tok | null {
  97. return this.pos + off < this.toks.length ? this.toks[this.pos + off] : null;
  98. }
  99. at(word: string): boolean {
  100. const t = this.peek();
  101. return !!t && t.upper === word;
  102. }
  103. next(): Tok {
  104. return this.toks[this.pos++];
  105. }
  106. expect(word: string): boolean {
  107. if (this.at(word)) { this.next(); return true; }
  108. return false;
  109. }
  110. /** Skip a parenthesis/bracket group, returns true when balanced. */
  111. skipGroup(open: string, close: string): boolean {
  112. if (!this.at(open)) return false;
  113. let depth = 0;
  114. while (this.pos < this.toks.length) {
  115. const t = this.next();
  116. if (t.upper === open) depth++;
  117. else if (t.upper === close) {
  118. depth--;
  119. if (depth === 0) return true;
  120. }
  121. }
  122. return false;
  123. }
  124. /** Skip a routine body: declarations then an optional compound.
  125. * Only section words skipSection() accepts are delegated to it;
  126. * anything else (notably a nested PROCEDURE/FUNCTION, which the
  127. * caller parses itself) ends the skip so the loop always progresses. */
  128. skipBody(name: string): void {
  129. for (;;) {
  130. const t = this.peek();
  131. if (!t) return;
  132. if (t.upper === 'BEGIN') {
  133. this.skipCompound();
  134. // trailing name after END (Oberon families)
  135. if (this.at('END')) { this.next(); if (this.peek()?.upper !== '.' && /^[A-Za-z_]/.test(this.peek()?.text ?? '')) this.next(); }
  136. return;
  137. }
  138. if (['CONST', 'TYPE', 'VAR', 'LABEL'].includes(t.upper)) {
  139. this.skipSection();
  140. continue;
  141. }
  142. return;
  143. }
  144. }
  145. skipCompound(): void {
  146. if (!this.at('BEGIN')) return;
  147. this.next();
  148. let depth = 1;
  149. while (this.pos < this.toks.length && depth > 0) {
  150. const t = this.next();
  151. if (t.upper === 'BEGIN' || t.upper === 'CASE' || t.upper === 'RECORD') depth++;
  152. else if (t.upper === 'END') depth--;
  153. }
  154. }
  155. /** Skip one declaration section (const/type/var/label). */
  156. skipSection(): void {
  157. const kw = this.peek()?.upper;
  158. if (kw !== 'CONST' && kw !== 'TYPE' && kw !== 'VAR' && kw !== 'LABEL') return;
  159. this.next();
  160. // items separated by ';' until a section starter, routine, BEGIN or END
  161. for (;;) {
  162. const t = this.peek();
  163. if (!t) return;
  164. if (['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END', 'IMPLEMENTATION', 'INTERFACE', 'FINALIZATION', 'INITIALIZATION'].includes(t.upper)) return;
  165. this.skipDeclItem();
  166. if (this.at(';')) this.next();
  167. else return;
  168. }
  169. }
  170. /** Skip one `name [, ...] ...` declaration item up to `;` at depth 0. */
  171. skipDeclItem(): void {
  172. let depth = 0;
  173. while (this.pos < this.toks.length) {
  174. const t = this.peek()!;
  175. if (t.upper === '(' || t.upper === '[') depth++;
  176. else if (t.upper === ')' || t.upper === ']') depth--;
  177. else if (t.upper === 'RECORD') depth++;
  178. else if (t.upper === 'END' && depth > 0) depth--;
  179. else if (depth === 0 && (t.upper === ';' || ['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(t.upper))) return;
  180. this.next();
  181. }
  182. }
  183. /** Parse an identifier list, returning the name tokens. */
  184. nameList(): Tok[] {
  185. const out: Tok[] = [];
  186. for (;;) {
  187. const t = this.peek();
  188. if (!t || !isIdent(t)) return out;
  189. out.push(this.next());
  190. if (this.at(',')) { this.next(); continue; }
  191. return out;
  192. }
  193. }
  194. }
  195. /** Parse a `RECORD ... END` field list; RECORD already consumed.
  196. *
  197. * Only plain `name [, ...] : type;` items at depth 1 become fields.
  198. * Variant parts (CASE), nested records and anything unrecognised are
  199. * skipped without emitting (misses acceptable, wrong entries are not).
  200. * The closing END is consumed. */
  201. export function parseRecordFields(p: Parser): PasSymbol[] {
  202. const fields: PasSymbol[] = [];
  203. let depth = 1;
  204. for (;;) {
  205. const t = p.peek();
  206. if (!t) return fields;
  207. if (t.upper === 'RECORD') { depth++; p.next(); continue; }
  208. if (t.upper === 'END') {
  209. depth--;
  210. p.next();
  211. if (depth <= 0) return fields;
  212. continue;
  213. }
  214. if (depth !== 1) { p.next(); continue; }
  215. if (!isIdent(t)) {
  216. p.skipDeclItem();
  217. if (p.at(';')) p.next();
  218. else if (p.peek() && p.peek()!.upper !== 'END') p.next();
  219. continue;
  220. }
  221. if (['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN'].includes(t.upper)) {
  222. // Lost sync: skip to the matching END so the caller stays aligned.
  223. while (p.pos < p.toks.length && depth > 0) {
  224. const k = p.next();
  225. if (k.upper === 'RECORD') depth++;
  226. else if (k.upper === 'END') depth--;
  227. }
  228. return fields;
  229. }
  230. const names = p.nameList();
  231. if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  232. p.next();
  233. const typeToks: string[] = [];
  234. let dd = 0;
  235. while (p.pos < p.toks.length) {
  236. const k = p.peek()!;
  237. if (k.upper === '(' || k.upper === '[') dd++;
  238. else if (k.upper === ')' || k.upper === ']') dd--;
  239. else if (k.upper === 'RECORD') dd++;
  240. else if (k.upper === 'END' && dd > 0) dd--;
  241. else if (dd === 0 && (k.upper === ';' || k.upper === 'END')) break;
  242. else if (dd === 0 && ['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'FUNCTION', 'BEGIN'].includes(k.upper)) break;
  243. typeToks.push(k.text);
  244. p.next();
  245. }
  246. const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
  247. for (const nm of names) {
  248. fields.push({
  249. name: nm.text, kind: 'field', detail: `${nm.text}: ${typeText}`,
  250. line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: []
  251. });
  252. }
  253. if (p.at(';')) p.next();
  254. }
  255. }
  256. /** Parse an enumeration literal list `(A, B, ...)`; `(` already consumed.
  257. * Returns the literal names with their tokens. Stops at `)` (consumed)
  258. * or on anything unparseable (partial list kept, never throws). */
  259. export function parseEnumLiterals(p: Parser): Tok[] {
  260. const out: Tok[] = [];
  261. for (;;) {
  262. const e = p.peek();
  263. if (!e) return out;
  264. if (e.upper === ')') { p.next(); return out; }
  265. if (e.upper === ';') return out;
  266. if (isIdent(e)) {
  267. out.push(p.next());
  268. const nx = p.peek();
  269. if (nx && (nx.upper === '=' || nx.upper === ':=')) {
  270. // Explicit ordinal (some dialects): skip value to ',' or ')'.
  271. p.next();
  272. let dd = 0;
  273. while (p.pos < p.toks.length) {
  274. const q = p.peek()!;
  275. if (q.upper === '(') dd++;
  276. else if (q.upper === ')') {
  277. if (dd === 0) break;
  278. dd--;
  279. }
  280. else if (dd === 0 && (q.upper === ',' || q.upper === ';')) break;
  281. p.next();
  282. }
  283. }
  284. if (p.at(',')) { p.next(); continue; }
  285. continue;
  286. }
  287. p.next();
  288. }
  289. }
  290. /** A `USES` clause unit name with its position (for go-to-unit). */
  291. export interface PasUse {
  292. name: string;
  293. line: number; ch: number; endCh: number;
  294. }
  295. /** All `USES a, b, ...;` unit names at paren-depth 0.
  296. *
  297. * `IN 'file'` segments are skipped (string contents are masked away,
  298. * so IN is followed by `,`/`;`/ident — everything up to the next `,`
  299. * or `;` is ignored). A USES not followed by an identifier, or one
  300. * without a terminating `;`, yields nothing. */
  301. export function parseUses(text: string): PasUse[] {
  302. const toks = lex(mask(text));
  303. const out: PasUse[] = [];
  304. let depth = 0;
  305. for (let k = 0; k < toks.length; k++) {
  306. const t = toks[k];
  307. if (t.upper === '(') { depth++; continue; }
  308. if (t.upper === ')') { depth--; continue; }
  309. if (depth !== 0 || t.upper !== 'USES') continue;
  310. const nx = toks[k + 1];
  311. if (!nx || !isIdent(nx)) continue;
  312. let j = k + 1;
  313. let ok = false;
  314. for (;;) {
  315. const u = toks[j];
  316. if (!u) break;
  317. if (u.upper === ';') { ok = true; break; }
  318. if (u.upper === 'BEGIN' || u.upper === 'END' || u.upper === 'IMPLEMENTATION' ||
  319. u.upper === 'PROCEDURE' || u.upper === 'FUNCTION') break;
  320. if (u.upper === 'IN') {
  321. j++;
  322. while (j < toks.length && toks[j].upper !== ',' && toks[j].upper !== ';') j++;
  323. continue;
  324. }
  325. if (isIdent(u)) {
  326. // Must be `name`, `name,`, or `name;` — anything else aborts.
  327. const after = toks[j + 1];
  328. if (!after || (after.upper !== ',' && after.upper !== ';' && after.upper !== 'IN')) break;
  329. out.push({ name: u.text, line: u.line, ch: u.ch, endCh: u.endCh });
  330. } else if (u.upper !== ',') {
  331. break;
  332. }
  333. j++;
  334. }
  335. if (ok) k = j;
  336. }
  337. return out;
  338. }
  339. /** Parse the declarations of one scope; stops before BEGIN/END/EOF. */
  340. function parseScope(p: Parser, out: PasSymbol[], stopAtBegin: boolean): void {
  341. for (;;) {
  342. const t = p.peek();
  343. if (!t) return;
  344. const u = t.upper;
  345. if (u === 'BEGIN') {
  346. if (stopAtBegin) return;
  347. p.skipBody('');
  348. return;
  349. }
  350. if (u === 'END') return;
  351. if (u === 'INTERFACE' || u === 'IMPLEMENTATION') { p.next(); continue; }
  352. if (u === 'INITIALIZATION' || u === 'FINALIZATION') {
  353. // Statement section: skip to its END so no statement noise leaks in.
  354. p.next();
  355. let depth = 0;
  356. while (p.pos < p.toks.length) {
  357. const k = p.next();
  358. if (k.upper === 'BEGIN' || k.upper === 'CASE' || k.upper === 'RECORD') depth++;
  359. else if (k.upper === 'END') {
  360. if (depth === 0) break;
  361. depth--;
  362. }
  363. }
  364. continue;
  365. }
  366. if (u === 'LABEL') { p.skipSection(); continue; }
  367. if (u === 'CONST') {
  368. p.next();
  369. for (;;) {
  370. const n = p.peek();
  371. if (!n || !isIdent(n)) break;
  372. if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
  373. const nm = p.next();
  374. if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  375. p.next();
  376. // value: skip to ';' at depth 0
  377. p.skipDeclItem();
  378. out.push({ name: nm.text, kind: 'constant', detail: `CONST ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
  379. if (p.at(';')) p.next();
  380. else break;
  381. }
  382. continue;
  383. }
  384. if (u === 'TYPE') {
  385. p.next();
  386. for (;;) {
  387. const n = p.peek();
  388. if (!n || !isIdent(n)) break;
  389. if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
  390. const nm = p.next();
  391. if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  392. p.next();
  393. const typeChildren: PasSymbol[] = [];
  394. const packedRec = p.at('PACKED') && p.peek(1)?.upper === 'RECORD';
  395. if (packedRec) p.next();
  396. if (p.at('RECORD')) {
  397. p.next();
  398. typeChildren.push(...parseRecordFields(p));
  399. } else if (p.at('(')) {
  400. p.next();
  401. for (const lit of parseEnumLiterals(p)) {
  402. typeChildren.push({
  403. name: lit.text, kind: 'constant', detail: `${nm.text}.${lit.text}`,
  404. line: lit.line, ch: lit.ch, endLine: lit.endLine, endCh: lit.endCh, children: []
  405. });
  406. }
  407. } else {
  408. p.skipDeclItem();
  409. }
  410. 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 });
  411. if (p.at(';')) p.next();
  412. else break;
  413. }
  414. continue;
  415. }
  416. if (u === 'VAR' || u === 'THREADVAR') {
  417. p.next();
  418. for (;;) {
  419. const n = p.peek();
  420. if (!n || !isIdent(n)) break;
  421. if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
  422. const names = p.nameList();
  423. if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  424. p.next();
  425. // Inline `RECORD ... END` type: fields become the variable's children.
  426. const packedInline = p.at('PACKED') && p.peek(1)?.upper === 'RECORD';
  427. if (packedInline) p.next();
  428. if (p.at('RECORD')) {
  429. p.next();
  430. const fields = parseRecordFields(p);
  431. for (const nm of names) {
  432. 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 });
  433. }
  434. if (p.at(';')) p.next();
  435. else break;
  436. continue;
  437. }
  438. // capture type text up to ';' or '=' (initializer) at depth 0
  439. const typeToks: string[] = [];
  440. let depth = 0;
  441. while (p.pos < p.toks.length) {
  442. const k = p.peek()!;
  443. if (k.upper === '(' || k.upper === '[') depth++;
  444. else if (k.upper === ')' || k.upper === ']') depth--;
  445. else if (k.upper === 'RECORD') depth++;
  446. else if (k.upper === 'END' && depth > 0) depth--;
  447. else if (depth === 0 && (k.upper === ';' || k.upper === '=')) break;
  448. else if (depth === 0 && ['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(k.upper)) break;
  449. typeToks.push(k.text);
  450. p.next();
  451. }
  452. const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
  453. if (p.at('=')) { p.next(); p.skipDeclItem(); }
  454. for (const nm of names) {
  455. 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: [] });
  456. }
  457. if (p.at(';')) p.next();
  458. else break;
  459. }
  460. continue;
  461. }
  462. if (u === 'PROCEDURE' || u === 'FUNCTION') {
  463. const isFunc = u === 'FUNCTION';
  464. const kw = p.next();
  465. // optional generic prefix / class / receiver: find the routine name.
  466. // Name = last ident before the formal list, result type or ';'.
  467. // Stops at '(' (formals parsed separately below); '<...>' type
  468. // parameter lists are skipped via angle depth.
  469. let depth = 0;
  470. let adepth = 0;
  471. let nameTok: Tok | null = null;
  472. let lastIdent: Tok | null = null;
  473. while (p.pos < p.toks.length) {
  474. const k = p.peek()!;
  475. if (k.upper === '<') adepth++;
  476. else if (k.upper === '>') adepth--;
  477. else if (k.upper === '(' && adepth === 0) break;
  478. else if (k.upper === '(') depth++;
  479. else if (k.upper === ')') depth--;
  480. else if (depth === 0 && adepth === 0 && (k.upper === ';' || k.upper === ':')) break;
  481. else if (depth === 0 && adepth === 0 && k.upper === '.') { lastIdent = null; }
  482. else if (depth === 0 && adepth === 0 && isIdent(k)) lastIdent = k;
  483. if (depth === 0 && adepth === 0 && ['BEGIN', 'END', 'CONST', 'TYPE', 'VAR', 'PROCEDURE', 'FUNCTION'].includes(k.upper)) break;
  484. p.next();
  485. }
  486. nameTok = lastIdent;
  487. // formal params
  488. const params: PasSymbol[] = [];
  489. if (p.at('(')) {
  490. p.next();
  491. let pd = 1;
  492. const pnames: Tok[] = [];
  493. let ptype: string[] = [];
  494. let inType = false;
  495. const flush = () => {
  496. const tt = ptype.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
  497. for (const nm of pnames) {
  498. params.push({ name: nm.text, kind: 'parameter', detail: `${nm.text}: ${tt}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
  499. }
  500. pnames.length = 0; ptype = []; inType = false;
  501. };
  502. while (p.pos < p.toks.length && pd > 0) {
  503. const k = p.next();
  504. if (k.upper === '(') { pd++; if (inType) ptype.push(k.text); continue; }
  505. if (k.upper === ')') { pd--; if (pd === 0) { if (pnames.length || ptype.length) flush(); break; } if (inType) ptype.push(k.text); continue; }
  506. if (pd !== 1) { if (inType) ptype.push(k.text); continue; }
  507. if (k.upper === ';') { flush(); continue; }
  508. if (k.upper === ':' && !inType) { inType = true; continue; }
  509. if (k.upper === ',') continue;
  510. if (k.upper === '=' && inType) {
  511. // default value: skip to ';' or ')' at depth 1
  512. let dd = 1;
  513. while (p.pos < p.toks.length && dd > 0) {
  514. const q = p.next();
  515. if (q.upper === '(') dd++;
  516. else if (q.upper === ')') { dd--; if (dd === 0) { pd--; if (pnames.length || ptype.length) flush(); break; } }
  517. else if (q.upper === ';' && dd === 1) { flush(); break; }
  518. }
  519. continue;
  520. }
  521. if (['VAR', 'OUT', 'CONST'].includes(k.upper) && !inType && pnames.length === 0) continue;
  522. if (isIdent(k)) { if (inType) ptype.push(k.text); else pnames.push(k); }
  523. else if (inType) ptype.push(k.text);
  524. }
  525. }
  526. // result type
  527. let restype = '';
  528. if (p.at(':')) {
  529. p.next();
  530. const rt: string[] = [];
  531. while (p.pos < p.toks.length) {
  532. const k = p.peek()!;
  533. if (k.upper === ';') break;
  534. rt.push(k.text);
  535. p.next();
  536. }
  537. restype = rt.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
  538. }
  539. if (!nameTok) {
  540. // unrecognised header: skip one item and continue (never emit)
  541. p.skipDeclItem();
  542. if (p.at(';')) p.next();
  543. continue;
  544. }
  545. const sym: PasSymbol = {
  546. name: nameTok.text,
  547. kind: isFunc ? 'function' : 'procedure',
  548. detail: `${isFunc ? 'FUNCTION' : 'PROCEDURE'} ${nameTok.text}${restype ? ': ' + restype : ''}`,
  549. line: kw.line, ch: kw.ch, endLine: nameTok.endLine, endCh: nameTok.endCh,
  550. children: params
  551. };
  552. out.push(sym);
  553. if (p.at(';')) p.next();
  554. // directives or body: skip directives (words + ';') then body
  555. for (;;) {
  556. const k = p.peek();
  557. if (!k) break;
  558. if (k.upper === 'INTERFACE' || k.upper === 'IMPLEMENTATION') { p.next(); continue; }
  559. if (k.upper === 'BEGIN') { p.skipBody(nameTok.text); break; }
  560. if (k.upper === 'FORWARD' || k.upper === 'EXTERNAL') {
  561. // bodyless: consume through ';'
  562. while (p.pos < p.toks.length && !p.at(';') && !['BEGIN', 'END', 'PROCEDURE', 'FUNCTION', 'CONST', 'TYPE', 'VAR'].includes(p.peek()!.upper)) p.next();
  563. if (p.at(';')) p.next();
  564. break;
  565. }
  566. if (k.upper === 'END' || k.upper === 'PROCEDURE' || k.upper === 'FUNCTION') break;
  567. // nested declarations belong to this routine: parse one level, then body
  568. if (['CONST', 'TYPE', 'VAR', 'LABEL'].includes(k.upper)) {
  569. parseScope(p, sym.children, true);
  570. continue;
  571. }
  572. // unknown directive word: skip to ';' (covers calling conventions)
  573. if (isIdent(k)) {
  574. p.next();
  575. if (p.at(';')) { p.next(); continue; }
  576. // not a lone directive: treat rest as body
  577. p.skipBody(nameTok.text);
  578. break;
  579. }
  580. break;
  581. }
  582. const tail = p.toks[p.pos - 1];
  583. if (tail) { sym.scopeEndLine = tail.endLine; sym.scopeEndCh = tail.endCh; }
  584. continue;
  585. }
  586. // anything else at scope level: skip one item to stay in sync
  587. p.skipDeclItem();
  588. if (p.at(';')) p.next();
  589. else if (p.pos < p.toks.length) p.next();
  590. }
  591. }
  592. /** Entry: top-level symbols of a Pascal unit/program. */
  593. export function parsePascal(text: string): PasSymbol[] {
  594. const toks = lex(mask(text));
  595. const p = new Parser(toks);
  596. // skip program/unit/library header up to the first ';' at depth 0
  597. const first = p.peek();
  598. if (first && ['PROGRAM', 'UNIT', 'LIBRARY'].includes(first.upper)) {
  599. p.next();
  600. let depth = 0;
  601. while (p.pos < p.toks.length) {
  602. const t = p.next();
  603. if (t.upper === '(') depth++;
  604. else if (t.upper === ')') depth--;
  605. else if (t.upper === ';' && depth === 0) break;
  606. }
  607. }
  608. const out: PasSymbol[] = [];
  609. parseScope(p, out, false);
  610. // module wrapper for outline friendliness when a header was seen
  611. if (first && ['PROGRAM', 'UNIT', 'LIBRARY'].includes(first.upper)) {
  612. const nm = p.toks.length > 1 ? p.toks[1] : null;
  613. if (nm && /^[A-Za-z_]/.test(nm.text)) {
  614. const last = p.toks[p.toks.length - 1];
  615. 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 };
  616. if (last) { mod.scopeEndLine = last.endLine; mod.scopeEndCh = last.endCh; }
  617. return [mod];
  618. }
  619. }
  620. return out;
  621. }
  622. /** True when (line, ch) lies within the symbol's scope, if known. */
  623. function inScope(s: PasSymbol, line: number, ch: number): boolean {
  624. if (s.scopeEndLine === undefined) return true;
  625. if (line < s.line) return false;
  626. if (line > s.scopeEndLine) return false;
  627. if (line === s.scopeEndLine && s.scopeEndCh !== undefined && ch > s.scopeEndCh) return false;
  628. return true;
  629. }
  630. /** Symbols visible at a position: globals plus the innermost enclosing
  631. * routine's params and locals. Forgiving by design (completion-only). */
  632. export function visibleAt(symbols: PasSymbol[], line: number, ch: number): PasSymbol[] {
  633. const top: PasSymbol[] = [];
  634. for (const s of symbols) {
  635. if (s.kind === 'module') top.push(...s.children);
  636. else top.push(s);
  637. }
  638. const result: PasSymbol[] = [];
  639. for (const s of top) {
  640. if (s.kind === 'constant' || s.kind === 'type' || s.kind === 'variable') result.push(s);
  641. }
  642. // innermost enclosing routine first
  643. const routines = top.filter(s => (s.kind === 'procedure' || s.kind === 'function') && inScope(s, line, ch));
  644. routines.sort((a, b) => (b.line - a.line) || ((b.ch ?? 0) - (a.ch ?? 0)));
  645. for (const r of routines) {
  646. result.push(...r.children);
  647. result.push(r);
  648. }
  649. // top-level routines are callable throughout the unit
  650. for (const s of top) {
  651. if ((s.kind === 'procedure' || s.kind === 'function') && !result.includes(s)) result.push(s);
  652. }
  653. return result;
  654. }