All files / utils LiteralUtils.ts

100% Statements 47/47
97.5% Branches 39/40
100% Functions 11/11
100% Lines 42/42

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                              169x                           49x                       80x 80x     80x 14x                                 21x     21x 21x                   8x                               13519x 13519x 294x                         5224x   5224x   5093x 5093x 15x 15x 15x       5078x 5078x 116x         4962x 4885x     77x                                 223x     223x 133x       90x 28x       62x 20x     42x                       101x         96x 96x                     3137x                 43x 40x 40x 25x          
/**
 * Utility class for analyzing literal values in the parse tree.
 *
 * Extracted from DivisionByZeroAnalyzer and FloatModuloAnalyzer
 * to eliminate duplicate literal checking code.
 */
 
import * as Parser from "../PARSE/2-Parse/grammar/CNextParser";
 
/**
 * A float literal's shape as the grammar spells one: digits, an optional
 * fraction, an optional exponent and an optional width suffix, captured. The
 * grammar also requires a fraction or an exponent, which `[.eE]` asserts
 * beside it -- in the pattern, that requirement doubled every arm.
 */
const FLOAT_LITERAL = /^\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(?:[fF](32|64))?$/;
 
/**
 * Static utility methods for literal analysis
 */
class LiteralUtils {
  /**
   * Check if a literal represents zero, in any C-Next literal format
   * (decimal, hex, binary, float, each with or without a suffix).
   *
   * @param ctx - The literal context from the parse tree
   * @returns true if the literal is zero
   */
  static isZero(ctx: Parser.LiteralContext): boolean {
    return LiteralUtils.isZeroText(ctx.getText());
  }
 
  /**
   * Whether a numeric literal's text is zero, by its VALUE rather than its
   * spelling -- for a node's text, and for a const's initializer, which
   * arrives as text from a declaration that may be in another file (#1664
   * box 7). The spelling test this replaced missed `00`, `0x00`, `0x00u8`
   * and every suffixed float (`0.0f32`). Anything that is not a numeric
   * literal (a string, a char, `false`) is not zero.
   */
  static isZeroText(text: string): boolean {
    const trimmed = text.trim();
    const integer = LiteralUtils.parseIntegerLiteral(
      trimmed.replace(/[uUiI](?:8|16|32|64)$/, ""),
    );
    if (integer !== undefined) return integer === 0;
    return (
      LiteralUtils.floatLiteralWidth(trimmed) !== null &&
      LiteralUtils.isFloatZero(trimmed.replace(/[fF](?:32|64)$/, ""))
    );
  }
 
  /**
   * Check if a float literal string represents zero.
   * Issue #1010: Detect float zero for division-by-zero checking.
   *
   * Handles: 0.0, .0, 0., 0.0f, 0.0F, 0.0e0, 0.0E0, etc.
   *
   * @param text - The float literal text
   * @returns true if the float is zero
   */
  static isFloatZero(text: string): boolean {
    // Remove optional float suffix (f, F)
    const withoutSuffix = text.replace(/[fF]$/, "");
 
    // Parse to number and check if zero
    const value = Number.parseFloat(withoutSuffix);
    return value === 0;
  }
 
  /**
   * Check if a literal is a floating-point number.
   *
   * @param ctx - The literal context from the parse tree
   * @returns true if the literal is a float
   */
  static isFloat(ctx: Parser.LiteralContext): boolean {
    return LiteralUtils.floatLiteralWidth(ctx.getText()) !== null;
  }
 
  /**
   * The width of a floating literal's type, read from its text: 32 for
   * `2.5f32`, 64 for `2.5f64` and for an unsuffixed `2.5` (a C `double`).
   * Null when the text is not a floating literal.
   *
   * #1668: the one decision of whether a literal is floating. It used to be
   * made three ways, each by a partial test that some other literal also
   * passes. A trailing `f32` also ends the hex integer `0xFF32`, which the
   * render layer then emitted as `0xFf`. A `.` also occurs in the char literal
   * `'.'`, which E0804 then rejected as a floating modulo operand. So the text
   * must match the grammar's FLOAT_LITERAL / SUFFIXED_FLOAT shape as a whole.
   */
  static floatLiteralWidth(text: string): 32 | 64 | null {
    const match = FLOAT_LITERAL.exec(text);
    if (!match || !/[.eE]/.test(text)) return null;
    return match[1] === "32" ? 32 : 64;
  }
 
  /**
   * ADR-024: Get the type from a literal (suffixed or unsuffixed).
   *
   * #1668: moved here from 2.2's ExpressionTypeResolver so that 2.1 can type a
   * composite's literal operand with the same rule 2.2 uses. Composite typing is
   * one decision (`CompositeType`) that both layers read, and it treats a
   * floating operand as a veto, so both have to agree on which literal operands
   * are floating.
   */
  static typeOf(ctx: Parser.LiteralContext): string | null {
    const text = ctx.getText();
 
    if (text === "true" || text === "false") return "bool";
 
    const suffixMatch = /([uUiI])(8|16|32|64)$/.exec(text);
    if (suffixMatch) {
      const signChar = suffixMatch[1].toLowerCase();
      const width = suffixMatch[2];
      return (signChar === "u" ? "u" : "i") + width;
    }
 
    // A plain float literal (no suffix) has type double in C
    const floatWidth = LiteralUtils.floatLiteralWidth(text);
    if (floatWidth !== null) {
      return `f${floatWidth}`;
    }
 
    // Plain integer literals (no suffix) have type int in C
    // Check for integer: starts with digit, no decimal point
    if (/^\d+$/.test(text) || /^0[xXbBoO][\da-fA-F]+$/.test(text)) {
      return "int";
    }
 
    return null;
  }
 
  /**
   * Parse an integer literal string to a numeric value.
   *
   * Handles all C-Next integer formats:
   * - Decimal: 42, -17
   * - Hex: 0x2A, 0X2a
   * - Binary: 0b101010, 0B101010
   *
   * Issue #455: Used for resolving const values in array dimensions.
   *
   * @param text - The literal text to parse
   * @returns The numeric value, or undefined if not a valid integer literal
   */
  static parseIntegerLiteral(text: string): number | undefined {
    const trimmed = text.trim();
 
    // Decimal integer (including negative)
    if (/^-?\d+$/.test(trimmed)) {
      return Number.parseInt(trimmed, 10);
    }
 
    // Hex literal (0x or 0X prefix)
    if (/^0[xX][0-9a-fA-F]+$/.test(trimmed)) {
      return Number.parseInt(trimmed, 16);
    }
 
    // Binary literal (0b or 0B prefix)
    if (/^0[bB][01]+$/.test(trimmed)) {
      return Number.parseInt(trimmed.substring(2), 2);
    }
 
    return undefined;
  }
 
  /**
   * Whether a folded value is exact (#1760 review). A double holds every
   * integer up to 2^53 - 1 and rounds past it, so `9007199254740993` parses
   * to 2^53. A fold that computed with the rounded value folded
   * `BIG - 9007199254740992` to 0, a false E0800, where C computes 1. Every
   * fold asks this of each literal it reads and each value it computes, and
   * gives no value otherwise: an unknown is never reported against.
   */
  static isExactInteger(value: number | undefined): value is number {
    return value !== undefined && Number.isSafeInteger(value);
  }
 
  /** An integer literal's value for a fold, when that value is exact */
  static exactIntegerLiteral(text: string): number | undefined {
    const value = LiteralUtils.parseIntegerLiteral(text);
    return LiteralUtils.isExactInteger(value) ? value : undefined;
  }
 
  /**
   * ADR-044 "Integer Literals" (owner ruling, 2026-10-03, #1728): whether a
   * decimal literal, with or without its type suffix, has a leading zero.
   * C-Next has no octal literal, so 2.1 reports one (E0912), and no reading
   * gives it a value before then -- not the decimal one, which C, reading
   * octal, would disagree with. The one rule every reading asks.
   */
  static hasLeadingZero(decimalText: string): boolean {
    return /^0\d/.test(decimalText);
  }
 
  /**
   * The value of an integer literal as written in C-Next source, with any
   * width suffix (`9u8`, `3i32`): decimal, hex or binary. Null for anything
   * else, and for a leading-zero literal (`010`, E0912).
   */
  static integerValue(text: string): number | null {
    if (LiteralUtils.hasLeadingZero(text)) return null;
    const match = /^(0[xX][\da-fA-F]+|0[bB][01]+|\d+)([uUiI]\d+)?$/.exec(text);
    if (match === null) return null;
    return LiteralUtils.parseIntegerLiteral(match[1]) ?? null;
  }
}
 
export default LiteralUtils;