All files / PARSE/3-Declare NameExistence.ts

100% Statements 10/10
100% Branches 13/13
100% Functions 7/7
100% Lines 10/10

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                                                                                                                                                                                      418x                                                                                                         271x                                         189x                   17x 4x 2x     15x               418x                                                   198x   117x          
/**
 * NameExistence — one answer to "does this name denote anything visible HERE?"
 *
 * `TypeBinding` maps a type context to a C name; it never asks whether the name
 * denotes a type, and its `null` means "no grammar alternative matched", not
 * "not found". So every position that consumes a name fell back to emitting the
 * raw source text: `CodeGenerator.getTypeName` via `resolved ?? ctx.getText()`
 * (#1312) and `TypeValidator.resolveBareIdentifier` via a `null` that conflates
 * "emit unchanged, it is fine" with "no idea what this is" (#1353). Both exited
 * 0 and emitted C that the C compiler rejects.
 *
 * ## Why two sources of truth, and which one answers which question
 *
 * The two available symbol views disagree, and the disagreement IS #1312:
 *
 * | view                              | scope     | sibling case says |
 * | --------------------------------- | --------- | ----------------- |
 * | `ICodeGenSymbols` `known*` sets    | per FILE  | `Mode` unknown    |
 * | `SymbolTable.getOverloadsByCName`  | whole RUN | `Mode` known      |
 *
 * `ICodeGenSymbols` is built by 1.4's `Program.deriveVisibleSymbols`, over the
 * include graph discovery resolved, so it holds exactly what this file can see
 * (#1435; it cited a `_declareFile` signature #1472 removed). The
 * `SymbolTable` accumulates every file in the run and is cleared once, so a
 * sibling that was never included is still in it. Asking the run-wide table
 * whether a C-Next type exists would answer "yes" in the file that cannot see
 * it, and the diagnostic would never fire where it is needed -- the trap #1312
 * names explicitly.
 *
 * So the two questions are routed to the view that can answer them:
 *
 *   - a **C-Next** name is checked against the per-file sets, because C-Next
 *     visibility is what the include graph governs;
 *   - a **C/C++** name is checked against the run-wide table, because external
 *     types arrive through header includes that are already resolved per file,
 *     and because the failure directions are not symmetric. Missing an external
 *     type means a diagnostic does not fire (the status quo). Wrongly rejecting
 *     one means valid code stops compiling. Only the second is a regression, so
 *     the external side is deliberately permissive.
 *
 * `TranspileState.isScopeType()` cannot serve here either: it answers only
 * for a type declared inside a SCOPE that the file being generated can see
 * (#1724), so it says nothing about a file-scope C-Next type or a C/C++ one.
 */
 
import ESourceLanguage from "../../utils/types/ESourceLanguage";
import ICodeGenSymbols from "../../types/ICodeGenSymbols";
import SymbolTable from "./SymbolTable";
 
class NameExistence {
  /**
   * Whether a bare type name denotes a type this file can see.
   *
   * `CodeGenState.callbackTypes` is deliberately NOT consulted. It is codegen
   * state -- filled by `CodeGenerator.registerCallbackType` and cleared at the
   * start of `generate()`, both of which run after the analyzers -- so at
   * analysis time it is empty for the first file and holds file N-1's function
   * names for every file after. Reading it made E0426 order-dependent: the same
   * two files with their `#include` lines swapped either diagnosed the undefined
   * type or emitted C the compiler rejects at exit 0. `_isKnownCNextType` already asks
   * `symbols.functionReturnTypes`, which is the per-file view of the same
   * ADR-029 fact and is correct here.
   *
   * Only the bare `userType()` branch belongs here. `this.T`, `global.T` and
   * `Scope.T` state their scope in the syntax and are resolved by their own
   * branches; once a name is a string those answers are indistinguishable from
   * a bare one, which is the same reason `TypeBinding` keeps them separate.
   *
   * ## A register is not a type (#1336)
   *
   * `symbols.knownRegisters` is deliberately NOT consulted. `TYPE_FORMING_KINDS`
   * already owns "does this kind introduce a type name" and already excludes
   * `register` for this reason; this predicate answers the same question from
   * the per-file name sets, and used to disagree with that owner. The
   * disagreement WAS the bug: E0426 asks whether a name is a type, the register
   * set answered "yes", and `Control c;` reached codegen with no typedef behind
   * it -- exit 0, then `unknown type name 'Control'` from the C compiler.
   *
   * ADR-004 is what makes the exclusion correct today: a register declares a
   * variable at an address, not a type. ADR-111 would make a register name a
   * type, but it is `Research`, and its own header states that while it is
   * Research "a register is still not a type".
   *
   * ADR-111: when it is IMPLEMENTED (not merely Accepted), add `knownRegisters`
   * back here, drop `isValueName` and `isRegisterName`, and retire E0429.
   */
  static isTypeName(
    typeName: string,
    symbols: ICodeGenSymbols,
    symbolTable: SymbolTable,
  ): boolean {
    return (
      NameExistence._isKnownCNextType(typeName, symbols, symbolTable) ||
      NameExistence._isKnownForeignName(typeName, symbolTable)
    );
  }
 
  /**
   * Whether a bare name denotes anything usable in a VALUE position.
   *
   * This is `isTypeName` plus registers plus file-scope variables, and the
   * difference is the whole point of the split (#1336). A type answers here
   * because it is the base of `Type.MEMBER`; a register answers here because
   * `GPIO.DR` reads a value at an address; a variable answers here because
   * being a value is what it is. One predicate served both positions and so
   * had to say "yes" to a register, which suppressed the type-position
   * diagnostic and let `Control c;` reach codegen with no type behind it.
   *
   * Each added term is named in exactly one place -- `isRegisterName`, and the
   * `knownVariables` read below -- so the two positions differ by those terms
   * rather than by two lists that must be kept in step.
   *
   * ## Why the variable term lives here (#1502 review)
   *
   * It was written in `UndeclaredValueAnalyzer.isVisible` first, three lines
   * above the call to this predicate, which made the opening sentence of this
   * comment false: a file-scope `const` is the most ordinary thing usable in a
   * value position, and this answered no while the caller quietly answered yes.
   * That is two modules deciding one question -- CLAUDE.md's rule that a single
   * source of truth is the DECISION and not merely the data -- with the module
   * that claims the question holding the incomplete answer.
   *
   * `knownVariables` is a per-file set, so it belongs on the per-file side of
   * the table above, beside the type-forming kinds. The run-wide `SymbolTable`
   * must NOT answer it, and that is #1398 itself: a const declared in a sibling
   * this file never included was reachable through `ScopeFrameResolver`'s
   * run-wide fallback, so E0427 could not fire across a file boundary while
   * E0426 fired for the identical type case.
   *
   * One call site passes a qualified `Scope__name`, which no bare file-scope
   * name can equal unless a variable is declared spelled with the transpiler's
   * own separator. That exposure is neither new nor specific to variables --
   * every `known*` set above is read on the same key by the same call -- and it
   * errs toward NOT firing, so its cost is a missed diagnostic rather than a
   * rejection of valid code.
   *
   * ADR-111: if a register becomes a type, this stops differing from
   * `isTypeName` by the register term. The variable term stays either way.
   */
  static isValueName(
    name: string,
    symbols: ICodeGenSymbols,
    symbolTable: SymbolTable,
  ): boolean {
    return (
      NameExistence.isTypeName(name, symbols, symbolTable) ||
      NameExistence.isRegisterName(name, symbols) ||
      // #1398: a file-scope variable or const declared in this file or in a
      // `.cnx` file it includes. Per-file on purpose -- see above.
      symbols.knownVariables.has(name)
    );
  }
 
  /**
   * Whether a bare name denotes a register (ADR-004).
   *
   * Separate from the two position predicates because the type position needs
   * to tell "this name is a register" apart from "this name is nothing at all"
   * -- E0429 against E0426. Answering "not a type" is enough to reject; naming
   * *why* is what makes the diagnostic worth reading, since the register is
   * declared right there in the file.
   *
   * ADR-111: retire this along with E0429 when a register becomes a type.
   */
  static isRegisterName(name: string, symbols: ICodeGenSymbols): boolean {
    return symbols.knownRegisters.has(name);
  }
 
  /**
   * ADR-017: an enum member may be written bare where the expected type makes
   * it unambiguous -- a variable declaration, a struct field initializer, a
   * switch case, a return statement, a ternary arm. It is a value, but it is
   * neither a variable nor a type, so neither of the other lookups sees it.
   */
  static isKnownEnumMember(name: string, symbols: ICodeGenSymbols): boolean {
    for (const members of symbols.enumMembers.values()) {
      if (members.has(name)) {
        return true;
      }
    }
    return false;
  }
 
  private static _isKnownCNextType(
    typeName: string,
    symbols: ICodeGenSymbols,
    symbolTable: SymbolTable,
  ): boolean {
    return (
      symbols.knownEnums.has(typeName) ||
      symbols.knownStructs.has(typeName) ||
      symbols.knownBitmaps.has(typeName) ||
      symbols.knownScopes.has(typeName) ||
      // #1511: from the table, which shares `OpaqueTypeResolution` with the
      // artifact. `ICodeGenSymbols` carried a merged copy of this set purely so
      // this line could read it per file; one decision, asked where it lives.
      symbolTable.isOpaqueType(typeName) ||
      // ADR-029: a function definition creates a callback type, so every
      // function name is also a type name. `CodeGenState.callbackTypes` is
      // filled during codegen, which is after this runs, so the per-file
      // function map is the view that has the answer at analysis time.
      symbols.functionReturnTypes.has(typeName)
    );
  }
 
  /**
   * A name declared by an included C or C++ header. Checked against the
   * run-wide table on purpose -- see the class comment on why the external side
   * is permissive.
   */
  private static _isKnownForeignName(
    name: string,
    symbolTable: SymbolTable,
  ): boolean {
    return symbolTable
      .getOverloadsByCName(name)
      .some((symbol) => symbol.sourceLanguage !== ESourceLanguage.CNext);
  }
}
 
export default NameExistence;