|
|
@@ -16,7 +16,8 @@ import { Token } from './lexer';
|
|
|
import { M2Symbol, M2Unit } from './parser';
|
|
|
import { resolveName } from './resolve';
|
|
|
import {
|
|
|
- M2Type, UNKNOWN, arithKind, arithResultType, displayType, isArrayArgCompatible, isAssignable,
|
|
|
+ M2Type, UNKNOWN, arithKind, arithResultType, displayType, findFieldInType,
|
|
|
+ findRecordField, isArrayArgCompatible, isAssignable,
|
|
|
isComparable, isOrdinal, numberLiteralType, parseTypeText, resolveDeclaredType, resolveQualified,
|
|
|
resolveType, returnTypeOf, unitOfFile,
|
|
|
} from './types';
|
|
|
@@ -441,7 +442,12 @@ class Walker {
|
|
|
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).
|
|
|
+ // Declaration behind the current value (for field selection).
|
|
|
+ let cur: { sym: M2Symbol; unit: M2Unit; dir: string } | null =
|
|
|
+ (info.kind === 'var' || info.kind === 'const')
|
|
|
+ ? { sym: info.sym, unit: info.host, dir: info.dir } : null;
|
|
|
+ let type = this.valueType(info, tok);
|
|
|
+ // Qualifier chain: M.X or r.field.
|
|
|
while (this.atSym('.')) {
|
|
|
this.next(); // .
|
|
|
const nm = this.peek();
|
|
|
@@ -456,6 +462,8 @@ class Walker {
|
|
|
info = PROC_BUILTINS.has(nm.text)
|
|
|
? { kind: 'builtinProc', name: nm.text }
|
|
|
: { kind: 'unknown' };
|
|
|
+ cur = null;
|
|
|
+ type = UNKNOWN;
|
|
|
continue;
|
|
|
}
|
|
|
switch (q.sym.kind) {
|
|
|
@@ -465,54 +473,18 @@ class Walker {
|
|
|
case 'type': info = { kind: 'type' }; break;
|
|
|
default: info = { kind: 'unknown' }; break;
|
|
|
}
|
|
|
+ cur = (info.kind === 'var' || info.kind === 'const')
|
|
|
+ ? { sym: info.sym, unit: info.host, dir: info.dir } : null;
|
|
|
+ type = this.valueType(info, nm);
|
|
|
} else {
|
|
|
+ const selected = this.selectField(cur, type, nm);
|
|
|
+ cur = selected.decl;
|
|
|
+ type = selected.type;
|
|
|
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();
|
|
|
- // 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)}`);
|
|
|
- badIndex = true;
|
|
|
- }
|
|
|
- type = type.element;
|
|
|
- } else if (type.kind !== 'unknown') {
|
|
|
- this.issue(open, `Cannot index ${displayType(type)}`);
|
|
|
- type = UNKNOWN;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- } 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 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;
|
|
|
- }
|
|
|
- }
|
|
|
+ // Postfix: indexing, dereferencing and field selection.
|
|
|
+ ({ info, tok, type } = this.parsePostfix(info, tok, type));
|
|
|
// Set constructor with explicit type (`BITSET{...}`, `MySet{...}`).
|
|
|
if ((info.kind === 'type' || info.kind === 'builtinType') && this.atSym('{')) {
|
|
|
this.next();
|
|
|
@@ -523,9 +495,16 @@ class Walker {
|
|
|
if (this.atSym('(')) {
|
|
|
const call = this.parseCall(info, tok);
|
|
|
if (mode === 'stmt') {
|
|
|
- if (call.returnsValue) {
|
|
|
+ // `F().x := ...` assigns into a call temporary (accepted by gm2):
|
|
|
+ // only a bare `F();` ignores its result.
|
|
|
+ if (call.returnsValue && !this.atPostfixStart()) {
|
|
|
this.issue(tok, `Return value of function "${nameOf(info, tok)}" is ignored`);
|
|
|
}
|
|
|
+ if (this.atPostfixStart()) {
|
|
|
+ const rest = this.parsePostfix(info, tok, UNKNOWN);
|
|
|
+ info = rest.info;
|
|
|
+ tok = rest.tok;
|
|
|
+ }
|
|
|
return { type: UNKNOWN, info, tok };
|
|
|
}
|
|
|
if (call.isVoid) {
|
|
|
@@ -533,6 +512,9 @@ class Walker {
|
|
|
return { type: UNKNOWN, info, tok };
|
|
|
}
|
|
|
type = call.type;
|
|
|
+ // Postfix after a call result (`F().x`, `a[i].f` already handled above).
|
|
|
+ ({ info, tok, type } = this.parsePostfix(info, tok, type));
|
|
|
+ return { type, info, tok };
|
|
|
} else if (mode === 'expr') {
|
|
|
if (info.kind === 'proc') {
|
|
|
// A bare procedure name on the right of a `P := Q` assignment
|
|
|
@@ -563,6 +545,110 @@ class Walker {
|
|
|
return { type, info, tok };
|
|
|
}
|
|
|
|
|
|
+ /** Type of `.name` selected on a value, with declaration tracking.
|
|
|
+ *
|
|
|
+ * `decl` carries the declaration behind the base value when known (a
|
|
|
+ * variable/constant, or a previously selected field); `base` is its
|
|
|
+ * computed type. Returns the field type (or UNKNOWN) plus the field's
|
|
|
+ * own declaration for chained selection. Genuine errors are reported:
|
|
|
+ * selection on a known non-record, or an absent field of a known
|
|
|
+ * record (gm2 rejects both); anything unknowable stays silent.
|
|
|
+ */
|
|
|
+ private selectField(
|
|
|
+ decl: { sym: M2Symbol; unit: M2Unit; dir: string } | null,
|
|
|
+ base: M2Type, nm: Token,
|
|
|
+ ): { type: M2Type; decl: { sym: M2Symbol; unit: M2Unit; dir: string } | null } {
|
|
|
+ const stripped = base.kind === 'subrange' ? base.base : base;
|
|
|
+ if (decl) {
|
|
|
+ const found = findRecordField(decl.unit, decl.dir, decl.sym, nm.text, nm.line, nm.ch);
|
|
|
+ if (found) {
|
|
|
+ return {
|
|
|
+ type: resolveDeclaredType(found.sym, found.unit, found.dir, nm.line, nm.ch),
|
|
|
+ decl: { sym: found.sym, unit: found.unit, dir: found.dir },
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (stripped.kind === 'record') {
|
|
|
+ this.issue(nm, `Record "${stripped.name ?? '?'}" has no field "${nm.text}"`);
|
|
|
+ return { type: UNKNOWN, decl: null };
|
|
|
+ }
|
|
|
+ if (stripped.kind !== 'unknown') {
|
|
|
+ this.issue(nm, `Type ${displayType(stripped)} is not a record`);
|
|
|
+ return { type: UNKNOWN, decl: null };
|
|
|
+ }
|
|
|
+ return { type: UNKNOWN, decl: null };
|
|
|
+ }
|
|
|
+ const found = findFieldInType(this.ctx.unit, this.ctx.docDir, base, nm.text, nm.line, nm.ch);
|
|
|
+ if (found) {
|
|
|
+ return {
|
|
|
+ type: resolveDeclaredType(found.sym, found.unit, found.dir, nm.line, nm.ch),
|
|
|
+ decl: { sym: found.sym, unit: found.unit, dir: found.dir },
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (stripped.kind !== 'unknown' && stripped.kind !== 'record') {
|
|
|
+ this.issue(nm, `Type ${displayType(stripped)} is not a record`);
|
|
|
+ return { type: UNKNOWN, decl: null };
|
|
|
+ }
|
|
|
+ if (stripped.kind === 'record') {
|
|
|
+ this.issue(nm, `Record "${stripped.name ?? '?'}" has no field "${nm.text}"`);
|
|
|
+ }
|
|
|
+ return { type: UNKNOWN, decl: null };
|
|
|
+ }
|
|
|
+
|
|
|
+ private atPostfixStart(): boolean {
|
|
|
+ return this.atSym('[') || this.atSym('^') || this.atSym('.');
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Indexing, dereferencing and field selection after `^`/`[]`
|
|
|
+ * (direct `r.x` is consumed by the qualifier chain instead). */
|
|
|
+ private parsePostfix(
|
|
|
+ info: HeadInfo, tok: Token, type: M2Type,
|
|
|
+ ): { info: HeadInfo; tok: Token; type: M2Type } {
|
|
|
+ for (;;) {
|
|
|
+ if (this.atSym('[')) {
|
|
|
+ const open = this.next()!;
|
|
|
+ const idxTypes = this.parseExprList();
|
|
|
+ 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]`).
|
|
|
+ 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)}`);
|
|
|
+ badIndex = true;
|
|
|
+ }
|
|
|
+ type = type.element;
|
|
|
+ } else if (type.kind !== 'unknown') {
|
|
|
+ this.issue(open, `Cannot index ${displayType(type)}`);
|
|
|
+ type = UNKNOWN;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } 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 if (this.atSym('.')) {
|
|
|
+ this.next(); // .
|
|
|
+ const nm = this.peek();
|
|
|
+ if (!nm || nm.kind !== 'ident') break;
|
|
|
+ this.next();
|
|
|
+ tok = nm;
|
|
|
+ // Only the computed type is known here (no declaration).
|
|
|
+ const selected = this.selectField(null, type, nm);
|
|
|
+ type = selected.type;
|
|
|
+ info = { kind: 'field' };
|
|
|
+ } else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { info, tok, type };
|
|
|
+ }
|
|
|
+
|
|
|
private valueType(info: HeadInfo, tok: Token): M2Type {
|
|
|
switch (info.kind) {
|
|
|
case 'var': return resolveDeclaredType(info.sym, info.host, info.dir, tok.line, tok.ch);
|
|
|
@@ -672,7 +758,8 @@ class Walker {
|
|
|
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;
|
|
|
+ if (info.kind === 'unknown' || info.kind === 'builtinProc') return;
|
|
|
+ // Variables and record fields check against their declared types.
|
|
|
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)}`);
|