All files / PARSE/4-Resolve ModificationFacts.ts

91.01% Statements 81/89
75.86% Branches 44/58
83.33% Functions 10/12
97.33% Lines 73/75

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 356 357 358 359 360 361 362 363                                                                                          3228x     3228x 3228x 3228x 3224x 3224x 3205x   3224x 3205x   3224x 3205x           3228x 3228x 3204x   30x                               3228x 3228x 3224x 6861x 2160x       3228x                       3228x                             20x 20x 20x 20x   20x                                               3246x         30x                                                   30x 30x 30x 1x   29x 1x                       28x 28x 28x 28x                                                     30x               2x     28x 28x             28x 23x             23x   23x               23x 23x 21x 21x               14x           14x                                   21x 18x 16x                         16x 16x 16x 37x 29x                 29x 27x 27x       27x 23x         2x                     27x 27x 23x 23x 23x   4x          
/**
 * The whole-program parameter-modification facts (ADR-006).
 *
 * 1.3 Declare records what each file's functions do to their own parameters
 * (`ModificationCollector`). Whether a call passes a parameter to a callee
 * that modifies it needs the callee, which is routinely in another file -- so
 * resolving the callee and propagating along the call graph are 1.4's, here.
 *
 * #1825: this used to sit in the orchestration layer, running 2.2 Plan's
 * collector over every tree, because it was the one place both halves were
 * reachable and `PARSE/` may not import `TRANSPILE/`. `Program.build` is its
 * one caller now, so the orchestrator and the test harness cannot each spell
 * the sequence -- these facts decide generated signatures, and #1161's fixture
 * caught exactly that kind of divergence between a `.c` and its `.h` (#1511).
 */
 
import type SymbolTable from "../3-Declare/SymbolTable";
import type SymbolRegistry from "../3-Declare/SymbolRegistry";
import type IFileSymbols from "../../types/IFileSymbols";
import type IModificationFacts from "./types/IModificationFacts";
import type ICallGraphEntry from "../../types/ICallGraphEntry";
import type IDeclaredCall from "../../types/IDeclaredCall";
import TransitiveModificationPropagator from "./TransitiveModificationPropagator";
import ScopeUtils from "../../utils/ScopeUtils";
import QualifiedCName from "../../utils/QualifiedCName";
import ESourceLanguage from "../../utils/types/ESourceLanguage";
 
class ModificationFacts {
  /**
   * Derive the parameter-modification facts for the WHOLE program.
   *
   * Every file's facts are merged first and the graph is propagated once,
   * which is what makes the result independent of file order. A later file's
   * declaration of a name replaces an earlier one's, as the single walk over
   * every tree that preceded this did.
   *
   * @param files every file's `IFileSymbols`, in declaration order
   * @param registry the run's scope graph; null where a test builds a program
   *        without one, which resolves every bare callee as written
   */
  static derive(
    files: ReadonlyArray<IFileSymbols>,
    registry: SymbolRegistry | null,
    symbolTable: SymbolTable,
  ): IModificationFacts {
    const functionParamLists = new Map<string, ReadonlyArray<string>>();
    // Copied, because propagation adds to these sets and 1.3's artifact must
    // keep saying what each file wrote.
    const modifiedParameters = new Map<string, Set<string>>();
    const declaredCalls = new Map<string, ReadonlyArray<IDeclaredCall>>();
    for (const file of files) {
      const facts = file.modifications;
      for (const [name, params] of facts.functionParamLists) {
        functionParamLists.set(name, params);
      }
      for (const [name, params] of facts.modifiedParameters) {
        modifiedParameters.set(name, new Set(params));
      }
      for (const [name, calls] of facts.calls) {
        declaredCalls.set(name, calls);
      }
    }
 
    // Resolved only now: the registry holds every file's scopes, so a bare
    // call into a scope reopened elsewhere (#1333) finds its member.
    const callGraph = new Map<string, ReadonlyArray<ICallGraphEntry>>();
    for (const [caller, calls] of declaredCalls) {
      callGraph.set(
        caller,
        calls.map((call) => ({
          callee: call.calleeIsBare
            ? ModificationFacts.resolveBareCallee(registry, caller, call.callee)
            : call.callee,
          paramIndex: call.paramIndex,
          argParamName: call.argParamName,
        })),
      );
    }
 
    // The C-Next symbols are not in the table yet -- `_publishResolvedFile`
    // puts them there, after this. Without them every scope field holding a
    // callback reads as an undeclared function, so #1178's fail-safe fires on
    // the very calls it exists to spare and the parameter is wrongly promoted
    // to a pointer. They are in hand right here, so the predicate is supplied
    // rather than left to depend on when a mutable table happens to be filled.
    const cnextValueCNames = new Set<string>();
    for (const file of files) {
      for (const symbol of file.symbols) {
        if (symbol.kind === "variable") {
          cnextValueCNames.add(symbol.fullyQualifiedCName);
        }
      }
    }
    ModificationFacts.propagate(
      callGraph,
      functionParamLists,
      modifiedParameters,
      symbolTable,
      (name: string): boolean =>
        cnextValueCNames.has(name) ||
        symbolTable
          .getOverloadsByCName(name)
          .some((symbol) => symbol.kind === "variable"),
    );
 
    return { modifiedParameters, functionParamLists };
  }
 
  /**
   * Issue #797: Resolve a bare function name to its scope-qualified name.
   * When inside a scope, bare calls like `fillData()` should resolve to
   * `Scope__fillData`, through ADR-057's scope chain. A name the chain does
   * not resolve -- a C or C++ function, or one this build cannot see -- is
   * the name as written.
   */
  private static resolveBareCallee(
    registry: SymbolRegistry | null,
    callerFuncName: string,
    bareCalleeName: string,
  ): string {
    Iif (!registry) return bareCalleeName;
    const callerScope = registry.getScopeByCFunctionName(callerFuncName);
    Iif (!callerScope) return bareCalleeName;
    const callee = registry.resolveFunction(bareCalleeName, callerScope);
    // ScopeUtils.getTranspiledCName is the single encoder for symbol identity.
    return callee ? ScopeUtils.getTranspiledCName(callee) : bareCalleeName;
  }
 
  /**
   * Run transitive modification propagation with the project's standard
   * callee resolver.
   *
   * Public for the resolver's own tests, which drive it with a call graph
   * rather than a program. `isValueSymbol` is required because the table is
   * filled as files are PUBLISHED, after this runs: asking it alone answers
   * "no" for every C-Next scope field, which turns each callback into an
   * unresolvable callee and fires #1178's fail-safe on exactly the calls #1178
   * exists to spare -- a wrong answer produced by call ORDER, not by the
   * program.
   *
   * @param modifiedParameters added to in place
   */
  static propagate(
    callGraph: ReadonlyMap<string, ReadonlyArray<ICallGraphEntry>>,
    functionParamLists: ReadonlyMap<string, ReadonlyArray<string>>,
    modifiedParameters: Map<string, Set<string>>,
    symbolTable: SymbolTable,
    isValueSymbol: (name: string) => boolean,
  ): void {
    TransitiveModificationPropagator.propagate(
      callGraph,
      functionParamLists,
      modifiedParameters,
      (callerName: string, callee: string, paramIndex: number): boolean =>
        ModificationFacts.calleeMayMutateParameter(
          functionParamLists,
          callerName,
          callee,
          paramIndex,
          isValueSymbol,
          symbolTable,
        ),
    );
  }
 
  /**
   * Whether this call invokes a value rather than a named function.
   *
   * An ADR-029 callback is called through a parameter (`cb(value)`), a scope
   * field (`listener(s)`) or a struct field (`config.listener(s)`). The name
   * recorded in the call graph is that value's, so no declaration will ever
   * match it -- which is a different fact from "this function is declared
   * somewhere this build cannot see".
   */
  private static calleeIsIndirectCall(
    functionParamLists: ReadonlyMap<string, ReadonlyArray<string>>,
    callerName: string,
    callee: string,
    isValueSymbol: (name: string) => boolean,
  ): boolean {
    const root = QualifiedCName.split(callee)[0];
    const callerParameters = functionParamLists.get(callerName) ?? [];
    if (callerParameters.includes(callee) || callerParameters.includes(root)) {
      return true;
    }
    if (isValueSymbol(callee) || isValueSymbol(root)) {
      return true;
    }
 
    // A scope field is indexed under its transpiled name (`Bus__listener`),
    // while the call graph records the bare name the source used, so qualify
    // with the caller's own scope before giving up.
    //
    // #1357: swap the caller's own leaf for the field name, keeping every
    // component before it. Taking `split(...)[0]` read only the OUTERMOST
    // component, so at depth two `Outer__Inner__handler` asked about
    // `Outer__field` -- a name that does not exist -- instead of
    // `Outer__Inner__field`.
    const parts = QualifiedCName.split(callerName);
    Iif (parts.length < 2) return false;
    parts[parts.length - 1] = root;
    return isValueSymbol(QualifiedCName.fromParts(parts));
  }
 
  /**
   * Issue #1178: answer "may this callee mutate the caller's argument through
   * this parameter?" for a callee that is not a C-Next function in this build.
   *
   * The propagator reaches here only when `functionParamLists` has no entry for
   * the callee. That used to mean "assume pure", which applied auto-const on the
   * strength of an absent answer. A C or C++ declaration is a definitive answer,
   * so consult it; only a callee nothing knows about falls back to the safe
   * assumption that it mutates.
   */
  private static calleeMayMutateParameter(
    functionParamLists: ReadonlyMap<string, ReadonlyArray<string>>,
    callerName: string,
    callee: string,
    paramIndex: number,
    isValueSymbol: (name: string) => boolean,
    symbolTable: SymbolTable,
  ): boolean {
    // ADR-029: an indirect call invokes a *value* -- a callback parameter, a
    // scope field, a struct field -- not a function name. Nothing will ever
    // declare it, so failing safe would fire on every callback that forwards
    // one of its caller's parameters, by construction rather than by accident.
    // Keep the pre-#1178 answer there; resolving the callback's declared
    // target is tracked separately.
    if (
      ModificationFacts.calleeIsIndirectCall(
        functionParamLists,
        callerName,
        callee,
        isValueSymbol,
      )
    ) {
      return false;
    }
 
    const symbols = symbolTable.getOverloadsByCName(callee);
    let sawCandidate = false;
 
    // Fold across every overload rather than answering from the first one.
    // Returning on the first match made the answer depend on declaration order
    // in the header: `store(const Sample&)` declared before
    // `store(Sample&, bool)` claimed the call could not mutate, for a call that
    // can only resolve to the second. Any candidate that may mutate wins.
    for (const symbol of symbols) {
      Iif (symbol.kind !== "function") continue;
      // getOverloadsByCName spans all three languages. A C-Next IFunctionSymbol
      // also has kind "function", but its IParameterInfo.type is a TType
      // object rather than a string, so the structural read below would be a
      // lie for it -- and typeIsIndirect would call .replace() on an object.
      // This method's premise is "not a C-Next function in this build", so say
      // so rather than letting the cast paper over it.
      Iif (symbol.sourceLanguage === ESourceLanguage.CNext) continue;
      const parameters = (
        symbol as {
          parameters?: ReadonlyArray<{
            type?: string;
            isArray?: boolean;
            isConst?: boolean;
          }>;
        }
      ).parameters;
      const parameter = parameters?.[paramIndex];
      if (!parameter) continue;
      sawCandidate = true;
      if (
        ModificationFacts.parameterCarriesIndirection(
          parameter.type ?? "",
          parameter.isArray ?? false,
          parameter.isConst ?? false,
          symbolTable,
        )
      ) {
        return true;
      }
    }
 
    // Nothing declares this callee at this position -- withhold auto-const
    // rather than assume purity. Explicit rather than a fallthrough.
    return !sawCandidate;
  }
 
  /**
   * Whether a C/C++ parameter lets the callee change something the caller can
   * observe.
   *
   * A by-value parameter is a copy, so it cannot. An array, pointer or
   * reference can -- unless the declaration says const, in which case the
   * callee may not write through it and auto-const on the caller's parameter
   * is still sound.
   */
  private static parameterCarriesIndirection(
    type: string,
    isArray: boolean,
    isConst: boolean,
    symbolTable: SymbolTable,
  ): boolean {
    if (isConst) return false;
    if (isArray) return true;
    return ModificationFacts.typeIsIndirect(type, symbolTable);
  }
 
  /**
   * Follow typedef aliases looking for pointer or reference indirection.
   * A typedef can hide it entirely (`typedef struct spi_device_t
   * *spi_device_handle_t`), so the alias chain is followed rather than the
   * spelling pattern-matched. Bounded so a self-referential chain cannot spin.
   */
  private static typeIsIndirect(
    type: string,
    symbolTable: SymbolTable,
  ): boolean {
    let current = type;
    const seen = new Set<string>();
    for (let hop = 0; hop < 8; hop++) {
      if (/[*&]/.test(current)) return true;
      const bare = current
        .replace(/\b(const|volatile|struct|union|enum)\b/g, "")
        .trim();
      // Both exits mean the chain is known and unfinished, exactly as running
      // out of hops does below -- so they answer the same way. An empty type is
      // unknown rather than by-value for the same reason. Neither is reachable
      // from valid C (a self-referential typedef is ill-formed and
      // ICParameterInfo.type is a required string), so nothing observable turns
      // on it; they are aligned so the three exits do not read as disagreeing.
      if (!bare || seen.has(bare)) return true;
      seen.add(bare);
      const alias = ModificationFacts.resolveTypedefTarget(bare, symbolTable);
      // Deliberate exception: an alias this build never parsed (uint8_t,
      // size_t) is treated as a plain value. Calling it indirection would
      // reintroduce exactly the #957/#995 false positives measured for #1178.
      if (alias === null) return false;
      current = alias;
    }
    // Out of hops means the chain is known and unfinished, not unknown --
    // answering "by value" here would be the same collapse of "I cannot tell"
    // into "it is pure" that #1178 removes one level up.
    return true;
  }
 
  /**
   * The underlying type of a C/C++ typedef, or null when the name is not a
   * typedef this build has seen.
   */
  private static resolveTypedefTarget(
    name: string,
    symbolTable: SymbolTable,
  ): string | null {
    const symbols = symbolTable.getOverloadsByCName(name);
    for (const symbol of symbols) {
      Iif (symbol.kind !== "type") continue;
      const aliased = (symbol as { type?: string }).type;
      Eif (typeof aliased === "string" && aliased.length > 0) return aliased;
    }
    return null;
  }
}
 
export default ModificationFacts;