|
|
@@ -21,6 +21,7 @@ import { lex, Token } from './lexer';
|
|
|
import { M2Symbol, M2Unit, parseUnitText, rangeContains } from './parser';
|
|
|
import { flattenUnit, loadDefModule, resolveName, topLevel } from './resolve';
|
|
|
import { checkBodies } from './checker';
|
|
|
+import { M2Type, parseTypeText } from './types';
|
|
|
|
|
|
export interface M2Issue {
|
|
|
line: number; ch: number; endLine: number; endCh: number;
|
|
|
@@ -51,6 +52,7 @@ export function analyseUnit(text: string, filePath: string, docDir: string): M2I
|
|
|
...duplicateDeclarations(unit),
|
|
|
...unknownIdentifiers(tokens, unit, docDir),
|
|
|
...fromMembership(unit, docDir),
|
|
|
+ ...circularDependencies(unit, docDir),
|
|
|
...checkBodies(tokens, unit, docDir),
|
|
|
];
|
|
|
}
|
|
|
@@ -97,6 +99,11 @@ function unknownIdentifiers(tokens: Token[], unit: M2Unit, docDir: string): M2Is
|
|
|
for (const { sym } of flattenUnit(unit)) {
|
|
|
declared.add(`${sym.nameRange.startLine}:${sym.nameRange.startCh}`);
|
|
|
}
|
|
|
+ // Formal parameters of procedure TYPE expressions (`P: PROCEDURE (e: E)`)
|
|
|
+ // are not recorded as symbols; their positions declare them.
|
|
|
+ for (const pos of procTypeFormalPositions(tokens)) {
|
|
|
+ declared.add(pos);
|
|
|
+ }
|
|
|
const fromNames = new Set<string>();
|
|
|
for (const imp of unit.imports) for (const n of imp.names) fromNames.add(n);
|
|
|
const withRegions = withRegionsOf(tokens);
|
|
|
@@ -153,6 +160,160 @@ function fromMembership(unit: M2Unit, docDir: string): M2Issue[] {
|
|
|
return issues;
|
|
|
}
|
|
|
|
|
|
+/** Circular type/constant dependencies.
|
|
|
+ *
|
|
|
+ * gm2 is multi-pass: declaration order never matters, so there is no
|
|
|
+ * use-before-declaration checking. What gm2 does reject is dependency
|
|
|
+ * cycles that never ground (`A = B; B = A`, `A = POINTER TO B;
|
|
|
+ * B = POINTER TO A`). Chains grounding at a RECORD, enumeration,
|
|
|
+ * predefined type or unknown are fine, as is the callback pattern
|
|
|
+ * (`H = PROCEDURE (e: E)` with `E` a record, which terminates because
|
|
|
+ * signatures are descended into but records ground). Only same-file
|
|
|
+ * declarations are followed, so every reported position belongs to the
|
|
|
+ * analysed document.
|
|
|
+ */
|
|
|
+function circularDependencies(unit: M2Unit, docDir: string): M2Issue[] {
|
|
|
+ const issues: M2Issue[] = [];
|
|
|
+ const grounded = new Set<string>();
|
|
|
+ const reportedCycles = new Set<string>();
|
|
|
+
|
|
|
+ const keyOf = (sym: M2Symbol): string =>
|
|
|
+ `${sym.nameRange.startLine}:${sym.nameRange.startCh}:${sym.name}`;
|
|
|
+
|
|
|
+ /** Same-file type/constant declaration a reference resolves to, if any. */
|
|
|
+ function follow(
|
|
|
+ line: number, ch: number, qualifier: string | null, name: string,
|
|
|
+ ): M2Symbol | null {
|
|
|
+ const r = resolveName(unit, docDir, line, ch, qualifier, name);
|
|
|
+ if (!r || r.filePath !== unit.filePath) return null;
|
|
|
+ if (r.sym.kind !== 'type' && r.sym.kind !== 'constant') return null;
|
|
|
+ return r.sym;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Outgoing dependency edges of one declaration. */
|
|
|
+ function edges(sym: M2Symbol): Array<{ qualifier: string | null; name: string }> {
|
|
|
+ if (sym.kind === 'type') {
|
|
|
+ const detail = sym.detail;
|
|
|
+ const eq = detail.indexOf('=');
|
|
|
+ if (eq < 0) return []; // opaque type: grounded
|
|
|
+ return typeRefs(parseTypeText(detail.slice(eq + 1)));
|
|
|
+ }
|
|
|
+ if (sym.kind === 'constant') {
|
|
|
+ const m = /^\w+\.\w+$/.exec(sym.detail);
|
|
|
+ if (m) return []; // enumeration literal: grounded
|
|
|
+ const eq = sym.detail.indexOf('=');
|
|
|
+ if (eq < 0) return [];
|
|
|
+ return valueRefs(sym.detail.slice(eq + 1));
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Named references inside a parsed type (descent mirrors gm2 grounding). */
|
|
|
+ function typeRefs(t: M2Type): Array<{ qualifier: string | null; name: string }> {
|
|
|
+ switch (t.kind) {
|
|
|
+ case 'named': return [{ qualifier: t.qualifier, name: t.name }];
|
|
|
+ case 'pointer': return typeRefs(t.target);
|
|
|
+ case 'array': return typeRefs(t.element);
|
|
|
+ case 'subrange': return typeRefs(t.base);
|
|
|
+ case 'proc': {
|
|
|
+ const out: Array<{ qualifier: string | null; name: string }> = [];
|
|
|
+ for (const p of t.params ?? []) out.push(...typeRefs(p));
|
|
|
+ if (t.result) out.push(...typeRefs(t.result));
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+ default: return []; // record, enum, predefined, unknown, literals: grounded
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Identifier references inside a constant value fragment. */
|
|
|
+ function valueRefs(fragment: string): Array<{ qualifier: string | null; name: string }> {
|
|
|
+ const out: Array<{ qualifier: string | null; name: string }> = [];
|
|
|
+ const toks = lex(fragment).filter(t => t.kind !== 'eof');
|
|
|
+ for (let k = 0; k < toks.length; k++) {
|
|
|
+ const t = toks[k];
|
|
|
+ if (t.kind !== 'ident') continue;
|
|
|
+ if (toks[k + 1]?.text === '.' && toks[k + 2]?.kind === 'ident') {
|
|
|
+ out.push({ qualifier: t.text, name: toks[k + 2].text });
|
|
|
+ k += 2;
|
|
|
+ } else if (toks[k - 1]?.text === '.') {
|
|
|
+ continue; // qualified-away part, handled with its qualifier
|
|
|
+ } else {
|
|
|
+ out.push({ qualifier: null, name: t.text });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ function visit(sym: M2Symbol, stack: M2Symbol[]): void {
|
|
|
+ const key = keyOf(sym);
|
|
|
+ const at = stack.findIndex(s => keyOf(s) === key);
|
|
|
+ if (at >= 0) {
|
|
|
+ const members = [...stack.slice(at).map(s => keyOf(s)), key].sort();
|
|
|
+ const canon = members.join('|');
|
|
|
+ if (!reportedCycles.has(canon)) {
|
|
|
+ reportedCycles.add(canon);
|
|
|
+ const root = stack[at];
|
|
|
+ issues.push({
|
|
|
+ line: root.nameRange.startLine, ch: root.nameRange.startCh,
|
|
|
+ endLine: root.nameRange.endLine, endCh: root.nameRange.endCh,
|
|
|
+ message: root.kind === 'constant'
|
|
|
+ ? `Circular dependency in constant "${root.name}" (type cannot be determined)`
|
|
|
+ : `Circular type dependency involving "${root.name}"`,
|
|
|
+ severity: 'error',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (grounded.has(key)) return;
|
|
|
+ stack.push(sym);
|
|
|
+ const line = sym.nameRange.startLine;
|
|
|
+ const ch = sym.nameRange.startCh;
|
|
|
+ for (const ref of edges(sym)) {
|
|
|
+ const target = follow(line, ch, ref.qualifier, ref.name);
|
|
|
+ if (target) visit(target, stack);
|
|
|
+ }
|
|
|
+ stack.pop();
|
|
|
+ grounded.add(key);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const { sym } of flattenUnit(unit)) {
|
|
|
+ if ((sym.kind === 'type' || sym.kind === 'constant') && sym.name) {
|
|
|
+ visit(sym, []);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return issues;
|
|
|
+}
|
|
|
+
|
|
|
+/** Positions of formal names in `PROCEDURE (...)` type expressions.
|
|
|
+ *
|
|
|
+ * Only triggers on the PROCEDURE keyword, so calls and case labels are
|
|
|
+ * unaffected: an undeclared `gren:` case label is still reported.
|
|
|
+ */
|
|
|
+function procTypeFormalPositions(tokens: Token[]): string[] {
|
|
|
+ const out: string[] = [];
|
|
|
+ for (let k = 0; k < tokens.length; k++) {
|
|
|
+ const t = tokens[k];
|
|
|
+ if (t.kind !== 'keyword' || t.text !== 'PROCEDURE') continue;
|
|
|
+ const open = tokens[k + 1];
|
|
|
+ if (!open || open.kind !== 'symbol' || open.text !== '(') continue;
|
|
|
+ let depth = 0;
|
|
|
+ for (let j = k + 1; j < tokens.length; j++) {
|
|
|
+ const u = tokens[j];
|
|
|
+ if (u.kind === 'symbol' && u.text === '(') depth++;
|
|
|
+ else if (u.kind === 'symbol' && u.text === ')') {
|
|
|
+ depth--;
|
|
|
+ if (depth === 0) break;
|
|
|
+ } else if (u.kind === 'ident' && depth >= 1) {
|
|
|
+ const nx = tokens[j + 1];
|
|
|
+ if (nx?.kind === 'symbol' && (nx.text === ':' || nx.text === ',')) {
|
|
|
+ out.push(`${u.line}:${u.ch}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
/** Offset spans of `WITH ... DO ... END` blocks (conservative bracket matching). */
|
|
|
function withRegionsOf(tokens: Token[]): Array<[number, number]> {
|
|
|
const regions: Array<[number, number]> = [];
|