Bladeren bron

Diagnostics: nominal comparison by declaration identity

R and M.R naming the same .def declaration are the same type;
same-shaped records from different declarations still mismatch.
Record, array, enum and set comparisons prefer the declaration
(file:line:ch) over the spelling, falling back to names.
Eric Streit 1 week geleden
bovenliggende
commit
c268127fbd
2 gewijzigde bestanden met toevoegingen van 44 en 20 verwijderingen
  1. 4 0
      extensions/modula2-language/DIAGNOSTICS.md
  2. 40 20
      extensions/modula2-language/src/m2/types.ts

+ 4 - 0
extensions/modula2-language/DIAGNOSTICS.md

@@ -90,6 +90,10 @@ a field of a known non-record (`f.x` on a `POINTER` — Modula-2 does not
 implicitly dereference) and unknown fields (`r.nope`). Anything
 unresolvable stays silent.
 
+Nominal comparison is by declaration identity, not spelling: `R` and
+`M.R` naming the same `.def` declaration are the same type, while two
+same-shaped records from different declarations are not.
+
 ## Circular dependencies (no use-before-declaration)
 
 gm2 is multi-pass, so declaration order never matters — there is

+ 40 - 20
extensions/modula2-language/src/m2/types.ts

@@ -31,12 +31,12 @@ export type M2Type =
   | { kind: 'stringLit', length: number }
   | { kind: 'bool' }
   | { kind: 'bitset' }
-  | { kind: 'setNamed', name: string }
+  | { kind: 'setNamed', name: string, decl?: string }
   | { kind: 'setLit' }
-  | { kind: 'array', element: M2Type, size: number | null, name: string | null }
-  | { kind: 'record', name: string | null, textKey: string }
-  | { kind: 'pointer', target: M2Type, name: string | null }
-  | { kind: 'enum', name: string }
+  | { kind: 'array', element: M2Type, size: number | null, name: string | null, decl?: string }
+  | { kind: 'record', name: string | null, textKey: string, decl?: string }
+  | { kind: 'pointer', target: M2Type, name: string | null, decl?: string }
+  | { kind: 'enum', name: string, decl?: string }
   | { kind: 'subrange', base: M2Type }
   | { kind: 'proc', params?: M2Type[], result?: M2Type | null }
   | { kind: 'nil' };
@@ -333,24 +333,33 @@ function resolveT(t: M2Type, unit: M2Unit, dir: string, line: number, ch: number
     // NIL assignment, same-type comparison and mismatch detection work.
     // (Approximation: an opaque type need not be pointer-implemented, but
     // treating it as unknown would silence all checking on it.)
-    return { kind: 'pointer', target: UNKNOWN, name: t.qualifier ? `${t.qualifier}.${t.name}` : t.name };
+    const r = resolved.sym.nameRange;
+    return {
+      kind: 'pointer', target: UNKNOWN,
+      name: t.qualifier ? `${t.qualifier}.${t.name}` : t.name,
+      decl: `${resolved.filePath}:${r.startLine}:${r.startCh}`,
+    };
   }
   seen.add(key);
   const inner = resolveT(
     parseTypeText(detail.slice(eq + 1)), host, path.dirname(resolved.filePath),
     resolved.sym.nameRange.startLine, resolved.sym.nameRange.startCh, seen,
   );
-  return attachName(inner, t.qualifier ? `${t.qualifier}.${t.name}` : t.name);
+  const r = resolved.sym.nameRange;
+  const decl = `${resolved.filePath}:${r.startLine}:${r.startCh}`;
+  return attachName(inner, t.qualifier ? `${t.qualifier}.${t.name}` : t.name, decl);
 }
 
-/** Attach the ultimate type name so nominal comparison works through aliases. */
-function attachName(t: M2Type, name: string): M2Type {
+/** Attach the ultimate type name (and declaration identity) so nominal
+ *  comparison works through aliases and qualified spellings
+ *  (`RiseSetResult` and `Sunrise.RiseSetResult` share one declaration). */
+function attachName(t: M2Type, name: string, decl: string | undefined): M2Type {
   switch (t.kind) {
-    case 'array': return { ...t, name };
-    case 'record': return { ...t, name };
-    case 'pointer': return { ...t, name };
-    case 'enum': return { ...t, name: t.name || name };
-    case 'setNamed': return { ...t, name: t.name || name };
+    case 'array': return { ...t, name, decl: decl ?? t.decl };
+    case 'record': return { ...t, name, decl: decl ?? t.decl };
+    case 'pointer': return { ...t, name, decl: decl ?? t.decl };
+    case 'enum': return { ...t, name: t.name || name, decl: decl ?? t.decl };
+    case 'setNamed': return { ...t, name: t.name || name, decl: decl ?? t.decl };
     default: return t;
   }
 }
@@ -388,10 +397,12 @@ export function isAssignable(to: M2Type, from: M2Type): boolean {
     return t.size === null || f.length <= t.size;
   }
   if (t.kind === 'array' && f.kind === 'array') {
+    if (t.decl && f.decl) return t.decl === f.decl;
     if (t.name || f.name) return sameNominal(t.name ?? null, f.name ?? null);
     return normKey(t) === normKey(f);
   }
   if (t.kind === 'record' && f.kind === 'record') {
+    if (t.decl && f.decl) return t.decl === f.decl;
     if (t.name || f.name) return sameNominal(t.name ?? null, f.name ?? null);
     return normKey(t) === normKey(f);
   }
@@ -399,11 +410,17 @@ export function isAssignable(to: M2Type, from: M2Type): boolean {
   if (t.kind === 'pointer' && f.kind === 'pointer') return typeKey(t.target) === typeKey(f.target);
   if (t.kind === 'proc' && f.kind === 'nil') return true;
   if (t.kind === 'proc' && f.kind === 'proc') return true;
-  if (t.kind === 'enum' && f.kind === 'enum') return t.name === f.name;
+  if (t.kind === 'enum' && f.kind === 'enum') {
+    if (t.decl && f.decl) return t.decl === f.decl;
+    return t.name === f.name;
+  }
   if (t.kind === 'bool' && f.kind === 'bool') return true;
   if (t.kind === 'bitset' && (f.kind === 'bitset' || f.kind === 'setLit')) return true;
   if (t.kind === 'setNamed' && (f.kind === 'setLit' || f.kind === 'bitset')) return true;
-  if (t.kind === 'setNamed' && f.kind === 'setNamed') return t.name === f.name;
+  if (t.kind === 'setNamed' && f.kind === 'setNamed') {
+    if (t.decl && f.decl) return t.decl === f.decl;
+    return t.name === f.name;
+  }
   return false;
 }
 
@@ -439,7 +456,10 @@ export function isComparable(a: M2Type, b: M2Type, ordering: boolean): boolean {
   if (ta.kind === 'stringLit' && tb.kind === 'array') return tb.element.kind === 'char' || tb.element.kind === 'unknown';
   if (tb.kind === 'stringLit' && ta.kind === 'array') return ta.element.kind === 'char' || ta.element.kind === 'unknown';
   if (ta.kind === 'bool' && tb.kind === 'bool') return !ordering;
-  if (ta.kind === 'enum' && tb.kind === 'enum') return ta.name === tb.name;
+  if (ta.kind === 'enum' && tb.kind === 'enum') {
+    if (ta.decl && tb.decl) return ta.decl === tb.decl;
+    return ta.name === tb.name;
+  }
   if (isSetish(ta) && isSetish(tb)) return !ordering;
   if (ta.kind === 'pointer' && tb.kind === 'pointer') {
     if (ordering) return false;
@@ -550,10 +570,10 @@ function typeKey(t: M2Type): string {
   switch (b.kind) {
     case 'whole': return `whole:${b.literal ? '*' : b.id}`;
     case 'real': return `real:${b.literal ? '*' : b.id}`;
-    case 'array': return `array:${b.name ?? normKey(b.element)}`;
-    case 'record': return `record:${b.name ?? normKey(b)}`;
+    case 'array': return `array:${b.decl ?? b.name ?? normKey(b.element)}`;
+    case 'record': return `record:${b.decl ?? b.name ?? normKey(b)}`;
     case 'pointer': return `pointer:${b.name ?? typeKey(b.target)}`;
-    case 'enum': return `enum:${b.name}`;
+    case 'enum': return `enum:${b.decl ?? b.name}`;
     case 'unknown': return 'unknown';
     default: return b.kind;
   }