|
|
@@ -0,0 +1,836 @@
|
|
|
+/** Statement-level semantic checking for Modula-2 bodies.
|
|
|
+ *
|
|
|
+ * Walks `BEGIN..END` bodies (recorded by the parser) with a small recursive
|
|
|
+ * statement/expression analyser and reports type errors: assignment
|
|
|
+ * incompatibility, call arity and argument mismatches, non-BOOLEAN
|
|
|
+ * conditions, bad `RETURN`s, illegal targets and operands. All operator and
|
|
|
+ * compatibility rules live in `types.ts` and were calibrated against gm2.
|
|
|
+ *
|
|
|
+ * Recovery-first: the walker always makes progress and never throws, so
|
|
|
+ * broken code under editing yields fewer diagnostics, never a crash.
|
|
|
+ * Uncertain cases resolve to `unknown` and stay silent.
|
|
|
+ */
|
|
|
+
|
|
|
+import type { M2Issue } from './analyse';
|
|
|
+import { Token } from './lexer';
|
|
|
+import { M2Symbol, M2Unit } from './parser';
|
|
|
+import { resolveName } from './resolve';
|
|
|
+import {
|
|
|
+ M2Type, UNKNOWN, arithKind, displayType, isArrayArgCompatible, isAssignable,
|
|
|
+ isComparable, isOrdinal, parseTypeText, resolveDeclaredType, resolveQualified,
|
|
|
+ resolveType, returnTypeOf, unitOfFile,
|
|
|
+} from './types';
|
|
|
+import * as path from 'path';
|
|
|
+
|
|
|
+const TYPE_BUILTINS = new Set([
|
|
|
+ 'INTEGER', 'CARDINAL', 'LONGINT', 'SHORTINT', 'LONGCARD', 'SHORTCARD',
|
|
|
+ 'REAL', 'LONGREAL', 'CHAR', 'BOOLEAN', 'BITSET', 'ADDRESS', 'WORD', 'BYTE',
|
|
|
+ 'OCTET', 'LOC',
|
|
|
+]);
|
|
|
+const PROC_BUILTINS = new Set([
|
|
|
+ 'ABS', 'ADR', 'CAP', 'CHR', 'DEC', 'DISPOSE', 'EXCL', 'FLOAT', 'HALT',
|
|
|
+ 'HIGH', 'INC', 'INCL', 'LFLOAT', 'MAX', 'MIN', 'NEW', 'ODD', 'ORD',
|
|
|
+ 'SIZE', 'TRUNC', 'TSIZE', 'VAL', 'CODE', 'COPY', 'FILL',
|
|
|
+]);
|
|
|
+
|
|
|
+const OPENERS = new Set(['BEGIN', 'IF', 'CASE', 'LOOP', 'WHILE', 'FOR', 'WITH', 'RECORD', 'MODULE']);
|
|
|
+const MAX_DEPTH = 80;
|
|
|
+const MAX_EXPR_DEPTH = 50;
|
|
|
+
|
|
|
+interface Ctx {
|
|
|
+ unit: M2Unit;
|
|
|
+ docDir: string;
|
|
|
+ issues: M2Issue[];
|
|
|
+}
|
|
|
+
|
|
|
+type HeadInfo =
|
|
|
+ | { kind: 'var'; sym: M2Symbol; host: M2Unit; dir: string }
|
|
|
+ | { kind: 'proc'; sym: M2Symbol; host: M2Unit; dir: string }
|
|
|
+ | { kind: 'const'; sym: M2Symbol; host: M2Unit; dir: string }
|
|
|
+ | { kind: 'literal'; type: M2Type }
|
|
|
+ | { kind: 'type' }
|
|
|
+ | { kind: 'module'; name: string }
|
|
|
+ | { kind: 'builtinType'; name: string }
|
|
|
+ | { kind: 'builtinProc'; name: string }
|
|
|
+ | { kind: 'field' }
|
|
|
+ | { kind: 'unknown' };
|
|
|
+
|
|
|
+interface Value {
|
|
|
+ type: M2Type;
|
|
|
+ info: HeadInfo;
|
|
|
+ tok: Token;
|
|
|
+}
|
|
|
+
|
|
|
+export function checkBodies(tokens: Token[], unit: M2Unit, docDir: string): M2Issue[] {
|
|
|
+ const ctx: Ctx = { unit, docDir, issues: [] };
|
|
|
+ for (const s of unit.symbols) checkContainer(ctx, tokens, s);
|
|
|
+ return ctx.issues;
|
|
|
+}
|
|
|
+
|
|
|
+function checkContainer(ctx: Ctx, tokens: Token[], sym: M2Symbol): void {
|
|
|
+ for (const c of sym.children) {
|
|
|
+ if (c.kind === 'procedure' || c.kind === 'module') checkContainer(ctx, tokens, c);
|
|
|
+ }
|
|
|
+ if (!sym.body) return;
|
|
|
+ const range = bodyIndexes(tokens, sym.body.startLine, sym.body.startCh, sym.body.endLine, sym.body.endCh);
|
|
|
+ if (!range) return;
|
|
|
+ new Walker(ctx, tokens, sym, range.from, range.to).parseSeq(TOP_STOPS, 0);
|
|
|
+}
|
|
|
+
|
|
|
+const TOP_STOPS = new Set(['END']);
|
|
|
+
|
|
|
+function bodyIndexes(tokens: Token[], sl: number, sc: number, el: number, ec: number): { from: number; to: number } | null {
|
|
|
+ let from = -1;
|
|
|
+ let to = -1;
|
|
|
+ for (let k = 0; k < tokens.length; k++) {
|
|
|
+ const t = tokens[k];
|
|
|
+ if (t.kind === 'eof') break;
|
|
|
+ if (from < 0 && t.line === sl && t.ch === sc && t.kind === 'keyword' && t.text === 'BEGIN') from = k + 1;
|
|
|
+ if (t.kind === 'keyword' && t.text === 'END' && t.endLine === el && t.endCh === ec) { to = k; break; }
|
|
|
+ }
|
|
|
+ if (from < 0 || to < 0 || from > to) return null;
|
|
|
+ return { from, to };
|
|
|
+}
|
|
|
+
|
|
|
+class Walker {
|
|
|
+ private pos: number;
|
|
|
+ private returnType: M2Type | null | undefined;
|
|
|
+ constructor(
|
|
|
+ private ctx: Ctx,
|
|
|
+ private toks: Token[],
|
|
|
+ private scope: M2Symbol,
|
|
|
+ pos: number,
|
|
|
+ private end: number,
|
|
|
+ ) { this.pos = pos; }
|
|
|
+
|
|
|
+ private issue(t: Token, message: string): void {
|
|
|
+ this.ctx.issues.push({
|
|
|
+ line: t.line, ch: t.ch, endLine: t.endLine, endCh: t.endCh,
|
|
|
+ message, severity: 'error',
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private peek(off = 0): Token | null {
|
|
|
+ const i = this.pos + off;
|
|
|
+ if (i < 0 || i >= this.end) return null;
|
|
|
+ const t = this.toks[i];
|
|
|
+ return t.kind === 'eof' ? null : t;
|
|
|
+ }
|
|
|
+
|
|
|
+ private atKw(word: string, off = 0): boolean {
|
|
|
+ const t = this.peek(off);
|
|
|
+ return !!t && t.kind === 'keyword' && t.text === word;
|
|
|
+ }
|
|
|
+
|
|
|
+ private atSym(sym: string, off = 0): boolean {
|
|
|
+ const t = this.peek(off);
|
|
|
+ return !!t && t.kind === 'symbol' && t.text === sym;
|
|
|
+ }
|
|
|
+
|
|
|
+ private next(): Token | null {
|
|
|
+ const t = this.peek();
|
|
|
+ if (t) this.pos++;
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ private expectKw(word: string): void {
|
|
|
+ if (this.atKw(word)) this.next();
|
|
|
+ }
|
|
|
+
|
|
|
+ parseSeq(stops: Set<string>, depth: number): void {
|
|
|
+ if (depth > MAX_DEPTH) { this.pos = this.end; return; }
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.kind === 'symbol' && t.text === ';') { this.next(); continue; }
|
|
|
+ if (t.kind === 'keyword' && stops.has(t.text)) return;
|
|
|
+ const before = this.pos;
|
|
|
+ this.parseStatement(stops, depth);
|
|
|
+ if (this.pos === before) this.next();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseStatement(stops: Set<string>, depth: number): void {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.kind === 'keyword') {
|
|
|
+ switch (t.text) {
|
|
|
+ case 'RETURN': this.parseReturn(); return;
|
|
|
+ case 'IF': this.parseIf(depth); return;
|
|
|
+ case 'WHILE': this.parseWhile(depth); return;
|
|
|
+ case 'REPEAT': this.parseRepeat(depth); return;
|
|
|
+ case 'LOOP': this.next(); this.parseSeq(new Set(['END']), depth + 1); this.expectKw('END'); return;
|
|
|
+ case 'FOR': this.parseFor(depth); return;
|
|
|
+ case 'WITH': this.parseWith(); return;
|
|
|
+ case 'CASE': this.parseCase(depth); return;
|
|
|
+ case 'EXIT': this.next(); return;
|
|
|
+ default:
|
|
|
+ if (stops.has(t.text)) return;
|
|
|
+ this.next(); return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (t.kind === 'ident') { this.parseDesignatorStatement(); return; }
|
|
|
+ this.skipToBoundary();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Skip garbage to the next plausible statement boundary (always progresses). */
|
|
|
+ private skipToBoundary(): void {
|
|
|
+ let depth = 0;
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.kind === 'symbol') {
|
|
|
+ if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
|
|
|
+ else if (t.text === ')' || t.text === ']' || t.text === '}') {
|
|
|
+ if (depth === 0) return;
|
|
|
+ depth--;
|
|
|
+ } else if (t.text === ';' && depth === 0) return;
|
|
|
+ this.next();
|
|
|
+ } else if (t.kind === 'keyword' && depth === 0) {
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ this.next();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private procReturn(): M2Type | null {
|
|
|
+ if (this.returnType === undefined) {
|
|
|
+ this.returnType = this.scope.kind === 'procedure'
|
|
|
+ ? returnTypeOf(this.scope, this.ctx.unit, this.ctx.docDir)
|
|
|
+ : null;
|
|
|
+ }
|
|
|
+ return this.returnType;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseReturn(): void {
|
|
|
+ const kw = this.next()!;
|
|
|
+ if (this.scope.kind !== 'procedure') {
|
|
|
+ this.issue(kw, 'RETURN outside a procedure');
|
|
|
+ this.skipToBoundary();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const ret = this.procReturn();
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t || t.text === ';' || t.kind === 'keyword') {
|
|
|
+ if (ret) this.issue(kw, `RETURN without a value in a function returning ${displayType(ret)}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const e = this.parseExpr(0);
|
|
|
+ if (!ret) {
|
|
|
+ this.issue(kw, 'Cannot return a value from a procedure without a result type');
|
|
|
+ } else if (e.type.kind !== 'unknown' && !isAssignable(ret, e.type)) {
|
|
|
+ this.issue(kw, `Cannot return ${displayType(e.type)} as ${displayType(ret)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseCondition(word: string): void {
|
|
|
+ const start = this.peek();
|
|
|
+ const e = this.parseExpr(0);
|
|
|
+ if (e.type.kind !== 'unknown' && e.type.kind !== 'bool') {
|
|
|
+ this.issue(start ?? this.toks[this.pos - 1], `Condition of ${word} must be BOOLEAN, found ${displayType(e.type)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseIf(depth: number): void {
|
|
|
+ this.next(); // IF
|
|
|
+ this.parseCondition('IF');
|
|
|
+ this.expectKw('THEN');
|
|
|
+ const stops = new Set(['ELSIF', 'ELSE', 'END']);
|
|
|
+ this.parseSeq(stops, depth + 1);
|
|
|
+ while (this.atKw('ELSIF')) {
|
|
|
+ this.next();
|
|
|
+ this.parseCondition('ELSIF');
|
|
|
+ this.expectKw('THEN');
|
|
|
+ this.parseSeq(stops, depth + 1);
|
|
|
+ }
|
|
|
+ if (this.atKw('ELSE')) {
|
|
|
+ this.next();
|
|
|
+ this.parseSeq(new Set(['END']), depth + 1);
|
|
|
+ }
|
|
|
+ this.expectKw('END');
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseWhile(depth: number): void {
|
|
|
+ this.next(); // WHILE
|
|
|
+ this.parseCondition('WHILE');
|
|
|
+ this.expectKw('DO');
|
|
|
+ this.parseSeq(new Set(['END']), depth + 1);
|
|
|
+ this.expectKw('END');
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseRepeat(depth: number): void {
|
|
|
+ this.next(); // REPEAT
|
|
|
+ this.parseSeq(new Set(['UNTIL']), depth + 1);
|
|
|
+ if (this.atKw('UNTIL')) {
|
|
|
+ this.next();
|
|
|
+ this.parseCondition('REPEAT..UNTIL');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseFor(depth: number): void {
|
|
|
+ this.next(); // FOR
|
|
|
+ const vt = this.peek();
|
|
|
+ let varType: M2Type = UNKNOWN;
|
|
|
+ let varOk = false;
|
|
|
+ if (vt?.kind === 'ident') {
|
|
|
+ this.next();
|
|
|
+ const info = this.resolveHead(vt);
|
|
|
+ if (info.kind === 'var') {
|
|
|
+ varType = resolveDeclaredType(info.sym, info.host, info.dir, vt.line, vt.ch);
|
|
|
+ if (varType.kind !== 'unknown' && !isOrdinal(varType)) {
|
|
|
+ this.issue(vt, `FOR loop variable must have an ordinal type, found ${displayType(varType)}`);
|
|
|
+ } else {
|
|
|
+ varOk = true;
|
|
|
+ }
|
|
|
+ } else if (info.kind === 'const' || info.kind === 'literal') {
|
|
|
+ this.issue(vt, `Cannot assign to constant "${vt.text}"`);
|
|
|
+ } else if (info.kind === 'proc') {
|
|
|
+ this.issue(vt, `Cannot assign to procedure "${vt.text}"`);
|
|
|
+ } else if (info.kind === 'type' || info.kind === 'builtinType') {
|
|
|
+ this.issue(vt, `"${vt.text}" is a type and cannot be used as a variable`);
|
|
|
+ } else if (info.kind === 'module') {
|
|
|
+ this.issue(vt, `"${vt.text}" is a module and cannot be used as a variable`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (this.atSym(':=')) {
|
|
|
+ const op = this.next()!;
|
|
|
+ const a = this.parseExpr(0);
|
|
|
+ if (varOk && varType.kind !== 'unknown' && a.type.kind !== 'unknown' && !isAssignable(varType, a.type)) {
|
|
|
+ this.issue(op, `Cannot assign ${displayType(a.type)} to ${displayType(varType)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.expectKw('TO');
|
|
|
+ const b = this.parseExpr(0);
|
|
|
+ if (varOk && varType.kind !== 'unknown' && b.type.kind !== 'unknown' && !isAssignable(varType, b.type)) {
|
|
|
+ this.issue(vt!, `Cannot assign ${displayType(b.type)} to ${displayType(varType)}`);
|
|
|
+ }
|
|
|
+ if (this.atKw('BY')) {
|
|
|
+ const by = this.next()!;
|
|
|
+ const c = this.parseExpr(0);
|
|
|
+ if (c.type.kind !== 'unknown' && c.type.kind !== 'whole') {
|
|
|
+ this.issue(by, `BY step must be a whole number, found ${displayType(c.type)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.expectKw('DO');
|
|
|
+ this.parseSeq(new Set(['END']), depth + 1);
|
|
|
+ this.expectKw('END');
|
|
|
+ }
|
|
|
+
|
|
|
+ /** WITH blocks use unqualified field access: skip without checks (conservative). */
|
|
|
+ private parseWith(): void {
|
|
|
+ this.next(); // WITH
|
|
|
+ let depth = 0;
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.kind === 'symbol') {
|
|
|
+ if (t.text === ';' && depth === 0) return;
|
|
|
+ if (t.text === '[' || t.text === '(' || t.text === '{') depth++;
|
|
|
+ else if (t.text === ']' || t.text === ')' || t.text === '}') depth = Math.max(0, depth - 1);
|
|
|
+ this.next();
|
|
|
+ } else if (t.kind === 'keyword' && t.text === 'DO' && depth === 0) {
|
|
|
+ this.next();
|
|
|
+ break;
|
|
|
+ } else if (t.kind === 'keyword') {
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ this.next();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let nest = 1;
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.kind === 'keyword' && OPENERS.has(t.text)) nest++;
|
|
|
+ else if (t.kind === 'keyword' && t.text === 'END') {
|
|
|
+ nest--;
|
|
|
+ this.next();
|
|
|
+ if (nest === 0) return;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.next();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseCase(depth: number): void {
|
|
|
+ this.next(); // CASE
|
|
|
+ const start = this.peek();
|
|
|
+ const sel = this.parseExpr(0);
|
|
|
+ if (sel.type.kind !== 'unknown' && !isOrdinal(sel.type)) {
|
|
|
+ this.issue(start ?? this.toks[this.pos - 1], `CASE selector must have an ordinal type, found ${displayType(sel.type)}`);
|
|
|
+ }
|
|
|
+ this.expectKw('OF');
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.kind === 'keyword' && t.text === 'END') { this.next(); return; }
|
|
|
+ if (t.kind === 'keyword' && t.text === 'ELSE') {
|
|
|
+ this.next();
|
|
|
+ this.parseSeq(new Set(['END']), depth + 1);
|
|
|
+ this.expectKw('END');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Skip case labels up to ':'.
|
|
|
+ let bdepth = 0;
|
|
|
+ for (;;) {
|
|
|
+ const u = this.peek();
|
|
|
+ if (!u) return;
|
|
|
+ if (u.kind === 'symbol') {
|
|
|
+ if (u.text === '[' || u.text === '(' || u.text === '{') bdepth++;
|
|
|
+ else if (u.text === ']' || u.text === ')' || u.text === '}') {
|
|
|
+ if (bdepth === 0) break;
|
|
|
+ bdepth--;
|
|
|
+ } else if (u.text === ':' && bdepth === 0) break;
|
|
|
+ this.next();
|
|
|
+ } else if (u.kind === 'keyword') {
|
|
|
+ break;
|
|
|
+ } else {
|
|
|
+ this.next();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (this.atSym(':')) this.next();
|
|
|
+ this.parseSeq(new Set(['|', 'ELSE', 'END']), depth + 1);
|
|
|
+ if (this.atSym('|')) { this.next(); continue; }
|
|
|
+ const v = this.peek();
|
|
|
+ if (v?.kind === 'keyword' && v.text === 'ELSE') continue;
|
|
|
+ if (v?.kind === 'keyword' && v.text === 'END') { this.next(); return; }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- Designators, calls, expressions ----
|
|
|
+
|
|
|
+ private resolveHead(t: Token): HeadInfo {
|
|
|
+ if (t.text === 'TRUE' || t.text === 'FALSE') return { kind: 'literal', type: { kind: 'bool' } };
|
|
|
+ if (t.text === 'NIL') return { kind: 'literal', type: { kind: 'nil' } };
|
|
|
+ const r = resolveName(this.ctx.unit, this.ctx.docDir, t.line, t.ch, null, t.text);
|
|
|
+ if (r) {
|
|
|
+ const sameFile = r.filePath === this.ctx.unit.filePath;
|
|
|
+ const host = sameFile ? this.ctx.unit : unitOfFile(r.filePath) ?? this.ctx.unit;
|
|
|
+ const dir = sameFile ? this.ctx.docDir : path.dirname(r.filePath);
|
|
|
+ switch (r.sym.kind) {
|
|
|
+ case 'variable': case 'parameter': return { kind: 'var', sym: r.sym, host, dir };
|
|
|
+ case 'procedure': return { kind: 'proc', sym: r.sym, host, dir };
|
|
|
+ case 'constant': return { kind: 'const', sym: r.sym, host, dir };
|
|
|
+ case 'type': return { kind: 'type' };
|
|
|
+ case 'module': return { kind: 'module', name: r.sym.name };
|
|
|
+ case 'field': return { kind: 'field' };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (TYPE_BUILTINS.has(t.text)) return { kind: 'builtinType', name: t.text };
|
|
|
+ if (PROC_BUILTINS.has(t.text)) return { kind: 'builtinProc', name: t.text };
|
|
|
+ return { kind: 'unknown' };
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseDesignatorStatement(): void {
|
|
|
+ const head = this.next()!;
|
|
|
+ const d = this.parseDesignatorRest(head, 'stmt');
|
|
|
+ if (this.atSym(':=')) {
|
|
|
+ const op = this.next()!;
|
|
|
+ this.checkAssignment(d, op);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Parse qualifier chain, postfixes and an optional call. Emits call/operand issues. */
|
|
|
+ private parseDesignatorRest(head: Token, mode: 'expr' | 'stmt'): Value {
|
|
|
+ let info = this.resolveHead(head);
|
|
|
+ let tok = head;
|
|
|
+ // Qualifier chain: M.X or r.field (unknown-typed beyond the head).
|
|
|
+ while (this.atSym('.')) {
|
|
|
+ this.next(); // .
|
|
|
+ const nm = this.peek();
|
|
|
+ if (!nm || nm.kind !== 'ident') break;
|
|
|
+ this.next();
|
|
|
+ tok = nm;
|
|
|
+ if (info.kind === 'module') {
|
|
|
+ const q = resolveQualified(this.ctx.unit, this.ctx.docDir, info.name, nm.text);
|
|
|
+ if (!q) { info = { kind: 'unknown' }; continue; }
|
|
|
+ switch (q.sym.kind) {
|
|
|
+ case 'variable': case 'parameter': info = { kind: 'var', sym: q.sym, host: q.host, dir: q.dir }; break;
|
|
|
+ case 'procedure': info = { kind: 'proc', sym: q.sym, host: q.host, dir: q.dir }; break;
|
|
|
+ case 'constant': info = { kind: 'const', sym: q.sym, host: q.host, dir: q.dir }; break;
|
|
|
+ case 'type': info = { kind: 'type' }; break;
|
|
|
+ default: info = { kind: 'unknown' }; break;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ info = { kind: 'field' };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Postfix: indexing and dereferencing.
|
|
|
+ let type = this.valueType(info, tok);
|
|
|
+ for (;;) {
|
|
|
+ if (this.atSym('[')) {
|
|
|
+ const open = this.next()!;
|
|
|
+ const idxTypes = this.parseExprList();
|
|
|
+ if (this.atSym(']')) this.next();
|
|
|
+ if (type.kind === 'array') {
|
|
|
+ for (const it of idxTypes) {
|
|
|
+ if (it.kind !== 'unknown' && !isOrdinal(it)) {
|
|
|
+ this.issue(open, `Array index must have an ordinal type, found ${displayType(it)}`);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ type = type.element;
|
|
|
+ } else if (type.kind !== 'unknown') {
|
|
|
+ this.issue(open, `Cannot index ${displayType(type)}`);
|
|
|
+ type = UNKNOWN;
|
|
|
+ }
|
|
|
+ } else if (this.atSym('^')) {
|
|
|
+ const op = this.next()!;
|
|
|
+ if (type.kind === 'pointer') {
|
|
|
+ type = type.target;
|
|
|
+ } else if (type.kind !== 'unknown') {
|
|
|
+ this.issue(op, `Cannot dereference ${displayType(type)}`);
|
|
|
+ type = UNKNOWN;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Optional call.
|
|
|
+ if (this.atSym('(')) {
|
|
|
+ const call = this.parseCall(info, tok);
|
|
|
+ if (mode === 'stmt') {
|
|
|
+ if (call.returnsValue) {
|
|
|
+ this.issue(tok, `Return value of function "${nameOf(info, tok)}" is ignored`);
|
|
|
+ }
|
|
|
+ return { type: UNKNOWN, info, tok };
|
|
|
+ }
|
|
|
+ if (call.isVoid) {
|
|
|
+ this.issue(tok, `Procedure "${nameOf(info, tok)}" does not return a value`);
|
|
|
+ return { type: UNKNOWN, info, tok };
|
|
|
+ }
|
|
|
+ type = call.type;
|
|
|
+ } else if (mode === 'expr') {
|
|
|
+ if (info.kind === 'proc') {
|
|
|
+ const checked = this.checkCall(info, [], tok);
|
|
|
+ if (checked.isVoid) this.issue(tok, `Procedure "${info.sym.name}" does not return a value`);
|
|
|
+ type = checked.type;
|
|
|
+ } else if (info.kind === 'type' || info.kind === 'builtinType') {
|
|
|
+ this.issue(tok, `"${tok.text}" is a type and cannot be used as a value`);
|
|
|
+ type = UNKNOWN;
|
|
|
+ } else if (info.kind === 'module') {
|
|
|
+ this.issue(tok, `"${tok.text}" is a module and cannot be used as a value`);
|
|
|
+ type = UNKNOWN;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Statement without call or assignment: bare procedure call `P;`.
|
|
|
+ // (When ':=' follows, the assignment path reports the error instead.)
|
|
|
+ if (info.kind === 'proc' && !this.atSym(':=')) {
|
|
|
+ const checked = this.checkCall(info, [], tok);
|
|
|
+ if (!checked.isVoid) this.issue(tok, `Return value of function "${info.sym.name}" is ignored`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { type, info, tok };
|
|
|
+ }
|
|
|
+
|
|
|
+ private valueType(info: HeadInfo, tok: Token): M2Type {
|
|
|
+ switch (info.kind) {
|
|
|
+ case 'var': return resolveDeclaredType(info.sym, info.host, info.dir, tok.line, tok.ch);
|
|
|
+ case 'const': return resolveDeclaredType(info.sym, info.host, info.dir, tok.line, tok.ch);
|
|
|
+ case 'literal': return info.type;
|
|
|
+ default: return UNKNOWN;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseCall(info: HeadInfo, tok: Token): { type: M2Type; isVoid: boolean; returnsValue: boolean } {
|
|
|
+ this.next(); // (
|
|
|
+ const args = this.parseExprList();
|
|
|
+ if (this.atSym(')')) this.next();
|
|
|
+ if (info.kind === 'proc') return this.checkCall(info, args, tok);
|
|
|
+ // Builtin procedures, casts through type names and unknown callees: unchecked.
|
|
|
+ if (info.kind === 'builtinProc' || info.kind === 'type' || info.kind === 'builtinType' ||
|
|
|
+ info.kind === 'unknown' || info.kind === 'field') {
|
|
|
+ return { type: UNKNOWN, isVoid: false, returnsValue: false };
|
|
|
+ }
|
|
|
+ this.issue(tok, `"${tok.text}" is not a procedure`);
|
|
|
+ return { type: UNKNOWN, isVoid: false, returnsValue: false };
|
|
|
+ }
|
|
|
+
|
|
|
+ private checkCall(
|
|
|
+ info: { kind: 'proc'; sym: M2Symbol; host: M2Unit; dir: string },
|
|
|
+ args: M2Type[], tok: Token,
|
|
|
+ ): { type: M2Type; isVoid: boolean; returnsValue: boolean } {
|
|
|
+ const params = info.sym.children.filter(c => c.kind === 'parameter');
|
|
|
+ const required = params.filter(p => !/\(optional\)\s*$/.test(p.detail)).length;
|
|
|
+ if (args.length < required || args.length > params.length) {
|
|
|
+ const want = required === params.length
|
|
|
+ ? `${params.length} argument${params.length === 1 ? '' : 's'}`
|
|
|
+ : `${required}..${params.length} arguments`;
|
|
|
+ this.issue(tok, `Procedure "${info.sym.name}" expects ${want} but ${args.length} given`);
|
|
|
+ }
|
|
|
+ for (let k = 0; k < Math.min(args.length, params.length); k++) {
|
|
|
+ const p = params[k];
|
|
|
+ const pt = this.paramType(p);
|
|
|
+ if (pt.kind !== 'unknown' && args[k].kind !== 'unknown' && !isArrayArgCompatible(pt, args[k])) {
|
|
|
+ this.issue(tok, `Argument ${k + 1} of "${info.sym.name}": cannot assign ${displayType(args[k])} to ${displayType(pt)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const ret = returnTypeOf(info.sym, info.host, info.dir);
|
|
|
+ if (!ret) return { type: UNKNOWN, isVoid: true, returnsValue: false };
|
|
|
+ return { type: ret, isVoid: false, returnsValue: true };
|
|
|
+ }
|
|
|
+
|
|
|
+ private paramType(p: M2Symbol): M2Type {
|
|
|
+ const clean = p.detail.replace(/\(optional\)\s*$/, '').trim();
|
|
|
+ const colon = clean.indexOf(':');
|
|
|
+ if (colon < 0) return UNKNOWN;
|
|
|
+ return resolveType(
|
|
|
+ parseTypeText(clean.slice(colon + 1)),
|
|
|
+ this.ctx.unit, this.ctx.docDir, p.nameRange.startLine, p.nameRange.startCh,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private checkAssignment(d: Value, op: Token): void {
|
|
|
+ const rhs = this.parseExpr(0);
|
|
|
+ const info = d.info;
|
|
|
+ if (info.kind === 'const' || info.kind === 'literal') {
|
|
|
+ this.issue(d.tok, `Cannot assign to constant "${d.tok.text}"`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (info.kind === 'proc') {
|
|
|
+ this.issue(d.tok, `Cannot assign to procedure "${d.tok.text}"`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (info.kind === 'type' || info.kind === 'builtinType') {
|
|
|
+ this.issue(d.tok, `"${d.tok.text}" is a type and cannot be assigned to`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (info.kind === 'module') {
|
|
|
+ this.issue(d.tok, `"${d.tok.text}" is a module and cannot be assigned to`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (info.kind === 'unknown' || info.kind === 'field' || info.kind === 'builtinProc') return;
|
|
|
+ const lhs = d.type;
|
|
|
+ if (lhs.kind !== 'unknown' && rhs.type.kind !== 'unknown' && !isAssignable(lhs, rhs.type)) {
|
|
|
+ this.issue(op, `Cannot assign ${displayType(rhs.type)} to ${displayType(lhs)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseExprList(): M2Type[] {
|
|
|
+ const out: M2Type[] = [];
|
|
|
+ if (this.atSym(')') || this.atSym(']')) return out;
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ // NOT can open an expression; any other keyword ends the list.
|
|
|
+ if (!t || t.kind === 'eof' || (t.kind === 'keyword' && t.text !== 'NOT') ||
|
|
|
+ (t.kind === 'symbol' && (t.text === ')' || t.text === ']' || t.text === ';'))) break;
|
|
|
+ out.push(this.parseExpr(0).type);
|
|
|
+ if (this.atSym(',')) { this.next(); continue; }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- Expressions (precedence climbing) ----
|
|
|
+
|
|
|
+ private parseExpr(depth: number): Value {
|
|
|
+ let left = this.parseSimple(depth);
|
|
|
+ for (;;) {
|
|
|
+ const op = this.peek();
|
|
|
+ if (!op || op.kind !== 'symbol' || !['=', '#', '<', '<=', '>', '>='].includes(op.text)) break;
|
|
|
+ const ordering = op.text !== '=' && op.text !== '#';
|
|
|
+ this.next();
|
|
|
+ const right = this.parseSimple(depth);
|
|
|
+ if (left.type.kind !== 'unknown' && right.type.kind !== 'unknown' &&
|
|
|
+ !isComparable(left.type, right.type, ordering)) {
|
|
|
+ this.issue(op, `Cannot compare ${displayType(left.type)} and ${displayType(right.type)}`);
|
|
|
+ }
|
|
|
+ left = { type: { kind: 'bool' }, info: { kind: 'unknown' }, tok: op };
|
|
|
+ }
|
|
|
+ // Membership: result BOOLEAN, operands unchecked (set element types are unknown).
|
|
|
+ if (this.atKw('IN')) {
|
|
|
+ this.next();
|
|
|
+ this.parseSimple(depth);
|
|
|
+ left = { type: { kind: 'bool' }, info: { kind: 'unknown' }, tok: left.tok };
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseSimple(depth: number): Value {
|
|
|
+ const t = this.peek();
|
|
|
+ if (t?.kind === 'symbol' && (t.text === '+' || t.text === '-')) {
|
|
|
+ const op = this.next()!;
|
|
|
+ const e = this.parseSimple(depth);
|
|
|
+ if (e.type.kind !== 'unknown' && arithKind(e.type) !== 'whole' && arithKind(e.type) !== 'real') {
|
|
|
+ this.issue(op, `Cannot apply "${op.text}" to ${displayType(e.type)}`);
|
|
|
+ }
|
|
|
+ return { type: e.type, info: e.info, tok: op };
|
|
|
+ }
|
|
|
+ let left = this.parseTerm(depth);
|
|
|
+ for (;;) {
|
|
|
+ const op = this.peek();
|
|
|
+ if (!op) break;
|
|
|
+ if (op.kind === 'keyword' && op.text === 'OR') {
|
|
|
+ this.next();
|
|
|
+ const right = this.parseTerm(depth);
|
|
|
+ this.checkBoolOperands(op, left.type, right.type);
|
|
|
+ left = { type: { kind: 'bool' }, info: { kind: 'unknown' }, tok: op };
|
|
|
+ } else if (op.kind === 'symbol' && (op.text === '+' || op.text === '-')) {
|
|
|
+ this.next();
|
|
|
+ const right = this.parseTerm(depth);
|
|
|
+ left = { type: this.arithResult(op, left.type, right.type), info: { kind: 'unknown' }, tok: op };
|
|
|
+ } else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseTerm(depth: number): Value {
|
|
|
+ let left = this.parseFactor(depth);
|
|
|
+ for (;;) {
|
|
|
+ const op = this.peek();
|
|
|
+ if (!op) break;
|
|
|
+ if (op.kind === 'keyword' && op.text === 'AND') {
|
|
|
+ this.next();
|
|
|
+ const right = this.parseFactor(depth);
|
|
|
+ this.checkBoolOperands(op, left.type, right.type);
|
|
|
+ left = { type: { kind: 'bool' }, info: { kind: 'unknown' }, tok: op };
|
|
|
+ } else if (op.kind === 'symbol' && (op.text === '*' || op.text === '/')) {
|
|
|
+ this.next();
|
|
|
+ const right = this.parseFactor(depth);
|
|
|
+ left = { type: this.arithResult(op, left.type, right.type), info: { kind: 'unknown' }, tok: op };
|
|
|
+ } else if (op.kind === 'keyword' && (op.text === 'DIV' || op.text === 'MOD' || op.text === 'REM')) {
|
|
|
+ this.next();
|
|
|
+ const right = this.parseFactor(depth);
|
|
|
+ left = { type: this.divResult(op, left.type, right.type), info: { kind: 'unknown' }, tok: op };
|
|
|
+ } else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private arithResult(op: Token, a: M2Type, b: M2Type): M2Type {
|
|
|
+ const ka = arithKind(a);
|
|
|
+ const kb = arithKind(b);
|
|
|
+ if (ka === 'unknown' || kb === 'unknown') return UNKNOWN;
|
|
|
+ if (ka === 'set' && kb === 'set') return { kind: 'bitset' };
|
|
|
+ if ((ka === 'whole' || ka === 'real') && (kb === 'whole' || kb === 'real')) {
|
|
|
+ if (ka !== kb) {
|
|
|
+ this.issue(op, `Cannot apply "${op.text}" to ${displayType(a)} and ${displayType(b)}`);
|
|
|
+ return UNKNOWN;
|
|
|
+ }
|
|
|
+ if (ka === 'real') {
|
|
|
+ const lit = a.kind === 'real' && b.kind === 'real' && a.literal && b.literal;
|
|
|
+ return { kind: 'real', id: 'REAL', literal: lit };
|
|
|
+ }
|
|
|
+ const lit = a.kind === 'whole' && b.kind === 'whole' && a.literal && b.literal;
|
|
|
+ return { kind: 'whole', id: 'INTEGER', literal: lit };
|
|
|
+ }
|
|
|
+ this.issue(op, `Cannot apply "${op.text}" to ${displayType(a)} and ${displayType(b)}`);
|
|
|
+ return UNKNOWN;
|
|
|
+ }
|
|
|
+
|
|
|
+ private divResult(op: Token, a: M2Type, b: M2Type): M2Type {
|
|
|
+ const ka = arithKind(a);
|
|
|
+ const kb = arithKind(b);
|
|
|
+ if (ka === 'unknown' || kb === 'unknown') return UNKNOWN;
|
|
|
+ if (ka === 'whole' && kb === 'whole') {
|
|
|
+ const lit = a.kind === 'whole' && b.kind === 'whole' && a.literal && b.literal;
|
|
|
+ return { kind: 'whole', id: 'INTEGER', literal: lit };
|
|
|
+ }
|
|
|
+ this.issue(op, `Operator "${op.text}" requires whole-number operands`);
|
|
|
+ return UNKNOWN;
|
|
|
+ }
|
|
|
+
|
|
|
+ private checkBoolOperands(op: Token, a: M2Type, b: M2Type): void {
|
|
|
+ for (const t of [a, b]) {
|
|
|
+ if (t.kind !== 'unknown' && t.kind !== 'bool') {
|
|
|
+ this.issue(op, `Operator "${op.text}" requires BOOLEAN operands, found ${displayType(t)}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseFactor(depth: number): Value {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return { type: UNKNOWN, info: { kind: 'unknown' }, tok: this.toks[this.pos - 1] };
|
|
|
+ if (t.kind === 'keyword' && t.text === 'NOT') {
|
|
|
+ this.next();
|
|
|
+ const e = this.parseFactor(depth);
|
|
|
+ if (e.type.kind !== 'unknown' && e.type.kind !== 'bool') {
|
|
|
+ this.issue(t, `Operator "NOT" requires a BOOLEAN operand, found ${displayType(e.type)}`);
|
|
|
+ }
|
|
|
+ return { type: { kind: 'bool' }, info: { kind: 'unknown' }, tok: t };
|
|
|
+ }
|
|
|
+ if (t.kind === 'symbol' && (t.text === '+' || t.text === '-')) {
|
|
|
+ const op = this.next()!;
|
|
|
+ const e = this.parseFactor(depth);
|
|
|
+ if (e.type.kind !== 'unknown' && arithKind(e.type) !== 'whole' && arithKind(e.type) !== 'real') {
|
|
|
+ this.issue(op, `Cannot apply "${op.text}" to ${displayType(e.type)}`);
|
|
|
+ }
|
|
|
+ return { type: e.type, info: e.info, tok: op };
|
|
|
+ }
|
|
|
+ if (t.kind === 'symbol' && t.text === '(') {
|
|
|
+ this.next();
|
|
|
+ if (depth > MAX_EXPR_DEPTH) {
|
|
|
+ this.skipBalancedRest();
|
|
|
+ return { type: UNKNOWN, info: { kind: 'unknown' }, tok: t };
|
|
|
+ }
|
|
|
+ const e = this.parseExpr(depth + 1);
|
|
|
+ if (this.atSym(')')) this.next();
|
|
|
+ return { type: e.type, info: e.info, tok: t };
|
|
|
+ }
|
|
|
+ if (t.kind === 'symbol' && t.text === '{') {
|
|
|
+ this.next();
|
|
|
+ this.parseSetConstructor();
|
|
|
+ return { type: { kind: 'setLit' }, info: { kind: 'unknown' }, tok: t };
|
|
|
+ }
|
|
|
+ if (t.kind === 'number') {
|
|
|
+ this.next();
|
|
|
+ return {
|
|
|
+ type: /[.eE]/.test(t.text)
|
|
|
+ ? { kind: 'real', id: 'REAL', literal: true }
|
|
|
+ : { kind: 'whole', id: 'INTEGER', literal: true },
|
|
|
+ info: { kind: 'unknown' }, tok: t,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (t.kind === 'string') {
|
|
|
+ this.next();
|
|
|
+ const inner = t.text.length >= 2 ? t.text.slice(1, -1) : '';
|
|
|
+ return { type: { kind: 'stringLit', length: inner.length }, info: { kind: 'unknown' }, tok: t };
|
|
|
+ }
|
|
|
+ if (t.kind === 'ident') {
|
|
|
+ const head = this.next()!;
|
|
|
+ return this.parseDesignatorRest(head, 'expr');
|
|
|
+ }
|
|
|
+ // Anything else cannot start an operand: stop without consuming.
|
|
|
+ return { type: UNKNOWN, info: { kind: 'unknown' }, tok: t };
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseSetConstructor(): void {
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t || (t.kind === 'symbol' && t.text === '}')) break;
|
|
|
+ this.parseExpr(0);
|
|
|
+ if (this.atSym('..')) {
|
|
|
+ this.next();
|
|
|
+ this.parseExpr(0);
|
|
|
+ }
|
|
|
+ if (this.atSym(',')) { this.next(); continue; }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (this.atSym('}')) this.next();
|
|
|
+ }
|
|
|
+
|
|
|
+ private skipBalancedRest(): void {
|
|
|
+ let depth = 1;
|
|
|
+ while (this.pos < this.end) {
|
|
|
+ const t = this.next();
|
|
|
+ if (!t || t.kind === 'eof') return;
|
|
|
+ if (t.kind === 'symbol' && t.text === '(') depth++;
|
|
|
+ else if (t.kind === 'symbol' && t.text === ')') {
|
|
|
+ depth--;
|
|
|
+ if (depth === 0) return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function nameOf(info: HeadInfo, tok: Token): string {
|
|
|
+ if (info.kind === 'proc') return info.sym.name;
|
|
|
+ return tok.text;
|
|
|
+}
|