|
|
@@ -98,6 +98,10 @@ class Walker {
|
|
|
private returnType: M2Type | null | undefined;
|
|
|
/** Depth of nested builtin-procedure argument lists (type args allowed). */
|
|
|
private builtinArgs = 0;
|
|
|
+ /** Guard records of enclosing WITH blocks (innermost last). */
|
|
|
+ private withStack: {
|
|
|
+ type: M2Type; decl: { sym: M2Symbol; unit: M2Unit; dir: string } | null;
|
|
|
+ }[] = [];
|
|
|
/** Parsing the right-hand side of an assignment to a procedure variable. */
|
|
|
private expectingProc = false;
|
|
|
constructor(
|
|
|
@@ -166,7 +170,7 @@ class Walker {
|
|
|
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 'WITH': this.parseWith(depth); return;
|
|
|
case 'CASE': this.parseCase(depth); return;
|
|
|
case 'EXIT': this.next(); return;
|
|
|
default:
|
|
|
@@ -323,40 +327,80 @@ class Walker {
|
|
|
this.expectKw('END');
|
|
|
}
|
|
|
|
|
|
- /** WITH blocks use unqualified field access: skip without checks (conservative). */
|
|
|
- private parseWith(): void {
|
|
|
+ /** WITH opens its guard record's fields as unqualified names.
|
|
|
+ *
|
|
|
+ * The guard is checked like any expression, then must denote a record
|
|
|
+ * (gm2 rejects anything else, including an undereferenced pointer).
|
|
|
+ * Field names shadow outer scope (verified against gm2); genuinely
|
|
|
+ * unknown names fall through to normal resolution and are reported
|
|
|
+ * by the analyser, never here. */
|
|
|
+ private parseWith(depth: number): 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();
|
|
|
- }
|
|
|
+ const guard = this.parseExpr(0);
|
|
|
+ this.expectKw('DO');
|
|
|
+ const gtype = guard.type.kind === 'subrange' ? guard.type.base : guard.type;
|
|
|
+ if (gtype.kind !== 'unknown' && gtype.kind !== 'record') {
|
|
|
+ this.issue(guard.tok, `WITH requires a record, found ${displayType(guard.type)}`);
|
|
|
}
|
|
|
- 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;
|
|
|
+ // The guard must be a variable (or field) designator: gm2 rejects
|
|
|
+ // even a record-valued function result (`WITH Get(0) DO` is a syntax
|
|
|
+ // error there). Unknown designators stay silent.
|
|
|
+ if (guard.info.kind === 'proc') {
|
|
|
+ this.issue(guard.tok, `WITH requires a variable, found procedure "${guard.tok.text}"`);
|
|
|
+ }
|
|
|
+ const decl = (guard.info.kind === 'var' || guard.info.kind === 'const')
|
|
|
+ ? { sym: guard.info.sym, unit: guard.info.host, dir: guard.info.dir } : null;
|
|
|
+ // An unresolvable guard designator (`WITH nope DO`, `WITH UNK[i] DO`)
|
|
|
+ // is unknown exactly as gm2 reports it; anything else (literals and
|
|
|
+ // the like) is covered by the record check below.
|
|
|
+ if (guard.info.kind === 'unknown' && guard.tok.kind === 'ident') {
|
|
|
+ this.issue(guard.tok, `Unknown identifier "${guard.tok.text}"`);
|
|
|
+ }
|
|
|
+ this.withStack.push({ type: guard.type, decl });
|
|
|
+ try {
|
|
|
+ this.parseSeq(new Set(['END']), depth + 1);
|
|
|
+ } finally {
|
|
|
+ this.withStack.pop();
|
|
|
+ }
|
|
|
+ this.expectKw('END');
|
|
|
+ }
|
|
|
+
|
|
|
+ /** A bare name visible through the WITH stack (innermost guard first).
|
|
|
+ *
|
|
|
+ * Pointer guards dereference to their target record (gm2 rejects the
|
|
|
+ * guard itself unless `^`-dereferenced, but the body still sees the
|
|
|
+ * record's fields). Absent fields and unknown guards fall through to
|
|
|
+ * normal resolution. */
|
|
|
+ private resolveWithField(head: Token): {
|
|
|
+ type: M2Type; decl: { sym: M2Symbol; unit: M2Unit; dir: string } | null;
|
|
|
+ } | null {
|
|
|
+ if (head.kind !== 'ident') return null;
|
|
|
+ for (let k = this.withStack.length - 1; k >= 0; k--) {
|
|
|
+ const g = this.withStack[k];
|
|
|
+ if (g.decl) {
|
|
|
+ const found = findRecordField(
|
|
|
+ g.decl.unit, g.decl.dir, g.decl.sym, head.text, head.line, head.ch);
|
|
|
+ if (found) {
|
|
|
+ return {
|
|
|
+ type: resolveDeclaredType(found.sym, found.unit, found.dir, head.line, head.ch),
|
|
|
+ decl: { sym: found.sym, unit: found.unit, dir: found.dir },
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let t = g.type.kind === 'subrange' ? g.type.base : g.type;
|
|
|
+ for (let n = 0; n < 8 && t.kind === 'pointer'; n++) t = t.target;
|
|
|
+ if (t.kind !== 'unknown' && t.kind === 'record') {
|
|
|
+ const found = findFieldInType(
|
|
|
+ this.ctx.unit, this.ctx.docDir, t, head.text, head.line, head.ch);
|
|
|
+ if (found) {
|
|
|
+ return {
|
|
|
+ type: resolveDeclaredType(found.sym, found.unit, found.dir, head.line, head.ch),
|
|
|
+ decl: { sym: found.sym, unit: found.unit, dir: found.dir },
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
- this.next();
|
|
|
}
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
private parseCase(depth: number): void {
|
|
|
@@ -440,13 +484,20 @@ class Walker {
|
|
|
|
|
|
/** 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);
|
|
|
+ const withField = this.resolveWithField(head);
|
|
|
+ let info = withField ? { kind: 'field' as const } : this.resolveHead(head);
|
|
|
+ // Inside WITH, a bare name resolving nowhere is unknown (the analyser
|
|
|
+ // skips WITH regions, so the checker reports it — once, here).
|
|
|
+ if (!withField && this.withStack.length > 0 && head.kind === 'ident' && info.kind === 'unknown') {
|
|
|
+ this.issue(head, `Unknown identifier "${head.text}"`);
|
|
|
+ }
|
|
|
let tok = head;
|
|
|
// Declaration behind the current value (for field selection).
|
|
|
let cur: { sym: M2Symbol; unit: M2Unit; dir: string } | null =
|
|
|
- (info.kind === 'var' || info.kind === 'const')
|
|
|
+ withField ? withField.decl
|
|
|
+ : (info.kind === 'var' || info.kind === 'const')
|
|
|
? { sym: info.sym, unit: info.host, dir: info.dir } : null;
|
|
|
- let type = this.valueType(info, tok);
|
|
|
+ let type = withField ? withField.type : this.valueType(info, tok);
|
|
|
// Qualifier chain: M.X or r.field.
|
|
|
while (this.atSym('.')) {
|
|
|
this.next(); // .
|