All files / PARSE/4-Resolve VisibleSymbols.ts

100% Statements 61/61
100% Branches 5/5
100% Functions 9/9
100% Lines 60/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355                                                                                                                                                                      150x 1x       149x 149x 149x 149x 149x 149x 149x 149x 149x 149x 149x     149x 149x 149x 149x 149x 149x 149x 149x 149x 1x         149x         149x 167x                                                   149x                                                                       167x 167x 167x           167x             167x             167x   167x         167x         167x       167x       167x         167x               167x         167x     24x   167x                         167x 167x       167x       167x       167x       167x       167x                           1002x 164x                                     64x   2505x 231x 213x                                                 745x 745x 40x   745x          
/**
 * What each file can SEE — 1.4 Resolve's half of the symbol view.
 *
 * `convert()` in 1.3 Declare answers "what does this file declare?", which is a
 * per-file question. What codegen actually needs is "what may this file use?",
 * and that is the file plus everything its include closure reaches — a
 * cross-file question no per-file pass can answer.
 *
 * The composition is local-wins: a name the file declares itself shadows the
 * same name from an include, and among includes the first one walked wins. That
 * rule IS the visibility decision, which is why it belongs to the pass that owns
 * cross-file facts rather than to the adapter that builds the per-file view
 * (#1511).
 *
 * It used to run per file during rendering, merging into a view that had been
 * built per file — so the answer depended on how much of the run had gone
 * before, and `TransitiveEnumCollector` silently skipped any file not yet
 * published. Composed once now, while the whole program is in hand.
 */
 
import type IBitmapFieldLayout from "../../types/IBitmapFieldLayout";
import type ICodeGenSymbols from "../../types/ICodeGenSymbols";
 
/**
 * The mutable collections mergeExternalSymbols accumulates into.
 *
 * Grouped into one object rather than passed as eight parameters: adding the
 * three type-forming sets for #1333 took the parameter list past what is
 * readable, and a positional list of eight same-shaped collections is a
 * transposition waiting to happen.
 */
interface IMergeAccumulator {
  readonly knownEnums: Set<string>;
  readonly knownScopes: Set<string>;
  readonly enumMembers: Map<string, Map<string, number>>;
  readonly functionReturnTypes: Map<string, string>;
  readonly scopeMemberVisibility: Map<
    string,
    Map<string, "public" | "private">
  >;
  readonly knownStructs: Set<string>;
  readonly knownBitmaps: Set<string>;
  readonly knownVariables: Set<string>;
  readonly bitmapFields: Map<string, Map<string, IBitmapFieldLayout>>;
  readonly bitmapBackingType: Map<string, string>;
  readonly knownRegisters: Set<string>;
  readonly scopedRegisters: Map<string, string>;
  readonly registerMemberAccess: Map<string, string>;
  readonly registerMemberTypes: Map<string, string>;
  readonly registerBaseAddresses: Map<string, string>;
  readonly registerMemberOffsets: Map<string, string>;
  readonly registerMemberCTypes: Map<string, string>;
  readonly bitmapBitWidth: Map<string, number>;
  readonly structFields: Map<string, Map<string, string>>;
  readonly structFieldArrays: Map<string, Set<string>>;
  readonly structFieldDimensions: Map<
    string,
    Map<string, ReadonlyArray<number | string>>
  >;
}
 
class VisibleSymbols {
  /**
   * Issue #465: Merge external symbol info into an existing ISymbolInfo.
   *
   * When a file includes other .cnx files, the enum types and scopes from those
   * external files need to be available for code generation. This enables:
   * - Enum member prefixing for external enums
   * - Cross-scope method calls like global.Scope.method() returning enums
   * - Visibility enforcement on members of included scopes (#1190)
   *
   * This method creates a new ISymbolInfo that includes both the base symbols
   * and merged info from external sources.
   *
   * @param base The ISymbolInfo from the current file
   * @param externalSources Array of ISymbolInfo from included .cnx files
   * @returns New ISymbolInfo with merged enum, scope and visibility data
   */
  static mergeExternalSymbols(
    base: ICodeGenSymbols,
    externalSources: readonly ICodeGenSymbols[],
  ): ICodeGenSymbols {
    // If no external sources, return base unchanged
    if (externalSources.length === 0) {
      return base;
    }
 
    // Create mutable copies of enum-related data and scope info
    const mergedKnownEnums = new Set(base.knownEnums);
    const mergedKnownScopes = new Set(base.knownScopes);
    const mergedKnownStructs = new Set(base.knownStructs);
    const mergedKnownBitmaps = new Set(base.knownBitmaps);
    const mergedKnownVariables = new Set(base.knownVariables);
    const mergedBitmapFields = VisibleSymbols._copyNestedMap(base.bitmapFields);
    const mergedBitmapBackingType = new Map(base.bitmapBackingType);
    const mergedBitmapBitWidth = new Map(base.bitmapBitWidth);
    const mergedEnumMembers = VisibleSymbols._copyNestedMap(base.enumMembers);
    const mergedFunctionReturnTypes = new Map(base.functionReturnTypes);
    const mergedScopeMemberVisibility = VisibleSymbols._copyNestedMap(
      base.scopeMemberVisibility,
    );
    const mergedKnownRegisters = new Set(base.knownRegisters);
    const mergedScopedRegisters = new Map(base.scopedRegisters);
    const mergedRegisterMemberAccess = new Map(base.registerMemberAccess);
    const mergedRegisterMemberTypes = new Map(base.registerMemberTypes);
    const mergedRegisterBaseAddresses = new Map(base.registerBaseAddresses);
    const mergedRegisterMemberOffsets = new Map(base.registerMemberOffsets);
    const mergedRegisterMemberCTypes = new Map(base.registerMemberCTypes);
    const mergedStructFields = VisibleSymbols._copyNestedMap(base.structFields);
    const mergedStructFieldArrays = new Map(
      [...base.structFieldArrays].map(([name, fields]) => [
        name,
        new Set(fields),
      ]),
    );
    const mergedStructFieldDimensions = VisibleSymbols._copyNestedMap(
      base.structFieldDimensions,
    );
 
    // Merge in external enum info, function return types, scopes and visibility
    for (const external of externalSources) {
      VisibleSymbols._mergeExternalSource(external, {
        knownEnums: mergedKnownEnums,
        knownScopes: mergedKnownScopes,
        enumMembers: mergedEnumMembers,
        functionReturnTypes: mergedFunctionReturnTypes,
        scopeMemberVisibility: mergedScopeMemberVisibility,
        knownStructs: mergedKnownStructs,
        knownBitmaps: mergedKnownBitmaps,
        bitmapFields: mergedBitmapFields,
        bitmapBackingType: mergedBitmapBackingType,
        bitmapBitWidth: mergedBitmapBitWidth,
        knownVariables: mergedKnownVariables,
        knownRegisters: mergedKnownRegisters,
        scopedRegisters: mergedScopedRegisters,
        registerMemberAccess: mergedRegisterMemberAccess,
        registerMemberTypes: mergedRegisterMemberTypes,
        registerBaseAddresses: mergedRegisterBaseAddresses,
        registerMemberOffsets: mergedRegisterMemberOffsets,
        registerMemberCTypes: mergedRegisterMemberCTypes,
        structFields: mergedStructFields,
        structFieldArrays: mergedStructFieldArrays,
        structFieldDimensions: mergedStructFieldDimensions,
      });
    }
 
    // Return new ICodeGenSymbols with merged enum data and scope info
    return {
      ...base,
      knownScopes: mergedKnownScopes,
      knownEnums: mergedKnownEnums,
      knownStructs: mergedKnownStructs,
      knownBitmaps: mergedKnownBitmaps,
      bitmapFields: mergedBitmapFields,
      bitmapBackingType: mergedBitmapBackingType,
      bitmapBitWidth: mergedBitmapBitWidth,
      enumMembers: mergedEnumMembers,
      functionReturnTypes: mergedFunctionReturnTypes,
      scopeMemberVisibility: mergedScopeMemberVisibility,
      knownVariables: mergedKnownVariables,
      knownRegisters: mergedKnownRegisters,
      scopedRegisters: mergedScopedRegisters,
      registerMemberAccess: mergedRegisterMemberAccess,
      registerMemberTypes: mergedRegisterMemberTypes,
      registerBaseAddresses: mergedRegisterBaseAddresses,
      registerMemberOffsets: mergedRegisterMemberOffsets,
      registerMemberCTypes: mergedRegisterMemberCTypes,
      structFields: mergedStructFields,
      structFieldArrays: mergedStructFieldArrays,
      structFieldDimensions: mergedStructFieldDimensions,
    };
  }
 
  private static _mergeExternalSource(
    external: ICodeGenSymbols,
    into: IMergeAccumulator,
  ): void {
    // #1333: every type-forming kind crosses the include boundary on the same
    // terms. Only knownEnums did, so ADR-057 qualification was kind-dependent: in
    // a scope spanning two files, an enum declared in the other file qualified and
    // a struct did not. Adjacent lines in one function emitted `Lib__Mode m` and
    // bare `Point p` -- the second does not compile. The asymmetry was invisible
    // while a scope could not span files at all, which is the bug this shipped with.
    VisibleSymbols._mergeNames(external.knownEnums, into.knownEnums);
    VisibleSymbols._mergeNames(external.knownStructs, into.knownStructs);
    VisibleSymbols._mergeNames(external.knownBitmaps, into.knownBitmaps);
 
    // Issue #1190: the visibility map travels with the scope name. Registering a
    // scope as known while leaving its visibility unknown makes every member of an
    // included scope look public, because the access check reads `undefined` and
    // only rejects an explicit "private".
    VisibleSymbols._mergeNames(external.knownScopes, into.knownScopes);
 
    // Issue #1398: file-scope VALUE names cross on the same terms as the
    // type-forming kinds above. The #1333 asymmetry this function was written to
    // fix was between two kinds of type; this is the same asymmetry one axis
    // over -- a type declared in an included file resolved and a const declared
    // beside it did not, so E0426 fired cross-file and E0427 could not.
    VisibleSymbols._mergeNames(external.knownVariables, into.knownVariables);
 
    // A type's NAME is not enough; its detail travels with it. enumMembers already
    // moved with knownEnums, which is exactly why enums were the only kind that
    // ever worked -- carrying knownBitmaps alone let a cross-file bitmap type
    // resolve and then hard-error on the field behind it ("Unknown bitmap field
    // 'Mode' on type 'Lib__Flags'"). Same asymmetry, one level down.
    const cloneMap = <K, V>(m: ReadonlyMap<K, V>): Map<K, V> => new Map(m);
 
    VisibleSymbols._mergePreferringLocal(
      external.enumMembers,
      into.enumMembers,
      cloneMap,
    );
    VisibleSymbols._mergePreferringLocal(
      external.bitmapFields,
      into.bitmapFields,
      cloneMap,
    );
    VisibleSymbols._mergePreferringLocal(
      external.bitmapBackingType,
      into.bitmapBackingType,
    );
    VisibleSymbols._mergePreferringLocal(
      external.bitmapBitWidth,
      into.bitmapBitWidth,
    );
    VisibleSymbols._mergePreferringLocal(
      external.scopeMemberVisibility,
      into.scopeMemberVisibility,
      cloneMap,
    );
    VisibleSymbols._mergePreferringLocal(
      external.functionReturnTypes,
      into.functionReturnTypes,
    );
    // A struct's fields travel with its name on the same terms (#1760 second
    // review): only the name crossed, so a field of an included struct had no
    // type, and `{ f: { A: 1 } }` for an included struct's bitmap field
    // reached C as a designated initializer on a scalar.
    VisibleSymbols._mergePreferringLocal(
      external.structFields,
      into.structFields,
      cloneMap,
    );
    VisibleSymbols._mergePreferringLocal(
      external.structFieldArrays,
      into.structFieldArrays,
      (fields) => new Set(fields),
    );
    VisibleSymbols._mergePreferringLocal(
      external.structFieldDimensions,
      into.structFieldDimensions,
      cloneMap,
    );
 
    // #1322: a register crosses on the same terms as every kind above. It never
    // did -- a board file declaring `register HW @ ...` and an application
    // file including it is ADR-004's whole use case, and the importer reported
    // `'HW' is not defined` (E0427) because nothing carried the name across.
    // Its detail travels with it, as the bitmap maps do: the access modifiers
    // are what E0870-E0872 read, and the offsets, C types and base addresses
    // are what codegen spells the access with.
    VisibleSymbols._mergeNames(external.knownRegisters, into.knownRegisters);
    VisibleSymbols._mergePreferringLocal(
      external.scopedRegisters,
      into.scopedRegisters,
    );
    VisibleSymbols._mergePreferringLocal(
      external.registerMemberAccess,
      into.registerMemberAccess,
    );
    VisibleSymbols._mergePreferringLocal(
      external.registerMemberTypes,
      into.registerMemberTypes,
    );
    VisibleSymbols._mergePreferringLocal(
      external.registerBaseAddresses,
      into.registerBaseAddresses,
    );
    VisibleSymbols._mergePreferringLocal(
      external.registerMemberOffsets,
      into.registerMemberOffsets,
    );
    VisibleSymbols._mergePreferringLocal(
      external.registerMemberCTypes,
      into.registerMemberCTypes,
    );
  }
 
  /**
   * Add every member of `from` into `into`. A name-only set: no precedence
   * question arises because the sets carry no payload.
   */
  private static _mergeNames(
    from: ReadonlySet<string>,
    into: Set<string>,
  ): void {
    for (const name of from) {
      into.add(name);
    }
  }
 
  /**
   * Merge `from` into `into`, **local wins**: an entry already present is never
   * overwritten by an external one.
   *
   * The nine merges in _mergeExternalSource were nine copies of this loop, which
   * is both the duplication and the cognitive complexity SonarCloud flagged
   * (S3776, 22 against 15). More to the point, "local takes precedence" was
   * restated nine times, so a tenth merge could silently choose otherwise.
   *
   * `clone` exists because half the values are Maps that must not be aliased
   * between the base and the merged result.
   */
  private static _mergePreferringLocal<K, V>(
    from: ReadonlyMap<K, V>,
    into: Map<K, V>,
    clone: (value: V) => V = (value) => value,
  ): void {
    for (const [key, value] of from) {
      if (!into.has(key)) {
        into.set(key, clone(value));
      }
    }
  }
 
  /**
   * Deep-copy an outer -> (inner -> value) map so the merged result never
   * aliases the base's inner maps.
   *
   * One decision, spelled three times -- inline for `bitmapFields`, and as
   * `_copyEnumMembers` and `_copyScopeMemberVisibility` -- unified, so a fourth
   * nested field cannot be added by copying whichever spelling sat nearest.
   *
   * `tsc` holds the deep copy in place, which is the interesting part and the
   * reason no test covers it: the SHALLOW `new Map(...)` the flat fields use
   * sits right beside these and reads equally idiomatic, but substituting it
   * here is rejected -- `Map<K, ReadonlyMap<IK, V>>` is not assignable to
   * `Map<K, Map<IK, V>>`. An earlier version of this comment justified the
   * helper with the aliasing bug that would follow instead, which the types
   * forbid and `_mergePreferringLocal` never reaches anyway; naming an
   * unreachable failure mode is the thing the rest of this change removes.
   */
  private static _copyNestedMap<K, IK, V>(
    source: ReadonlyMap<K, ReadonlyMap<IK, V>>,
  ): Map<K, Map<IK, V>> {
    const copy = new Map<K, Map<IK, V>>();
    for (const [key, inner] of source) {
      copy.set(key, new Map(inner));
    }
    return copy;
  }
}
 
export default VisibleSymbols;