All files / transpiler/logic/analysis ArrayIndexTypeAnalyzer.ts

90.51% Statements 124/137
77.77% Branches 56/72
100% Functions 26/26
96.69% Lines 117/121

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 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358                                                          192x     192x             233x 233x     185x     185x     44x 44x     4x 4x                           192x 192x 192x           102x 102x   18x 18x 21x             58x     58x   49x 49x 50x                 71x   71x 75x 75x   44x 18x 18x 18x     26x       3x 3x 3x     23x 19x       4x 3x       1x 1x 1x                     72x 72x   72x 72x         72x   72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 78x                         76x 76x   76x 76x     76x     76x       76x 5x 5x 5x       76x 16x 16x     76x                   76x 76x 29x   28x       47x 47x 1x 1x 1x 1x           46x 46x   46x     46x 46x     5x 5x   5x                     16x 5x 5x 5x     5x     3x 3x       11x     8x     8x 1x       7x 7x 7x             3x 3x 3x                     192x           192x     192x 192x 192x     192x 192x   192x                       22x                             1x          
/**
 * Array Index Type Analyzer
 * Detects signed and floating-point types used as array or bit subscript indexes
 *
 * C-Next requires unsigned integer types for all subscript operations to prevent
 * undefined behavior from negative indexes. This analyzer catches type violations
 * at compile time with clear error messages.
 *
 * Two-pass analysis:
 * 1. Collect variable declarations with their types
 * 2. Validate subscript expressions use unsigned integer types
 *
 * Uses CodeGenState for state-based type resolution (struct fields, function
 * return types, enum detection) to handle complex expressions like arr[x + 1].
 */
 
import { ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../parser/grammar/CNextListener";
import * as Parser from "../parser/grammar/CNextParser";
import IArrayIndexTypeError from "./types/IArrayIndexTypeError";
import LiteralUtils from "../../../utils/LiteralUtils";
import ParserUtils from "../../../utils/ParserUtils";
import TypeConstants from "../../../utils/constants/TypeConstants";
import CodeGenState from "../../state/CodeGenState";
 
/**
 * First pass: Collect variable declarations with their types
 */
class VariableTypeCollector extends CNextListener {
  private readonly varTypes: Map<string, string> = new Map();
 
  public getVarTypes(): Map<string, string> {
    return this.varTypes;
  }
 
  private trackType(
    typeCtx: Parser.TypeContext | null,
    identifier: { getText(): string } | null,
  ): void {
    Iif (!typeCtx || !identifier) return;
    this.varTypes.set(identifier.getText(), typeCtx.getText());
  }
 
  override enterVariableDeclaration = (
    ctx: Parser.VariableDeclarationContext,
  ): void => {
    this.trackType(ctx.type(), ctx.IDENTIFIER());
  };
 
  override enterParameter = (ctx: Parser.ParameterContext): void => {
    this.trackType(ctx.type(), ctx.IDENTIFIER());
  };
 
  override enterForVarDecl = (ctx: Parser.ForVarDeclContext): void => {
    this.trackType(ctx.type(), ctx.IDENTIFIER());
  };
}
 
/**
 * Second pass: Validate subscript index expressions use unsigned integer types
 */
class IndexTypeListener extends CNextListener {
  private readonly analyzer: ArrayIndexTypeAnalyzer;
 
  // eslint-disable-next-line @typescript-eslint/lines-between-class-members
  private readonly varTypes: Map<string, string>;
 
  constructor(analyzer: ArrayIndexTypeAnalyzer, varTypes: Map<string, string>) {
    super();
    this.analyzer = analyzer;
    this.varTypes = varTypes;
  }
 
  /**
   * Check postfix operations in expressions (RHS: arr[idx], flags[bit])
   */
  override enterPostfixOp = (ctx: Parser.PostfixOpContext): void => {
    if (!ctx.LBRACKET()) return;
 
    const expressions = ctx.expression();
    for (const expr of expressions) {
      this.validateIndexExpression(expr);
    }
  };
 
  /**
   * Check postfix target operations in assignments (LHS: arr[idx] <- val)
   */
  override enterPostfixTargetOp = (
    ctx: Parser.PostfixTargetOpContext,
  ): void => {
    if (!ctx.LBRACKET()) return;
 
    const expressions = ctx.expression();
    for (const expr of expressions) {
      this.validateIndexExpression(expr);
    }
  };
 
  /**
   * Validate that a subscript index expression uses an unsigned integer type.
   * Collects all leaf operands from the expression and checks each one.
   */
  private validateIndexExpression(ctx: Parser.ExpressionContext): void {
    const operands = this.collectOperands(ctx);
 
    for (const operand of operands) {
      const resolvedType = this.resolveOperandType(operand);
      if (!resolvedType) continue;
 
      if (TypeConstants.SIGNED_TYPES.includes(resolvedType)) {
        const { line, column } = ParserUtils.getPosition(ctx);
        this.analyzer.addError(line, column, "E0850", resolvedType);
        return;
      }
 
      if (
        resolvedType === "float literal" ||
        TypeConstants.FLOAT_TYPES.includes(resolvedType)
      ) {
        const { line, column } = ParserUtils.getPosition(ctx);
        this.analyzer.addError(line, column, "E0851", resolvedType);
        return;
      }
 
      if (TypeConstants.UNSIGNED_INDEX_TYPES.includes(resolvedType)) {
        continue;
      }
 
      // Enum types are valid indices (ADR-054: transpile to unsigned constants)
      if (CodeGenState.isKnownEnum(resolvedType)) {
        continue;
      }
 
      // Other non-integer types (e.g., string, struct) - E0852
      const { line, column } = ParserUtils.getPosition(ctx);
      this.analyzer.addError(line, column, "E0852", resolvedType);
      return;
    }
  }
 
  /**
   * Collect all leaf unary expression operands from an expression tree.
   * Handles binary operators at any level by flatMapping through the grammar hierarchy.
   */
  private collectOperands(
    ctx: Parser.ExpressionContext,
  ): Parser.UnaryExpressionContext[] {
    const ternary = ctx.ternaryExpression();
    Iif (!ternary) return [];
 
    const orExpressions = ternary.orExpression();
    Iif (orExpressions.length === 0) return [];
 
    // For ternary (cond ? true : false), skip the condition (index 0)
    // and only check the value branches (indices 1 and 2)
    const valueExpressions =
      orExpressions.length === 3 ? orExpressions.slice(1) : orExpressions;
 
    return valueExpressions
      .flatMap((o) => o.andExpression())
      .flatMap((a) => a.equalityExpression())
      .flatMap((e) => e.relationalExpression())
      .flatMap((r) => r.bitwiseOrExpression())
      .flatMap((bo) => bo.bitwiseXorExpression())
      .flatMap((bx) => bx.bitwiseAndExpression())
      .flatMap((ba) => ba.shiftExpression())
      .flatMap((s) => s.additiveExpression())
      .flatMap((a) => a.multiplicativeExpression())
      .flatMap((m) => m.unaryExpression());
  }
 
  /**
   * Resolve the type of a unary expression operand.
   * Uses local varTypes first, then falls back to CodeGenState for
   * struct fields, function return types, and enum detection.
   *
   * Returns null if the type cannot be resolved (pass-through).
   */
  private resolveOperandType(
    operand: Parser.UnaryExpressionContext,
  ): string | null {
    const postfixExpr = operand.postfixExpression();
    Iif (!postfixExpr) return null;
 
    const primaryExpr = postfixExpr.primaryExpression();
    Iif (!primaryExpr) return null;
 
    // Resolve base type from primaryExpression
    let currentType = this.resolveBaseType(primaryExpr);
 
    // Walk postfix operators to transform the type
    const postfixOps = postfixExpr.postfixOp();
 
    // If base type is null but there are postfix ops, use identifier name
    // for function call / member access resolution (e.g., getIndex())
    if (!currentType && postfixOps.length > 0) {
      const identifier = primaryExpr.IDENTIFIER();
      Eif (identifier) {
        currentType = identifier.getText();
      }
    }
 
    for (const op of postfixOps) {
      Iif (!currentType) return null;
      currentType = this.resolvePostfixOpType(currentType, op);
    }
 
    return currentType;
  }
 
  /**
   * Resolve the base type of a primary expression.
   */
  private resolveBaseType(
    primaryExpr: Parser.PrimaryExpressionContext,
  ): string | null {
    // Check for literal
    const literal = primaryExpr.literal();
    if (literal) {
      if (LiteralUtils.isFloat(literal)) return "float literal";
      // Integer literals are always valid
      return null;
    }
 
    // Check for parenthesized expression — recurse
    const parenExpr = primaryExpr.expression();
    if (parenExpr) {
      const innerOperands = this.collectOperands(parenExpr);
      for (const innerOp of innerOperands) {
        const innerType = this.resolveOperandType(innerOp);
        Eif (innerType) return innerType;
      }
      return null;
    }
 
    // Check for identifier
    const identifier = primaryExpr.IDENTIFIER();
    Iif (!identifier) return null;
 
    const varName = identifier.getText();
 
    // Local variables first (params, for-loop vars, function body vars)
    const localType = this.varTypes.get(varName);
    if (localType) return localType;
 
    // Fall back to CodeGenState for cross-file variables
    const typeInfo = CodeGenState.getVariableTypeInfo(varName);
    Iif (typeInfo) return typeInfo.baseType;
 
    return null;
  }
 
  /**
   * Resolve the resulting type after applying a postfix operator.
   */
  private resolvePostfixOpType(
    currentType: string,
    op: Parser.PostfixOpContext,
  ): string | null {
    // Dot access (e.g., config.value, EColor.RED)
    if (op.DOT()) {
      const fieldId = op.IDENTIFIER();
      Iif (!fieldId) return null;
      const fieldName = fieldId.getText();
 
      // Check if it's an enum access — always valid
      if (CodeGenState.isKnownEnum(currentType)) return null;
 
      // Check struct field type
      const fieldType = CodeGenState.getStructFieldType(currentType, fieldName);
      return fieldType ?? null;
    }
 
    // Array/bit subscript (e.g., lookup[idx])
    if (op.LBRACKET()) {
      // If current type is an array, result is the element type
      // If current type is an integer, result is "bool" (bit access)
      Iif (TypeConstants.UNSIGNED_INDEX_TYPES.includes(currentType)) {
        return "bool";
      }
      if (TypeConstants.SIGNED_TYPES.includes(currentType)) {
        return "bool";
      }
      // Array element type — strip rightmost array dimension
      // e.g., "u8[8]" → "u8", "u8[8][4]" → "u8[8]", "u8[CONST]" → "u8"
      const strippedType = currentType.replace(/\[[^\]]*\]$/, "");
      Eif (strippedType !== currentType) {
        return strippedType;
      }
      // Not an array type (e.g., struct), return as-is
      return currentType;
    }
 
    // Function call (e.g., getIndex())
    Eif (op.LPAREN()) {
      const returnType = CodeGenState.getFunctionReturnType(currentType);
      return returnType ?? null;
    }
 
    return null;
  }
}
 
/**
 * Analyzer that detects non-unsigned-integer types used as subscript indexes
 */
class ArrayIndexTypeAnalyzer {
  private errors: IArrayIndexTypeError[] = [];
 
  /**
   * Analyze the parse tree for invalid subscript index types
   */
  public analyze(tree: Parser.ProgramContext): IArrayIndexTypeError[] {
    this.errors = [];
 
    // First pass: collect variable types
    const collector = new VariableTypeCollector();
    ParseTreeWalker.DEFAULT.walk(collector, tree);
    const varTypes = collector.getVarTypes();
 
    // Second pass: validate subscript index expressions
    const listener = new IndexTypeListener(this, varTypes);
    ParseTreeWalker.DEFAULT.walk(listener, tree);
 
    return this.errors;
  }
 
  /**
   * Add an index type error
   */
  public addError(
    line: number,
    column: number,
    code: string,
    actualType: string,
  ): void {
    this.errors.push({
      code,
      line,
      column,
      actualType,
      message: `Subscript index must be an unsigned integer type; got '${actualType}'`,
      helpText:
        "Use an unsigned integer type (u8, u16, u32, u64) for array and bit subscript indexes.",
    });
  }
 
  /**
   * Get all detected errors
   */
  public getErrors(): IArrayIndexTypeError[] {
    return this.errors;
  }
}
 
export default ArrayIndexTypeAnalyzer;