All files / TRANSPILE/1-Analyze DefineDirectiveAnalyzer.ts

100% Statements 19/19
100% Branches 4/4
100% Functions 5/5
100% Lines 19/19

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                                                                                                    46x 6x 6x 6x       497x     497x                               13x     13x 2x 2x           2x     11x 4x 4x                             6x 6x           497x 497x 497x          
/**
 * ADR-037 `#define` shape: E0501, E0502.
 *
 * #1322. Two throws in `IncludeGenerator.processDefineDirective`, both
 * reporting `1:0` after computing the real line and spending it on prose --
 * `… Use inline functions instead. Line 7`. A file whose offending directive
 * sits on line 7 reported position 1:0 and said "Line 7" in its message, which
 * is the exact shape this card exists to remove.
 *
 * ## The plan said these needed the preprocessor artifact. They do not.
 *
 * #1322's definition of done groups E0501/E0502 with "the six include
 * diagnostics (E0501-E0506), which need the preprocessor artifact rather than
 * a tree walk". That is false, and it is worth writing down why, because the
 * sentence reads plausible: a `#define` inside an INCLUDED C HEADER genuinely
 * is not in the C-Next parse tree -- but it never reached these throws either.
 * C headers are lexed by the C grammar, whose directive rule sends every
 * `#`-line to ANTLR's hidden channel before parsing, so a header holding
 * `#define SQUARE(x) ((x) * (x))` transpiles at exit 0 today and always did.
 * The throws' own parameter type is the proof: `DefineDirectiveContext` is a
 * C-NEXT grammar node, and the C grammar has no such rule to produce one.
 *
 * What the rule actually sees is `defineDirective`, whose three alternatives
 * ARE the three cases the throw discriminated (`DEFINE_FLAG`,
 * `DEFINE_WITH_VALUE`, `DEFINE_FUNCTION`), reached from `program`'s directive
 * children. There is nowhere else in a `.cnx` file a `#define` can parse: after
 * a declaration, inside a `scope`, or inside a function body it is a syntax
 * error, so this walk is exhaustive by construction rather than by search.
 *
 * A `#define` in an included `.cnx` is still rejected, because that file is
 * itself a pipeline file whose own tree is analyzed -- and it is attributed to
 * the included file, as it was before.
 */
 
import { ParseTreeWalker } from "antlr4ng";
 
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import invariant from "../../utils/invariant";
import ParserUtils from "../../utils/ParserUtils";
import IDefineDirectiveError from "./types/IDefineDirectiveError";
 
/**
 * The macro name a `#define` declares.
 *
 * All three `defineDirective` tokens require `[a-zA-Z_][a-zA-Z0-9_]*` after
 * `define` and its whitespace, so a node that exists has a name by
 * construction and the miss is an invariant, not a case. Codegen answered
 * `"unknown"` here and put it in the user's message.
 */
const nameOf = (text: string): string => {
  const match = /#\s*define\s+([a-zA-Z_]\w*)/.exec(text);
  invariant(match !== null, "a defineDirective token always carries a name");
  return match[1];
};
 
class DefineDirectiveListener extends CNextListener {
  private readonly found: IDefineDirectiveError[] = [];
 
  public errors(): IDefineDirectiveError[] {
    return this.found;
  }
 
  /**
   * ADR-037 allows exactly one `#define` form -- a flag with no value, which
   * passes through to the generated C. The other two alternatives are these
   * two diagnostics, so the grammar rule is covered by naming the rejected
   * forms and nothing else.
   *
   * There is deliberately no `if (ctx.DEFINE_FLAG()) return;` early exit above
   * these. The first draft had one, described as the negative control, and
   * mutation-checking it reddened NOTHING: a flag is protected by the two
   * predicates below being false, never by that line. An arm that reads as a
   * rule and enforces nothing is worse than no arm, because the fixture beside
   * it then reads as covering something it does not.
   */
  override enterDefineDirective = (
    ctx: Parser.DefineDirectiveContext,
  ): void => {
    if (ctx.DEFINE_FUNCTION()) {
      const name = nameOf(ctx.getText());
      this.report(
        ctx,
        "E0501",
        `Function-like macro '${name}' is not allowed. Use inline functions instead.`,
        "A macro is textual substitution, so its arguments are evaluated wherever they appear and its operators bind by text (ADR-037); write an `inline` function instead.",
      );
      return;
    }
 
    if (ctx.DEFINE_WITH_VALUE()) {
      const name = nameOf(ctx.getText());
      this.report(
        ctx,
        "E0502",
        `#define with value '${name}' is not allowed. Use 'const' instead: const u32 ${name} <- value;`,
        "A `#define` has no type and no scope, so the compiler cannot check its use (ADR-037); a `const` has both.",
      );
    }
  };
 
  private report(
    at: Parser.DefineDirectiveContext,
    code: string,
    message: string,
    helpText: string,
  ): void {
    const { line, column } = ParserUtils.getPosition(at);
    this.found.push({ code, line, column, message, helpText });
  }
}
 
class DefineDirectiveAnalyzer {
  public analyze(tree: Parser.ProgramContext): IDefineDirectiveError[] {
    const listener = new DefineDirectiveListener();
    ParseTreeWalker.DEFAULT.walk(listener, tree);
    return listener.errors();
  }
}
 
export default DefineDirectiveAnalyzer;