All files / TRANSPILE/1-Analyze BitmapAccessAnalyzer.ts

94.73% Statements 72/76
87.5% Branches 49/56
100% Functions 14/14
98.48% Lines 65/66

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                                                                                  468x   468x 468x       468x       1731x     1731x 1731x 1731x 1731x 1731x 504x   1731x           1731x                         161x 161x 161x 124x   161x       161x           161x         27x     161x 27x 27x 27x 5x                                   1892x 1892x   827x 827x 38x         38x 38x 3x           3x     35x 35x     35x 33x 1x 3x 1x               1x   32x                               827x   827x           827x 26x 26x 21x   5x           801x 759x             759x   827x 827x                     9x 9x           468x     468x 468x 468x 161x       468x   4x          
/**
 * ADR-034 bitmap access: E0881, E0882, E0883.
 *
 * #1322. Three throws in three files, each resolving "what bitmap is this?" its
 * own way and all reporting `1:0` -- the bracket-indexing one built a
 * `Error at line N:` prefix into its message, which is a position smuggled
 * through prose rather than carried by the diagnostic.
 *
 * ## One resolution, two ways to hold a bitmap
 *
 * A bitmap is reached either through a declared variable (`Flags f; f.Mode`)
 * or through a register member typed by one (`this.SysTick.CTRL`). Codegen
 * asked those separately -- the literal-overflow check keyed on the assignment
 * handler's already-resolved field, the unknown-field check on a map lookup
 * deep in expression generation, and the bracket check on
 * `registerMemberTypes` keyed by a GENERATED C name, which is why it could
 * only ever fire for registers. Here both routes answer one question,
 * `bitmapOf`, and all three rules ask it.
 *
 * That is also what closes the bracket rule's hole: `f[0]` on a bitmap
 * VARIABLE was accepted and emitted as a bit index, because the check looked
 * only in the register map. ADR-034 says a bitmap is addressed by named field.
 */
 
import { ParserRuleContext, ParseTreeWalker } from "antlr4ng";
 
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import ParserUtils from "../../utils/ParserUtils";
import OperandTyper from "../../utils/OperandTyper";
import AssignmentSiteListener from "./AssignmentSiteListener";
import PROPERTY_NAMES from "../../utils/constants/PROPERTY_NAMES";
import ChainRoot from "../../utils/ChainRoot";
import RegisterMemberReference from "./helpers/RegisterMemberReference";
import ConstantExpression from "./helpers/ConstantExpression";
import IBitmapAccessError from "./types/IBitmapAccessError";
import type TAssignmentSite from "./types/TAssignmentSite";
import TChainRoot from "../../types/TChainRoot";
import type IAnalysisContext from "./types/IAnalysisContext";
 
class BitmapAccessListener extends CNextListener {
  private readonly found: IBitmapAccessError[] = [];
 
  public constructor(private readonly context: IAnalysisContext) {
    super();
  }
 
  public errors(): IBitmapAccessError[] {
    return this.found;
  }
 
  /** Reads: `f.Mode`, `this.SysTick.CTRL.EN`, and the bracket forms. */
  override enterPostfixExpression = (
    ctx: Parser.PostfixExpressionContext,
  ): void => {
    const primary = ctx.primaryExpression();
    Iif (!primary) return;
    const root = ChainRoot.ofPrimary(primary);
    const ops = ctx.postfixOp();
    const names = ops.map((op) =>
      op.DOT() !== null ? op.IDENTIFIER()!.getText() : null,
    );
    const head = root === null ? primary.IDENTIFIER()?.getText() : undefined;
    // A `this.`/`global.` root puts the chain's FIRST name in an op, where a
    // bare head comes from the primary expression and consumes none. Slicing
    // here is what keeps `ops[i]` the op that FOLLOWS `chain[i]` in both
    // forms -- the alignment an assignment target has for free, since its head
    // is always the target's own IDENTIFIER.
    this.checkChain(
      RegisterMemberReference.leadingNames(head, names),
      root,
      root === null ? ops : ops.slice(1),
      ctx,
    );
  };
 
  /**
   * Targets: `f.Mode <- 10;` and `this.SysTick.CTRL[0] <- true;` -- a write,
   * in a statement or a `for` header (#1726).
   */
  public checkSite(ctx: TAssignmentSite): void {
    const target = ctx.assignmentTarget();
    const ops = target.postfixTargetOp();
    const names = ops.map((op) =>
      op.DOT() !== null ? op.IDENTIFIER()!.getText() : null,
    );
    const chain = RegisterMemberReference.leadingNames(
      target.IDENTIFIER().getText(),
      names,
    );
    const bitmapAt = this.checkChain(
      chain,
      ChainRoot.ofTarget(target),
      ops,
      target,
    );
    if (bitmapAt === null) return;
 
    // E0881: the value must fit the field it is written to. Folded by the
    // one binder, so a const is its value (#1760 second review: reading the
    // literal text let `f.Mode <- BIG` truncate silently).
    const layout = this.context.symbols.bitmapFields
      .get(bitmapAt.bitmap)
      ?.get(bitmapAt.field);
    Iif (layout === undefined) return;
    const value = ConstantExpression.valueAt(ctx.expression(), this.context);
    const maximum = 2 ** layout.width - 1;
    if (value === null || value <= maximum) return;
    this.report(
      ctx.expression(),
      "E0881",
      `Value ${value} exceeds ${layout.width}-bit field '${bitmapAt.field}' maximum of ${maximum}`,
      `A ${layout.width}-bit field holds 0 through ${maximum}; widen the field in the bitmap declaration, or write a value that fits (ADR-034).`,
    );
  }
 
  /**
   * Walk one chain. Reports E0882 and E0883 where they apply, and returns the
   * bitmap field a chain ends at when it ends at one.
   */
  private checkChain(
    chain: string[],
    root: TChainRoot,
    ops: readonly (Parser.PostfixOpContext | Parser.PostfixTargetOpContext)[],
    node: ParserRuleContext,
  ): { bitmap: string; field: string } | null {
    const bitmaps = this.context.symbols.bitmapFields;
    if (!bitmaps || chain.length === 0) return null;
 
    const found = this.bitmapOf(chain, root, node);
    if (found === null) return null;
    const { bitmap, at } = found;
 
    // The op that follows the bitmap -- `ops` is aligned by the caller so that
    // `ops[i]` follows `chain[i]`. A subscript there is E0883: ADR-034
    // addresses a bitmap by named field, never by bit index.
    const after = ops[at - 1];
    if (after?.DOT() === null) {
      this.report(
        node,
        "E0883",
        `Cannot use bracket indexing on bitmap type '${bitmap}'`,
        `A bitmap is addressed by named field, not by bit index -- write ${chain[at - 1]}.FIELD_NAME (ADR-034).`,
      );
      return null;
    }
 
    const field = chain[at];
    Iif (field === undefined) return null;
    // ADR-058's properties describe the type's shape rather than name a field,
    // so they are not unknown fields. E0867 owns whether one is used correctly.
    if (PROPERTY_NAMES.has(field)) return null;
    if (!bitmaps.get(bitmap)?.has(field)) {
      const known = [...(bitmaps.get(bitmap)?.keys() ?? [])];
      const quoted = known.map((k) => `'${k}'`).join(", ");
      this.report(
        node,
        "E0882",
        `Unknown bitmap field '${field}' on '${bitmap}'`,
        known.length === 0
          ? "The bitmap declares no fields (ADR-034)."
          : `'${bitmap}' declares ${quoted} (ADR-034).`,
      );
      return null;
    }
    return { bitmap, field };
  }
 
  /**
   * The bitmap a chain reaches and how many names it took to get there.
   *
   * Two routes, asked in the order codegen resolved them: a register member
   * whose declared type is a bitmap, then a declared variable whose type is
   * one. A register wins because its spelling (`this.SysTick.CTRL`) consumes
   * more names than a variable ever can.
   */
  private bitmapOf(
    chain: string[],
    root: TChainRoot,
    node: ParserRuleContext,
  ): { bitmap: string; at: number } | null {
    const symbols = this.context.symbols;
 
    const member = RegisterMemberReference.resolve(
      root,
      chain,
      node,
      this.context,
    );
    if (member !== null) {
      const type = symbols.registerMemberTypes.get(member.key);
      if (type !== undefined && symbols.bitmapFields.has(type)) {
        return { bitmap: type, at: member.consumed };
      }
      return null;
    }
 
    // A declared variable: `Flags f;` then `f.Mode`, or `this.f.Mode`. The
    // typer binds the root and names its bitmap type as `bitmapFields` keys
    // it, however the declaration spelled the type (#1668)
    if (root === "global") return null;
    Iif (
      !(node instanceof Parser.PostfixExpressionContext) &&
      !(node instanceof Parser.AssignmentTargetContext)
    ) {
      return null;
    }
    // An ARRAY of bitmaps is not a bitmap: `arr[0].Mode` indexes the array
    const declared = OperandTyper.chainOf(node, this.context).steps[0]?.before;
    const bitmap =
      declared?.dimensions.length === 0 ? declared.bitmapTypeName : null;
    return bitmap !== null && symbols.bitmapFields.has(bitmap)
      ? { bitmap, at: 1 }
      : null;
  }
 
  private report(
    at: ParserRuleContext,
    code: string,
    message: string,
    helpText: string,
  ): void {
    const { line, column } = ParserUtils.getPosition(at);
    this.found.push({ code, line, column, message, helpText });
  }
}
 
class BitmapAccessAnalyzer {
  /** #1456: handed in rather than read off shared state. */
  constructor(private readonly context: IAnalysisContext) {}
 
  public analyze(tree: Parser.ProgramContext): IBitmapAccessError[] {
    const listener = new BitmapAccessListener(this.context);
    ParseTreeWalker.DEFAULT.walk(listener, tree);
    ParseTreeWalker.DEFAULT.walk(
      new AssignmentSiteListener((site) => listener.checkSite(site)),
      tree,
    );
    // Reported in source order, as the one walk these replace did
    return listener
      .errors()
      .sort((a, b) => a.line - b.line || a.column - b.column);
  }
}
 
export default BitmapAccessAnalyzer;