All files / TRANSPILE/1-Analyze RegisterAccessAnalyzer.ts

97.01% Statements 65/67
89.74% Branches 35/39
100% Functions 15/15
100% Lines 56/56

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                                                                                                                      475x   475x 475x       475x           1785x     1785x 1785x 1785x 1785x   512x 1785x 1785x 1785x 1785x 13x         170x 170x 170x 170x   42x 42x 8x           8x   34x   1x 1x     33x 33x 14x 10x 10x 8x 7x 170x 170x                                           1785x             10x 10x       7x 7x 1x               1x 1x           1x 1x                   14x 6x                           21x 21x           475x     475x 475x 475x 170x       475x   13x          
/**
 * ADR-004 register access modifiers: E0870, E0871, E0872.
 *
 * #1322. Four throws in `output/` -- `MemberAccessValidator` for a read of a
 * `wo` member, `AssignmentValidator` for a write to an `ro` member, and two in
 * `AssignmentHandlerUtils` for a zero assigned to a write-1 bit -- reaching the
 * user as `1:0`, across three fixtures that assert that position.
 *
 * ## One chain reader, five spellings
 *
 * A register member is reached as `R.M`, `this.R.M`, `global.R.M`, `S.R.M`
 * or `global.S.R.M`. Codegen resolved that chain separately on the read path
 * (`PostfixExpressionGenerator`), the whole-member write path
 * (`AssignmentValidator`, which keyed on the FIRST two identifiers) and the bit
 * write path (`RegisterHandlers`, which prefixed the scope). The three
 * disagreed, and the gaps were holes -- all probed before this existed:
 *
 * - `this.R.ST <- 1`, `Board.R.ST <- 1` from `main`, `global.Board.R.ST <- 1`
 *   and `this.R.ST[3] <- true` on an `ro` member of a SCOPED register were
 *   accepted. The emitted C assigns through a `volatile uint32_t const *`
 *   macro, which the C compiler rejects; C-Next said nothing.
 * - `R.SET +<- 1` on a `wo` member was accepted and emitted `R__SET += 1`,
 *   a read of a write-only register. A compound operator reads its target.
 * - `R.SET[3] <- 0x0`, `<- 0b0`, `<- 00` and `<- OFF` with `const u32 OFF <- 0`
 *   were accepted and emitted `R__SET = (1U << 3)` -- the zero the author wrote
 *   to clear a bit SET it, because the check compared the generated text
 *   against the two spellings `"false"` and `"0"`.
 *
 * Here the chain is resolved once, to the member's key in the per-file symbol
 * view, and the three rules read the access modifier from that. The zero rule
 * evaluates the value as a constant rather than reading its spelling.
 *
 * ## What is reproduced, not closed
 *
 * `R.SET <- 0` -- a zero written to the WHOLE `wo` member -- is accepted, as it
 * was. A write-only data or command register takes zero as a value; only the
 * bit forms mean "clear", and only those are rejected. `w1c`/`w1s` members
 * read freely (a status register is read, and written with 1s to clear), and
 * their bits reject zero as codegen's `isWriteOnlyRegister` set did.
 */
 
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 AssignmentSiteListener from "./AssignmentSiteListener";
import ChainRoot from "../../utils/ChainRoot";
import RegisterMemberReference from "./helpers/RegisterMemberReference";
import BoundDeclaration from "./helpers/BoundDeclaration";
import IRegisterMember from "./types/IRegisterMember";
import IRegisterAccessError from "./types/IRegisterAccessError";
import TChainRoot from "../../types/TChainRoot";
import type TAssignmentSite from "./types/TAssignmentSite";
import ConstantExpression from "./helpers/ConstantExpression";
import RegisterAccessMode from "../../utils/RegisterAccessMode";
import type IAnalysisContext from "./types/IAnalysisContext";
 
class RegisterAccessListener extends CNextListener {
  private readonly found: IRegisterAccessError[] = [];
 
  public constructor(private readonly context: IAnalysisContext) {
    super();
  }
 
  public errors(): IRegisterAccessError[] {
    return this.found;
  }
 
  // --- The two positions --------------------------------------------------
 
  /** Every postfix expression is a read: targets are `assignmentTarget`. */
  override enterPostfixExpression = (
    ctx: Parser.PostfixExpressionContext,
  ): void => {
    const primary = ctx.primaryExpression();
    Iif (!primary) return;
    const root: TChainRoot = ChainRoot.ofPrimary(primary);
    const names = ctx
      .postfixOp()
      .map((op) => (op.DOT() !== null ? op.IDENTIFIER()!.getText() : null));
    const head = root === null ? primary.IDENTIFIER()?.getText() : undefined;
    const chain = RegisterMemberReference.leadingNames(head, names);
    const found = this.resolve(root, chain, ctx);
    if (found === null) return;
    this.reportRead(found, ctx);
  };
 
  /** A write, in a statement or a `for` header (#1726) */
  public checkSite(ctx: TAssignmentSite): void {
    const target = ctx.assignmentTarget();
    const ops = target.postfixTargetOp();
    const found = RegisterMemberReference.ofTarget(target, this.context);
    if (found === null) return;
 
    const op = ctx.assignmentOperator().getText();
    if (found.access === "ro") {
      this.report(
        target,
        "E0871",
        `cannot assign to read-only register member '${found.member}' (${found.spelling} has 'ro' access modifier)`,
        "An `ro` register member is hardware that cannot be written (ADR-004); the generated C declares it `const`. Write the register's own writable member, or change the declaration if the hardware allows writes.",
      );
      return;
    }
    if (op !== "<-") {
      // `R.SET +<- 1` reads SET to compute the new value.
      this.reportRead(found, target);
      return;
    }
    // Bit forms: the ops after the member are exactly one subscript.
    const rest = ops.slice(found.consumed - 1);
    if (rest.length !== 1 || rest[0].DOT() !== null) return;
    if (!RegisterAccessMode.isWriteOne(found.access)) return;
    const exprs = rest[0].expression();
    if (!this.isZero(ctx.expression(), target)) return;
    const index = exprs.map((e) => e.getText()).join(", ");
    const what = exprs.length === 1 ? "false to" : "0 to";
    const noun = exprs.length === 1 ? "bit" : "bits";
    this.report(
      target,
      "E0872",
      `Cannot assign ${what} write-only register ${noun} ${found.spelling}[${index}]`,
      "Writing 0 to a write-1 register (`wo`, `w1s`, `w1c`) does not clear the bit -- the hardware ignores zeros, and a spelled-out zero (`0x0`, a const) would have SET it. Use the corresponding CLEAR register to clear bits.",
    );
  }
 
  // --- Resolution ---------------------------------------------------------
 
  /**
   * #1322: the chain walk, the candidate order and the shadowing rule moved to
   * `helpers/RegisterMemberReference` when ADR-034's bitmap rules needed the
   * same answer -- a register member may be TYPED by a bitmap, so a bitmap
   * rule has to reach it through the register. Two copies would have been free
   * to disagree about which register a spelling names.
   */
  private resolve(
    root: TChainRoot,
    chain: string[],
    node: ParserRuleContext,
  ): IRegisterMember | null {
    return RegisterMemberReference.resolve(root, chain, node, this.context);
  }
 
  private isZero(
    expr: Parser.ExpressionContext,
    node: ParserRuleContext,
  ): boolean {
    const text = expr.getText().trim();
    if (text === "false") return true;
    // #1322 review: the flat map again -- and this one decides whether a
    // register write clears a `wo` bit, so the wrong scope's const changed
    // which diagnostic fired. `isFalseConst` below was already scope-aware.
    const value = ConstantExpression.valueAt(expr, this.context);
    if (value !== null) return value === 0;
    return this.isFalseConst(text, node);
  }
 
  /**
   * `const bool NAME <- false`, as the name binds here: a const local, a
   * scope member or a file-scope const, through Program's one binder (#1668)
   */
  private isFalseConst(name: string, node: ParserRuleContext): boolean {
    Iif (!/^[A-Za-z_]\w*$/.test(name)) return false;
    const binding = this.context.program.bindValue(
      this.context.sourceFile,
      null,
      name,
      ParserUtils.getPosition(node),
    );
    const declared = BoundDeclaration.of(binding);
    return (
      declared !== null &&
      declared.isConst &&
      declared.initialValue?.trim() === "false"
    );
  }
 
  // --- Reporting ----------------------------------------------------------
 
  private reportRead(found: IRegisterMember, at: ParserRuleContext): void {
    if (found.access !== "wo") return;
    this.report(
      at,
      "E0870",
      `cannot read from write-only register member '${found.member}' (${found.spelling} has 'wo' access modifier)`,
      "A `wo` register member is hardware that returns nothing meaningful when read (ADR-004). Read the register's own readable member, or keep a shadow copy of what was written.",
    );
  }
 
  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 RegisterAccessAnalyzer {
  /** #1456: handed in rather than read off shared state. */
  constructor(private readonly context: IAnalysisContext) {}
 
  public analyze(tree: Parser.ProgramContext): IRegisterAccessError[] {
    const listener = new RegisterAccessListener(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 RegisterAccessAnalyzer;