All files / transpiler/state SymbolRegistry.ts

97.56% Statements 40/41
91.66% Branches 22/24
100% Functions 11/11
100% Lines 35/35

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                                  99x     99x                           4236x                   3x 3x                               2894x 2893x   372x 372x 372x   372x   2894x 2894x 2894x                                                             1226x 1220x                                                     1226x 321x                                       813x 561x     64x 23x     41x                           1255x 1255x                                   194x 684x 177x         17x 15x 16x 14x         3x                       190x 190x          
/**
 * SymbolRegistry - Central registry for C-Next symbol management
 *
 * Provides centralized storage and lookup for all symbols in the C-Next transpiler.
 *
 * Design decisions:
 * - Static class with global state (reset between transpilation runs)
 * - `getOrCreateScope` handles scope merging across files (same scope name = same object)
 * - `resolveFunction` walks scope chain (current -> parent -> global)
 * - String keys in Maps for lookup, but values are proper symbol objects
 */
import ScopeUtils from "../../utils/ScopeUtils";
import type IScopeSymbol from "../types/symbols/IScopeSymbol";
import type IFunctionSymbol from "../types/symbols/IFunctionSymbol";
 
class SymbolRegistry {
  /** The global scope singleton (recreated on reset) */
  private static globalScope: IScopeSymbol = ScopeUtils.createGlobalScope();
 
  /** Map from scope path (e.g., "Outer.Inner") to scope object */
  private static readonly scopes: Map<string, IScopeSymbol> = new Map();
 
  // ============================================================================
  // Scope Management
  // ============================================================================
 
  /**
   * Get the global scope singleton.
   *
   * The global scope has:
   * - name: "" (empty string)
   * - parent: points to itself (self-reference)
   */
  static getGlobalScope(): IScopeSymbol {
    return this.globalScope;
  }
 
  /**
   * Get a scope by its dotted path without creating it.
   *
   * Use this for read-only lookups where you don't want to create
   * orphaned scopes. Returns null if the scope doesn't exist.
   */
  static getScope(path: string): IScopeSymbol | null {
    Iif (path === "") return this.globalScope;
    return this.scopes.get(path) ?? null;
  }
 
  /**
   * Get or create a scope by its dotted path.
   *
   * For simple names (e.g., "Test"), creates scope with global parent.
   * For dotted paths (e.g., "Outer.Inner"), creates nested scopes.
   *
   * If the scope already exists, returns the existing scope.
   * This enables scope merging across files.
   *
   * Note: This creates scopes that don't exist. For read-only lookups,
   * use getScope() instead to avoid creating orphaned scopes.
   */
  static getOrCreateScope(path: string): IScopeSymbol {
    if (path === "") return this.globalScope;
    if (this.scopes.has(path)) return this.scopes.get(path)!;
 
    const parts = path.split(".");
    const name = parts.pop()!;
    const parentPath = parts.join(".");
    const parent =
      parentPath === "" ? this.globalScope : this.getOrCreateScope(parentPath);
 
    const scope = ScopeUtils.createScope(name, parent);
    this.scopes.set(path, scope);
    return scope;
  }
 
  // ============================================================================
  // Function Management
  // ============================================================================
 
  /**
   * Register a function in its scope.
   *
   * This is idempotent, and that is the CONTRACT of a declare pass rather than a
   * workaround for one caller. #1313 states the target as "a pass is a pure
   * function of its input", so registering the same declaration twice must leave
   * the registry as one registration left it, however many times it is called.
   *
   * #1358 introduced the guard for a concrete reason: Transpiler stages 3 and 5
   * each resolved every file while `reset()` ran once per run, so an unconditional
   * push appended a second copy of every function in the program. #1301 has since
   * removed that double pass -- stage 5 consumes the declare stage 3 performed --
   * so nothing in the pipeline calls this twice today. The guard is kept
   * deliberately, as a ratchet: a second unconditional pass, if one is ever
   * reintroduced, is absorbed here rather than silently duplicating. Its live
   * callers are the idempotence test and negative control in
   * `__tests__/SymbolRegistry.test.ts`.
   *
   * Idempotence here must key on the SYMBOL, never on the scope. `getOrCreateScope`
   * is deliberately repeat-safe because that is the mechanism by which a scope
   * spanned across two files merges (#1333); suppressing registration for an
   * already-seen scope would break spanned scopes, which are a designed feature.
   */
  static registerFunction(func: IFunctionSymbol): void {
    if (this.isAlreadyRegistered(func)) return;
    func.scope.functions.push(func);
  }
 
  /**
   * Is `func` a re-registration of a declaration already in its scope?
   *
   * Keyed on `fullyQualifiedCName`, but this does NOT rest on ADR-063's
   * program-wide injectivity. The search is over `func.scope.functions` -- one
   * scope's array -- where the qualified prefix is constant, so the key reduces
   * to the bare name. The property actually required is the narrower "no two
   * functions in ONE scope share a name", which is the stronger result: it holds
   * even if the encoder changes.
   *
   * E0425 is what supplies it. C-Next has no function overloads, so two functions
   * sharing a name in a scope are rejected by `SymbolTable.detectCNextDuplicate`
   * before anything reads this list, whether their signatures differ or not --
   * gated by tests/bugs/issue-1358-declare-idempotence/. The same holds in the
   * global scope, where two same-named top-level functions in different files
   * also raise E0425 (verified, not assumed). So a collision reaching this point
   * is always the same declaration seen twice, never two declarations.
   *
   * Deliberately NOT keyed on the scope. `getOrCreateScope` is repeat-safe by
   * design -- that is how a scope spanned across two files merges (#1333) -- so
   * suppressing registration for an already-seen scope would break spanned
   * scopes. The negative control in the tests covers exactly that.
   */
  private static isAlreadyRegistered(func: IFunctionSymbol): boolean {
    return func.scope.functions.some(
      (existing) => existing.fullyQualifiedCName === func.fullyQualifiedCName,
    );
  }
 
  /**
   * Resolve a function by name, walking the scope chain.
   *
   * Searches in order:
   * 1. Current scope
   * 2. Parent scope
   * 3. Parent's parent (recursively)
   * 4. Global scope
   *
   * Returns null if the function is not found.
   */
  static resolveFunction(
    name: string,
    fromScope: IScopeSymbol,
  ): IFunctionSymbol | null {
    // Search in current scope
    const found = fromScope.functions.find((f) => f.name === name);
    if (found) return found;
 
    // Walk up the scope chain (stop when we reach global scope's self-reference)
    if (fromScope !== this.globalScope && fromScope.parent !== fromScope) {
      return this.resolveFunction(name, fromScope.parent);
    }
 
    return null;
  }
 
  // ============================================================================
  // Reset
  // ============================================================================
 
  /**
   * Reset all registry state.
   *
   * Creates a fresh global scope and clears all registered scopes.
   * Call this between transpilation runs.
   */
  static reset(): void {
    this.globalScope = ScopeUtils.createGlobalScope();
    this.scopes.clear();
  }
 
  // ============================================================================
  // Bridge Methods (for gradual migration from string-based lookups)
  // ============================================================================
 
  /**
   * Find a function by its transpiled C name (e.g., "Test_fillData").
   *
   * This is a bridge method for gradual migration. New code should use
   * resolveFunction() with bare names and scope references instead.
   *
   * @param cName Transpiled C function name (e.g., "Test_fillData", "main")
   * @returns The function symbol, or null if not found
   */
  static findByCName(cName: string): IFunctionSymbol | null {
    // Check global scope first (no underscore = global function)
    for (const func of this.globalScope.functions) {
      if (func.name === cName) {
        return func;
      }
    }
 
    // Check all scopes - the C name should match scope_name pattern
    for (const scope of this.scopes.values()) {
      for (const func of scope.functions) {
        if (ScopeUtils.getTranspiledCName(func) === cName) {
          return func;
        }
      }
    }
 
    return null;
  }
 
  /**
   * Get the scope of a function given its transpiled C name.
   *
   * This is a bridge method for gradual migration.
   *
   * @param cName Transpiled C function name
   * @returns The scope the function belongs to, or null if not found
   */
  static getScopeByCFunctionName(cName: string): IScopeSymbol | null {
    const func = this.findByCName(cName);
    return func?.scope ?? null;
  }
}
 
export default SymbolRegistry;