All files / transpiler/logic/analysis DivisionByZeroAnalyzer.ts

93.44% Statements 57/61
88.88% Branches 32/36
100% Functions 8/8
94.91% Lines 56/59

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                                                            328x     328x             410x       410x 371x     39x 39x       39x 39x 39x         39x 39x 7x                           328x 328x 328x             1328x       1328x 1328x 1279x       49x 50x 50x   50x 50x 14x     36x 36x     36x 22x                   36x 36x 1x       35x 35x         35x 35x 22x                 13x 13x 12x 12x         1x               328x                 328x     328x 328x 328x     328x 328x   328x             22x 22x 22x   22x                               1x          
/**
 * Division By Zero Analyzer
 * Detects division and modulo by zero at compile time (ADR-051)
 *
 * Implemented Phases:
 * ✓ Phase 1: Literal zero detection (10 / 0, 10 % 0)
 * ✓ Phase 3: Const zero detection (const u32 ZERO <- 0; x / ZERO)
 *
 * Future Enhancement (Phase 3+):
 * - Const expression evaluation (const u32 VALUE <- 5 - 5; x / VALUE)
 *
 * Issue #1220: the const-zero pass walks only THIS file's parse tree, so a
 * const arriving through an #include was invisible and the check silently
 * passed. Cross-file consts are resolved through CodeGenState, which reads the
 * one const-value derivation on SymbolTable.
 */
 
import { ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../parser/grammar/CNextListener";
import * as Parser from "../parser/grammar/CNextParser";
import IDivisionByZeroError from "./types/IDivisionByZeroError";
import LiteralUtils from "../../../utils/LiteralUtils";
import ExpressionUtils from "../../../utils/ExpressionUtils";
import ParserUtils from "../../../utils/ParserUtils";
import CodeGenState from "../../state/CodeGenState";
 
/**
 * First pass: Collect const declarations that are zero
 */
class ConstZeroCollector extends CNextListener {
  private readonly constZeros: Set<string> = new Set();
 
  public getConstZeros(): Set<string> {
    return this.constZeros;
  }
 
  /**
   * Track const declarations
   * variableDeclaration: atomicModifier? volatileModifier? constModifier? ...
   */
  override enterVariableDeclaration = (
    ctx: Parser.VariableDeclarationContext,
  ): void => {
    // Only process const declarations
    if (!ctx.constModifier()) {
      return;
    }
 
    const identifier = ctx.IDENTIFIER();
    Iif (!identifier) {
      return;
    }
 
    const name = identifier.getText();
    const expr = ctx.expression();
    Iif (!expr) {
      return;
    }
 
    // Check if the expression is a literal zero
    const literal = ExpressionUtils.extractLiteral(expr);
    if (literal && LiteralUtils.isZero(literal)) {
      this.constZeros.add(name);
    }
  };
}
 
/**
 * Second pass: Detect division by zero (including const identifiers)
 */
class DivisionByZeroListener extends CNextListener {
  private readonly analyzer: DivisionByZeroAnalyzer;
  // eslint-disable-next-line @typescript-eslint/lines-between-class-members
  private readonly constZeros: Set<string>;
 
  constructor(analyzer: DivisionByZeroAnalyzer, constZeros: Set<string>) {
    super();
    this.analyzer = analyzer;
    this.constZeros = constZeros;
  }
 
  /**
   * Check multiplicative expressions for division/modulo by zero
   * multiplicativeExpression: unaryExpression (('*' | '/' | '%') unaryExpression)*
   */
  override enterMultiplicativeExpression = (
    ctx: Parser.MultiplicativeExpressionContext,
  ): void => {
    // Get all unary expressions
    const operands = ctx.unaryExpression();
    if (operands.length < 2) {
      return; // No operator, just a single operand
    }
 
    // Check each operator and its right operand
    for (let i = 0; i < operands.length - 1; i++) {
      const operatorToken = ctx.getChild(i * 2 + 1); // Operators are at odd indices
      Iif (!operatorToken) continue;
 
      const operator = operatorToken.getText();
      if (operator !== "/" && operator !== "%") {
        continue; // Only check division and modulo
      }
 
      const rightOperand = operands[i + 1];
      const { line, column } = ParserUtils.getPosition(rightOperand);
 
      // Check if right operand is zero
      if (this.isZero(rightOperand)) {
        this.analyzer.addError(operator, line, column);
      }
    }
  };
 
  /**
   * Check if a unary expression evaluates to zero
   */
  private isZero(ctx: Parser.UnaryExpressionContext): boolean {
    // Get the postfix expression
    const postfixExpr = ctx.postfixExpression();
    if (!postfixExpr) {
      return false;
    }
 
    // Get the primary expression
    const primaryExpr = postfixExpr.primaryExpression();
    Iif (!primaryExpr) {
      return false;
    }
 
    // Check if it's a literal
    const literal = primaryExpr.literal();
    if (literal) {
      return LiteralUtils.isZero(literal);
    }
 
    // Check if it's a const identifier that evaluates to zero. The local pass
    // covers consts declared in this file (including function-local ones, which
    // never reach the symbol table); CodeGenState covers the rest.
    //
    // Issue #1220: without the second half, `10 / ZERO` with an imported ZERO
    // passed silently and emitted a real runtime division by zero.
    const identifier = primaryExpr.IDENTIFIER();
    if (identifier) {
      const name = identifier.getText();
      return (
        this.constZeros.has(name) || CodeGenState.getCNextConstValue(name) === 0
      );
    }
 
    return false;
  }
}
 
/**
 * Analyzer that detects division by zero
 */
class DivisionByZeroAnalyzer {
  private errors: IDivisionByZeroError[] = [];
 
  /**
   * Analyze the parse tree for division by zero
   * Two-pass analysis:
   * 1. Collect const declarations that are zero
   * 2. Detect division/modulo by literal zero or const zero
   */
  public analyze(tree: Parser.ProgramContext): IDivisionByZeroError[] {
    this.errors = [];
 
    // First pass: collect const zeros
    const collector = new ConstZeroCollector();
    ParseTreeWalker.DEFAULT.walk(collector, tree);
    const constZeros = collector.getConstZeros();
 
    // Second pass: detect division by zero
    const listener = new DivisionByZeroListener(this, constZeros);
    ParseTreeWalker.DEFAULT.walk(listener, tree);
 
    return this.errors;
  }
 
  /**
   * Add a division by zero error
   */
  public addError(operator: string, line: number, column: number): void {
    const isDivision = operator === "/";
    const code = isDivision ? "E0800" : "E0802";
    const opName = isDivision ? "Division" : "Modulo";
 
    this.errors.push({
      code,
      operator,
      line,
      column,
      message: `${opName} by zero`,
      helpText: isDivision
        ? "Consider using safe_div(output, numerator, divisor, defaultValue) for runtime safety"
        : "Consider using safe_mod(output, numerator, divisor, defaultValue) for runtime safety",
    });
  }
 
  /**
   * Get all detected errors
   */
  public getErrors(): IDivisionByZeroError[] {
    return this.errors;
  }
}
 
export default DivisionByZeroAnalyzer;