|
|
@@ -530,6 +530,13 @@ class Walker {
|
|
|
this.issue(tok, `"${tok.text}" is a type and cannot be used as a value`);
|
|
|
}
|
|
|
type = UNKNOWN;
|
|
|
+ } else if (info.kind === 'builtinProc') {
|
|
|
+ // A bare builtin name used as a value (`i := INC;`): check it as a
|
|
|
+ // call with no arguments so arity is reported; void builtins report
|
|
|
+ // through the generic "does not return a value" path below.
|
|
|
+ const checked = this.checkBuiltinCall(nameOf(info, tok), [], tok);
|
|
|
+ if (checked.isVoid) this.issue(tok, `Procedure "${tok.text}" does not return a value`);
|
|
|
+ type = checked.type;
|
|
|
} else if (info.kind === 'module') {
|
|
|
this.issue(tok, `"${tok.text}" is a module and cannot be used as a value`);
|
|
|
type = UNKNOWN;
|
|
|
@@ -540,6 +547,8 @@ class Walker {
|
|
|
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`);
|
|
|
+ } else if (info.kind === 'builtinProc' && !this.atSym(':=')) {
|
|
|
+ this.checkBuiltinCall(info.name, [], tok);
|
|
|
}
|
|
|
}
|
|
|
return { type, info, tok };
|
|
|
@@ -606,7 +615,7 @@ class Walker {
|
|
|
for (;;) {
|
|
|
if (this.atSym('[')) {
|
|
|
const open = this.next()!;
|
|
|
- const idxTypes = this.parseExprList();
|
|
|
+ const idxTypes = this.parseExprList().map(v => v.type);
|
|
|
if (this.atSym(']')) this.next();
|
|
|
// Consume one dimension per index: gm2 flattens `a[i, j]` over
|
|
|
// nested arrays (`symSet[0, i]` means `symSet[0][i]`).
|
|
|
@@ -659,23 +668,25 @@ 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.
|
|
|
+ // Builtin procedures (`SIZE`, `VAL`, `MAX`, ...): arguments are parsed
|
|
|
+ // in lenient mode (type names allowed) and checked against the
|
|
|
+ // gm2-calibrated signatures in `checkBuiltinCall`.
|
|
|
if (info.kind === 'builtinProc') {
|
|
|
this.next(); // (
|
|
|
this.builtinArgs++;
|
|
|
+ let args: Value[];
|
|
|
try {
|
|
|
- this.parseExprList();
|
|
|
+ args = this.parseExprList();
|
|
|
if (this.atSym(')')) this.next();
|
|
|
} finally {
|
|
|
this.builtinArgs--;
|
|
|
}
|
|
|
- return { type: UNKNOWN, isVoid: false, returnsValue: false };
|
|
|
+ return this.checkBuiltinCall(info.name, args, tok);
|
|
|
}
|
|
|
this.next(); // (
|
|
|
- const args = this.parseExprList();
|
|
|
+ const argValues = this.parseExprList();
|
|
|
if (this.atSym(')')) this.next();
|
|
|
- if (info.kind === 'proc') return this.checkCall(info, args, tok);
|
|
|
+ if (info.kind === 'proc') return this.checkCall(info, argValues.map(a => a.type), tok);
|
|
|
// Calls through procedure variables (`handler(x)`): signatures are
|
|
|
// untracked, so such calls are unchecked but legal.
|
|
|
if (info.kind === 'var' || info.kind === 'const') {
|
|
|
@@ -725,6 +736,200 @@ class Walker {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ /** Arity and argument checking for pervasive builtin procedures.
|
|
|
+ *
|
|
|
+ * Every rule below was probed against real gm2 (16.0.1); uncertain
|
|
|
+ * cases stay silent. `HALT`/`NEW`/`DISPOSE` accept any argument list
|
|
|
+ * (halt codes and `ALLOCATE` variant tags are unknowable), and the
|
|
|
+ * murky `LENGTH`/`TSIZE`/`CODE`/`COPY`/`FILL` (undefined unqualified
|
|
|
+ * in this gm2; `TSIZE` even accepts what `SIZE` rejects) stay fully
|
|
|
+ * unchecked, exactly as before.
|
|
|
+ */
|
|
|
+ private checkBuiltinCall(
|
|
|
+ name: string, args: Value[], tok: Token,
|
|
|
+ ): { type: M2Type; isVoid: boolean; returnsValue: boolean } {
|
|
|
+ const fun = (type: M2Type) => ({ type, isVoid: false, returnsValue: true });
|
|
|
+ const proc = () => ({ type: UNKNOWN, isVoid: true, returnsValue: false });
|
|
|
+ const silent = () => ({ type: UNKNOWN, isVoid: false, returnsValue: false });
|
|
|
+ const want = (min: number, max: number): boolean => {
|
|
|
+ if (args.length < min || args.length > max) {
|
|
|
+ const n = min === max
|
|
|
+ ? `${min} argument${min === 1 ? '' : 's'}`
|
|
|
+ : `${min}..${max} arguments`;
|
|
|
+ this.issue(tok, `Procedure "${name}" expects ${n} but ${args.length} given`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+ const expectKind = (arg: Value, n: number, ok: boolean, what: string): void => {
|
|
|
+ if (!ok) {
|
|
|
+ this.issue(arg.tok, `Argument ${n} of "${name}": expected ${what}, found ${displayType(arg.type)}`);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ switch (name) {
|
|
|
+ case 'INC':
|
|
|
+ case 'DEC': {
|
|
|
+ if (!want(1, 2)) return proc();
|
|
|
+ if (!isVariableArg(args[0])) {
|
|
|
+ this.issue(args[0].tok, `Argument 1 of "${name}" must be a variable`);
|
|
|
+ } else {
|
|
|
+ expectKind(args[0], 1, isOrdinal(args[0].type), 'an ordinal value');
|
|
|
+ }
|
|
|
+ if (args.length > 1) {
|
|
|
+ const t = args[1].type;
|
|
|
+ const strOk = t.kind === 'stringLit' && t.length <= 1;
|
|
|
+ expectKind(args[1], 2, t.kind === 'unknown' || strOk || isOrdinal(t), 'an ordinal value');
|
|
|
+ }
|
|
|
+ return proc();
|
|
|
+ }
|
|
|
+ case 'INCL':
|
|
|
+ case 'EXCL': {
|
|
|
+ // gm2's message claims "1 or 2 parameters" but every one-argument
|
|
|
+ // form probed (`INCL(s)`, `INCL(3)`, `INCL(BITSET{1,2})`) is
|
|
|
+ // rejected: exactly two arguments are required.
|
|
|
+ if (!want(2, 2)) return proc();
|
|
|
+ if (!isVariableArg(args[0])) {
|
|
|
+ this.issue(args[0].tok, `Argument 1 of "${name}" must be a variable`);
|
|
|
+ } else {
|
|
|
+ expectKind(args[0], 1, isSetish(args[0].type), 'a set');
|
|
|
+ }
|
|
|
+ expectKind(args[1], 2, isBitNumber(args[1].type), 'a CARDINAL value');
|
|
|
+ return proc();
|
|
|
+ }
|
|
|
+ case 'HALT':
|
|
|
+ case 'NEW':
|
|
|
+ case 'DISPOSE':
|
|
|
+ return proc();
|
|
|
+ case 'LENGTH':
|
|
|
+ case 'TSIZE':
|
|
|
+ case 'CODE':
|
|
|
+ case 'COPY':
|
|
|
+ case 'FILL':
|
|
|
+ return silent();
|
|
|
+ case 'ABS': {
|
|
|
+ if (!want(1, 1)) return fun(UNKNOWN);
|
|
|
+ // gm2 evaluates almost anything but rejects (or crashes on)
|
|
|
+ // composite arguments: only scalars and sets pass through.
|
|
|
+ expectKind(args[0], 1, isScalarOrSet(args[0].type), 'a scalar value');
|
|
|
+ return fun(args[0].type);
|
|
|
+ }
|
|
|
+ case 'CAP': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'char' });
|
|
|
+ const t = args[0].type;
|
|
|
+ const b = deepBase(t);
|
|
|
+ const strOk = t.kind === 'stringLit' && t.length <= 1;
|
|
|
+ expectKind(args[0], 1, t.kind === 'unknown' || strOk || b.kind === 'char' || b.kind === 'whole',
|
|
|
+ 'a CHAR or whole-number value');
|
|
|
+ return fun({ kind: 'char' });
|
|
|
+ }
|
|
|
+ case 'CHR': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'char' });
|
|
|
+ expectKind(args[0], 1, isScalarOrSet(args[0].type), 'a scalar value');
|
|
|
+ return fun({ kind: 'char' });
|
|
|
+ }
|
|
|
+ case 'ORD': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'whole', id: 'CARDINAL', literal: false });
|
|
|
+ expectKind(args[0], 1, isScalarOrSet(args[0].type), 'a scalar value');
|
|
|
+ return fun({ kind: 'whole', id: 'CARDINAL', literal: false });
|
|
|
+ }
|
|
|
+ case 'ORDL': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'whole', id: 'LONGCARD', literal: false });
|
|
|
+ expectKind(args[0], 1, isScalarOrSet(args[0].type), 'a scalar value');
|
|
|
+ return fun({ kind: 'whole', id: 'LONGCARD', literal: false });
|
|
|
+ }
|
|
|
+ case 'ODD': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'bool' });
|
|
|
+ const b = deepBase(args[0].type);
|
|
|
+ expectKind(args[0], 1,
|
|
|
+ b.kind === 'unknown' || b.kind === 'whole' || b.kind === 'enum',
|
|
|
+ 'a whole number');
|
|
|
+ return fun({ kind: 'bool' });
|
|
|
+ }
|
|
|
+ case 'FLOAT': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'real', id: 'REAL', literal: false });
|
|
|
+ expectKind(args[0], 1, isScalarOrSet(args[0].type), 'a scalar value');
|
|
|
+ return fun({ kind: 'real', id: 'REAL', literal: false });
|
|
|
+ }
|
|
|
+ case 'LFLOAT': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'real', id: 'LONGREAL', literal: false });
|
|
|
+ expectKind(args[0], 1, isScalarOrSet(args[0].type), 'a scalar value');
|
|
|
+ return fun({ kind: 'real', id: 'LONGREAL', literal: false });
|
|
|
+ }
|
|
|
+ case 'TRUNC': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'whole', id: 'INTEGER', literal: false });
|
|
|
+ expectKind(args[0], 1,
|
|
|
+ args[0].type.kind === 'unknown' || deepBase(args[0].type).kind === 'real',
|
|
|
+ 'a real number');
|
|
|
+ return fun({ kind: 'whole', id: 'INTEGER', literal: false });
|
|
|
+ }
|
|
|
+ case 'VAL': {
|
|
|
+ if (!want(2, 2)) return fun(UNKNOWN);
|
|
|
+ const target = args[0].info.kind === 'type' || args[0].info.kind === 'builtinType'
|
|
|
+ ? this.namedArgType(args[0])
|
|
|
+ : UNKNOWN;
|
|
|
+ if (args[0].info.kind !== 'type' && args[0].info.kind !== 'builtinType') {
|
|
|
+ this.issue(args[0].tok, `Argument 1 of "VAL" must be a type`);
|
|
|
+ }
|
|
|
+ expectKind(args[1], 2, isScalarOrSet(args[1].type), 'a scalar value');
|
|
|
+ return fun(target);
|
|
|
+ }
|
|
|
+ case 'SIZE': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'whole', id: 'CARDINAL', literal: true });
|
|
|
+ // gm2 accepts any type or value (even records): only arity.
|
|
|
+ return fun({ kind: 'whole', id: 'CARDINAL', literal: true });
|
|
|
+ }
|
|
|
+ case 'HIGH': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'whole', id: 'CARDINAL', literal: false });
|
|
|
+ // Arrays, strings, `CHAR` and (oddly) sets; every scalar,
|
|
|
+ // record and pointer probed is rejected.
|
|
|
+ const b = deepBase(args[0].type);
|
|
|
+ expectKind(args[0], 1,
|
|
|
+ b.kind === 'unknown' || b.kind === 'array' || b.kind === 'stringLit' ||
|
|
|
+ b.kind === 'char' || b.kind === 'bitset' || b.kind === 'setNamed' ||
|
|
|
+ b.kind === 'setLit',
|
|
|
+ 'an array, string, CHAR or set value');
|
|
|
+ return fun({ kind: 'whole', id: 'CARDINAL', literal: false });
|
|
|
+ }
|
|
|
+ case 'MAX':
|
|
|
+ case 'MIN': {
|
|
|
+ if (!want(1, 1)) return fun(UNKNOWN);
|
|
|
+ const a = args[0];
|
|
|
+ const at = a.info.kind === 'type' || a.info.kind === 'builtinType'
|
|
|
+ ? this.namedArgType(a) : a.type;
|
|
|
+ const b = deepBase(at);
|
|
|
+ if (b.kind === 'record') {
|
|
|
+ this.issue(a.tok, `Cannot apply "${name}" to ${displayType(at)}`);
|
|
|
+ return fun(UNKNOWN);
|
|
|
+ }
|
|
|
+ return fun(maxResultType(at));
|
|
|
+ }
|
|
|
+ case 'ADR': {
|
|
|
+ if (!want(1, 1)) return fun({ kind: 'whole', id: 'ADDRESS', literal: false });
|
|
|
+ const a = args[0];
|
|
|
+ const stringArg = a.type.kind === 'stringLit';
|
|
|
+ const ok = a.info.kind === 'var' || a.info.kind === 'field' || a.info.kind === 'proc' ||
|
|
|
+ (a.info.kind === 'unknown' && (a.tok.kind === 'ident' || stringArg)) ||
|
|
|
+ (a.info.kind === 'const' && stringArg);
|
|
|
+ if (!ok) {
|
|
|
+ this.issue(a.tok, `Argument 1 of "ADR" must be a variable, a procedure or a string constant`);
|
|
|
+ }
|
|
|
+ return fun({ kind: 'whole', id: 'ADDRESS', literal: false });
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return silent();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Resolve a type-name argument (`VAL(T, x)`, `MAX(T)`) to a concrete
|
|
|
+ * type. Unresolvable spellings (including qualified `M.T`, whose
|
|
|
+ * qualifier is unavailable here) fall back to unknown: silent. */
|
|
|
+ private namedArgType(a: Value): M2Type {
|
|
|
+ return resolveType(
|
|
|
+ parseTypeText(a.tok.text),
|
|
|
+ this.ctx.unit, this.ctx.docDir, a.tok.line, a.tok.ch,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
private checkAssignment(d: Value, op: Token): void {
|
|
|
const info = d.info;
|
|
|
// Assignments to procedure variables accept bare procedure designators.
|
|
|
@@ -766,15 +971,15 @@ class Walker {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private parseExprList(): M2Type[] {
|
|
|
- const out: M2Type[] = [];
|
|
|
+ private parseExprList(): Value[] {
|
|
|
+ const out: Value[] = [];
|
|
|
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);
|
|
|
+ out.push(this.parseExpr(0));
|
|
|
if (this.atSym(',')) { this.next(); continue; }
|
|
|
break;
|
|
|
}
|
|
|
@@ -979,3 +1184,68 @@ function nameOf(info: HeadInfo, tok: Token): string {
|
|
|
if (info.kind === 'proc') return info.sym.name;
|
|
|
return tok.text;
|
|
|
}
|
|
|
+
|
|
|
+/** Base type through any nesting of subranges. */
|
|
|
+function deepBase(t: M2Type): M2Type {
|
|
|
+ let b = t;
|
|
|
+ while (b.kind === 'subrange') b = b.base;
|
|
|
+ return b;
|
|
|
+}
|
|
|
+
|
|
|
+/** Usable where gm2 demands a variable (`INC`/`INCL` first argument,
|
|
|
+ * `ADR`): variables, record fields, and unresolvable idents (silent —
|
|
|
+ * the analyser reports undeclared names separately). Parenthesised
|
|
|
+ * variables keep their `var`/`field` info, so `INC((i))` is accepted
|
|
|
+ * exactly as gm2 accepts it; literals and constructors are not. */
|
|
|
+function isVariableArg(v: Value): boolean {
|
|
|
+ if (v.info.kind === 'var' || v.info.kind === 'field') return true;
|
|
|
+ return v.info.kind === 'unknown' && v.tok.kind === 'ident';
|
|
|
+}
|
|
|
+
|
|
|
+/** Whole, real, char, boolean, enum, set or string argument (or
|
|
|
+ * unknown): the domain gm2 accepts for `CHR`/`ORD`/`FLOAT`/`LFLOAT`
|
|
|
+ * and `VAL` sources. Arrays, records, pointers and procedures are
|
|
|
+ * rejected by gm2 ("aggregate/pointer used where ... expected"). */
|
|
|
+function isScalarOrSet(t: M2Type): boolean {
|
|
|
+ const b = deepBase(t);
|
|
|
+ return b.kind === 'unknown' || b.kind === 'whole' || b.kind === 'real' ||
|
|
|
+ b.kind === 'char' || b.kind === 'bool' || b.kind === 'enum' ||
|
|
|
+ b.kind === 'bitset' || b.kind === 'setNamed' || b.kind === 'setLit' ||
|
|
|
+ b.kind === 'stringLit';
|
|
|
+}
|
|
|
+
|
|
|
+/** BITSET or set-of: gm2 accepts any set as an `INCL`/`EXCL` target. */
|
|
|
+function isSetish(t: M2Type): boolean {
|
|
|
+ const b = deepBase(t);
|
|
|
+ return b.kind === 'unknown' || b.kind === 'bitset' ||
|
|
|
+ b.kind === 'setNamed' || b.kind === 'setLit';
|
|
|
+}
|
|
|
+
|
|
|
+/** `INCL`/`EXCL` element: a `CARDINAL`-family value, a subrange, a
|
|
|
+ * constant or unknown. gm2 rejects `INTEGER` variables, `CHAR`,
|
|
|
+ * enumerations and everything else ("BITNUM ... incompatible"). */
|
|
|
+function isBitNumber(t: M2Type): boolean {
|
|
|
+ if (t.kind === 'subrange') {
|
|
|
+ const b = deepBase(t);
|
|
|
+ return b.kind === 'whole' || b.kind === 'unknown';
|
|
|
+ }
|
|
|
+ const b = deepBase(t);
|
|
|
+ if (b.kind === 'unknown') return true;
|
|
|
+ if (b.kind === 'whole') {
|
|
|
+ if (b.literal) return true;
|
|
|
+ return b.id !== 'INTEGER' && b.id !== 'LONGINT' && b.id !== 'SHORTINT';
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+/** `MAX`/`MIN` result: the argument type, except sets and pointers (a
|
|
|
+ * `CARDINAL` constant) and arrays (the element type) — all probed. */
|
|
|
+function maxResultType(t: M2Type): M2Type {
|
|
|
+ const b = deepBase(t);
|
|
|
+ if (b.kind === 'bitset' || b.kind === 'setNamed' || b.kind === 'setLit' ||
|
|
|
+ b.kind === 'pointer') {
|
|
|
+ return { kind: 'whole', id: 'CARDINAL', literal: true };
|
|
|
+ }
|
|
|
+ if (b.kind === 'array') return b.element;
|
|
|
+ return t;
|
|
|
+}
|