|
|
@@ -16,8 +16,8 @@ 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,
|
|
|
+ M2Type, UNKNOWN, arithKind, arithResultType, displayType, isArrayArgCompatible, isAssignable,
|
|
|
+ isComparable, isOrdinal, numberLiteralType, parseTypeText, resolveDeclaredType, resolveQualified,
|
|
|
resolveType, returnTypeOf, unitOfFile,
|
|
|
} from './types';
|
|
|
import * as path from 'path';
|
|
|
@@ -30,7 +30,7 @@ const TYPE_BUILTINS = new Set([
|
|
|
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',
|
|
|
+ 'ORDL', 'SIZE', 'TRUNC', 'TSIZE', 'VAL', 'CODE', 'COPY', 'FILL', 'LENGTH',
|
|
|
]);
|
|
|
|
|
|
const OPENERS = new Set(['BEGIN', 'IF', 'CASE', 'LOOP', 'WHILE', 'FOR', 'WITH', 'RECORD', 'MODULE']);
|
|
|
@@ -95,6 +95,10 @@ function bodyIndexes(tokens: Token[], sl: number, sc: number, el: number, ec: nu
|
|
|
class Walker {
|
|
|
private pos: number;
|
|
|
private returnType: M2Type | null | undefined;
|
|
|
+ /** Depth of nested builtin-procedure argument lists (type args allowed). */
|
|
|
+ private builtinArgs = 0;
|
|
|
+ /** Parsing the right-hand side of an assignment to a procedure variable. */
|
|
|
+ private expectingProc = false;
|
|
|
constructor(
|
|
|
private ctx: Ctx,
|
|
|
private toks: Token[],
|
|
|
@@ -212,7 +216,8 @@ class Walker {
|
|
|
}
|
|
|
const ret = this.procReturn();
|
|
|
const t = this.peek();
|
|
|
- if (!t || t.text === ';' || t.kind === 'keyword') {
|
|
|
+ // `;`, `|` (CASE separator), `ELSE`/`END`/etc. terminate a bare RETURN.
|
|
|
+ if (!t || t.text === ';' || t.text === '|' || t.kind === 'keyword') {
|
|
|
if (ret) this.issue(kw, `RETURN without a value in a function returning ${displayType(ret)}`);
|
|
|
return;
|
|
|
}
|
|
|
@@ -445,7 +450,14 @@ class Walker {
|
|
|
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; }
|
|
|
+ if (!q) {
|
|
|
+ // Qualified builtin (`SYSTEM.TSIZE`, `SYSTEM.ADR`, ...): the defining
|
|
|
+ // `.def` is usually unavailable, but the name is still a builtin call.
|
|
|
+ info = PROC_BUILTINS.has(nm.text)
|
|
|
+ ? { kind: 'builtinProc', name: nm.text }
|
|
|
+ : { 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;
|
|
|
@@ -464,17 +476,21 @@ class Walker {
|
|
|
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)) {
|
|
|
+ // Consume one dimension per index: gm2 flattens `a[i, j]` over
|
|
|
+ // nested arrays (`symSet[0, i]` means `symSet[0][i]`).
|
|
|
+ let badIndex = false;
|
|
|
+ for (const it of idxTypes) {
|
|
|
+ if (type.kind === 'array') {
|
|
|
+ if (!badIndex && it.kind !== 'unknown' && !isOrdinal(it)) {
|
|
|
this.issue(open, `Array index must have an ordinal type, found ${displayType(it)}`);
|
|
|
- break;
|
|
|
+ badIndex = true;
|
|
|
}
|
|
|
+ type = type.element;
|
|
|
+ } else if (type.kind !== 'unknown') {
|
|
|
+ this.issue(open, `Cannot index ${displayType(type)}`);
|
|
|
+ type = UNKNOWN;
|
|
|
+ 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()!;
|
|
|
@@ -484,10 +500,25 @@ class Walker {
|
|
|
this.issue(op, `Cannot dereference ${displayType(type)}`);
|
|
|
type = UNKNOWN;
|
|
|
}
|
|
|
+ } else if (this.atSym('.')) {
|
|
|
+ // Field selection after indexing or dereferencing (`f^.x`, `a[i].x`).
|
|
|
+ // (Direct `r.x` is consumed by the qualifier chain above.)
|
|
|
+ this.next(); // .
|
|
|
+ const nm = this.peek();
|
|
|
+ if (nm?.kind === 'ident') this.next();
|
|
|
+ // Field types are untracked: any structured value yields unknown.
|
|
|
+ type = UNKNOWN;
|
|
|
+ info = { kind: 'field' };
|
|
|
} else {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
+ // Set constructor with explicit type (`BITSET{...}`, `MySet{...}`).
|
|
|
+ if ((info.kind === 'type' || info.kind === 'builtinType') && this.atSym('{')) {
|
|
|
+ this.next();
|
|
|
+ this.parseSetConstructor();
|
|
|
+ return { type: { kind: 'setLit' }, info: { kind: 'unknown' }, tok };
|
|
|
+ }
|
|
|
// Optional call.
|
|
|
if (this.atSym('(')) {
|
|
|
const call = this.parseCall(info, tok);
|
|
|
@@ -504,11 +535,18 @@ class Walker {
|
|
|
type = call.type;
|
|
|
} else if (mode === 'expr') {
|
|
|
if (info.kind === 'proc') {
|
|
|
+ // A bare procedure name on the right of a `P := Q` assignment
|
|
|
+ // denotes the procedure itself, not a call.
|
|
|
+ if (this.expectingProc) return { type: { kind: 'proc' }, info, tok };
|
|
|
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 names are legal arguments to builtin procedures
|
|
|
+ // (`SIZE(T)`, `TSIZE(T)`, `VAL(T, x)`, `MAX(T)`).
|
|
|
+ if (this.builtinArgs === 0) {
|
|
|
+ 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`);
|
|
|
@@ -535,12 +573,31 @@ class Walker {
|
|
|
}
|
|
|
|
|
|
private parseCall(info: HeadInfo, tok: Token): { type: M2Type; isVoid: boolean; returnsValue: boolean } {
|
|
|
+ // Builtin procedures (`SIZE`, `VAL`, `MAX`, ...) accept type names and
|
|
|
+ // are otherwise unchecked: parse arguments in lenient mode.
|
|
|
+ if (info.kind === 'builtinProc') {
|
|
|
+ this.next(); // (
|
|
|
+ this.builtinArgs++;
|
|
|
+ try {
|
|
|
+ this.parseExprList();
|
|
|
+ if (this.atSym(')')) this.next();
|
|
|
+ } finally {
|
|
|
+ this.builtinArgs--;
|
|
|
+ }
|
|
|
+ return { type: UNKNOWN, isVoid: false, returnsValue: false };
|
|
|
+ }
|
|
|
this.next(); // (
|
|
|
const args = this.parseExprList();
|
|
|
if (this.atSym(')')) this.next();
|
|
|
if (info.kind === 'proc') return this.checkCall(info, args, tok);
|
|
|
+ // Calls through procedure variables (`handler(x)`): signatures are
|
|
|
+ // untracked, so such calls are unchecked but legal.
|
|
|
+ if (info.kind === 'var' || info.kind === 'const') {
|
|
|
+ const vt = this.valueType(info, tok);
|
|
|
+ if (vt.kind === 'proc') return { type: UNKNOWN, isVoid: false, returnsValue: false };
|
|
|
+ }
|
|
|
// Builtin procedures, casts through type names and unknown callees: unchecked.
|
|
|
- if (info.kind === 'builtinProc' || info.kind === 'type' || info.kind === 'builtinType' ||
|
|
|
+ if (info.kind === 'type' || info.kind === 'builtinType' ||
|
|
|
info.kind === 'unknown' || info.kind === 'field') {
|
|
|
return { type: UNKNOWN, isVoid: false, returnsValue: false };
|
|
|
}
|
|
|
@@ -583,8 +640,22 @@ class Walker {
|
|
|
}
|
|
|
|
|
|
private checkAssignment(d: Value, op: Token): void {
|
|
|
- const rhs = this.parseExpr(0);
|
|
|
const info = d.info;
|
|
|
+ // Assignments to procedure variables accept bare procedure designators.
|
|
|
+ const lhsProc = info.kind === 'var' && d.type.kind === 'proc';
|
|
|
+ let rhs: Value;
|
|
|
+ if (lhsProc) {
|
|
|
+ this.expectingProc = true;
|
|
|
+ try {
|
|
|
+ rhs = this.parseExpr(0);
|
|
|
+ } finally {
|
|
|
+ this.expectingProc = false;
|
|
|
+ }
|
|
|
+ if (rhs.type.kind === 'proc' || rhs.type.kind === 'unknown') return;
|
|
|
+ this.issue(op, `Cannot assign ${displayType(rhs.type)} to procedure variable "${d.tok.text}"`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ rhs = this.parseExpr(0);
|
|
|
if (info.kind === 'const' || info.kind === 'literal') {
|
|
|
this.issue(d.tok, `Cannot assign to constant "${d.tok.text}"`);
|
|
|
return;
|
|
|
@@ -704,36 +775,25 @@ class Walker {
|
|
|
}
|
|
|
|
|
|
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 };
|
|
|
+ const t = arithResultType(a, b);
|
|
|
+ if (!t) {
|
|
|
+ this.issue(op, `Cannot apply "${op.text}" to ${displayType(a)} and ${displayType(b)}`);
|
|
|
+ return UNKNOWN;
|
|
|
}
|
|
|
- this.issue(op, `Cannot apply "${op.text}" to ${displayType(a)} and ${displayType(b)}`);
|
|
|
- return UNKNOWN;
|
|
|
+ return t;
|
|
|
}
|
|
|
|
|
|
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 };
|
|
|
+ const t = arithResultType(a, b);
|
|
|
+ if (!t) {
|
|
|
+ this.issue(op, `Operator "${op.text}" requires whole-number operands`);
|
|
|
+ return UNKNOWN;
|
|
|
}
|
|
|
- this.issue(op, `Operator "${op.text}" requires whole-number operands`);
|
|
|
- return UNKNOWN;
|
|
|
+ if (t.kind !== 'unknown' && arithKind(t) !== 'whole') {
|
|
|
+ this.issue(op, `Operator "${op.text}" requires whole-number operands`);
|
|
|
+ return UNKNOWN;
|
|
|
+ }
|
|
|
+ return t;
|
|
|
}
|
|
|
|
|
|
private checkBoolOperands(op: Token, a: M2Type, b: M2Type): void {
|
|
|
@@ -782,9 +842,7 @@ class Walker {
|
|
|
if (t.kind === 'number') {
|
|
|
this.next();
|
|
|
return {
|
|
|
- type: /[.eE]/.test(t.text)
|
|
|
- ? { kind: 'real', id: 'REAL', literal: true }
|
|
|
- : { kind: 'whole', id: 'INTEGER', literal: true },
|
|
|
+ type: numberLiteralType(t.text),
|
|
|
info: { kind: 'unknown' }, tok: t,
|
|
|
};
|
|
|
}
|