All files / transpiler/output/codegen/subscript SubscriptDepthValidator.ts

100% Statements 16/16
100% Branches 16/16
100% Functions 2/2
100% Lines 15/15

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                                                                                                          1204x 1204x 694x 505x   189x   1204x                                       1208x 421x       787x   1208x 210x     577x 1208x 1208x 11x                      
/**
 * Issue #1106: shared validation for subscript-chain depth.
 *
 * Per ADR-036 each subscript peels one array dimension; per ADR-007 a scalar
 * integer/float may be bit-indexed once. So a base variable allows at most
 * `arrayDimensions + 1` subscript operations. A deeper chain indexes a value
 * that is not an array — e.g. `flags[4][3]` on a scalar `u8`, where `flags[4]`
 * is already a single bit.
 *
 * This is the single source of truth for the "how many subscripts may this
 * base take?" decision. Both the assignment (write) path
 * (AssignmentClassifier) and the expression (read) path
 * (PostfixExpressionGenerator) consult it, so the two paths cannot diverge.
 * It owns the *counting* as well as the check (`countLeadingSubscripts`):
 * sharing only the check while each path re-derived the count would leave the
 * two agreeing by coincidence rather than by construction.
 */
import TTypeInfo from "../../../types/TTypeInfo";
import TypeCheckUtils from "../../../../utils/TypeCheckUtils";
import CodeGenErrors from "../helpers/CodeGenErrors";
 
/**
 * Structural shape common to `postfixOp` (read path) and `postfixTargetOp`
 * (write path). Both expose their bracket contents via `expression()`: a
 * subscript has 1 (`[i]`) or 2 (`[start, width]`), while a member access or a
 * call has none.
 */
interface IPostfixOpLike {
  expression(): unknown[];
}
 
class SubscriptDepthValidator {
  /**
   * Count the leading run of subscript operations applied directly to a base,
   * starting at `startIndex` and stopping at the first operation that changes
   * the type (member access or call) — after that, subsequent subscripts apply
   * to a different value, not to this base.
   *
   * Counts OPERATIONS, not expressions: the bit range `flags[4, 3]` is one
   * operation with two expressions, whereas the chain `flags[4][3]` is two
   * operations with one each. Conflating them would reject valid bit ranges.
   *
   * `startIndex` skips operations already consumed in identifying the base —
   * on the read path `this.flags[4][3]` parses as `this` plus the ops
   * `.flags`, `[4]`, `[3]`, so the member op is skipped with `startIndex: 1`.
   * The write path needs no offset: `assignmentTarget` consumes the `this .
   * IDENTIFIER` prefix in the grammar rule itself, so its ops are subscripts
   * from index 0.
   */
  static countLeadingSubscripts(
    ops: readonly IPostfixOpLike[],
    startIndex = 0,
  ): number {
    let count = 0;
    for (let index = startIndex; index < ops.length; index++) {
      if (ops[index].expression().length === 0) {
        break;
      }
      count++;
    }
    return count;
  }
 
  /**
   * Validate that a leading run of `subscriptOpCount` subscript operations
   * applied directly to `varName` (declared type `typeInfo`) is within range.
   *
   * Only integer/float bases are checked: those are the bit-indexable scalar
   * element types (ADR-007). Strings are char arrays with their own semantics,
   * bitmaps reject bracket indexing elsewhere, and struct/other bases are
   * handled by the member-access paths — none are validated here.
   *
   * @throws when the chain is deeper than `arrayDimensions + 1`.
   */
  static validate(
    typeInfo: TTypeInfo | undefined,
    subscriptOpCount: number,
    varName: string,
    line: number,
  ): void {
    if (!typeInfo || typeInfo.isString || typeInfo.isBitmap) {
      return;
    }
 
    const isBitIndexable =
      TypeCheckUtils.isInteger(typeInfo.baseType) ||
      TypeCheckUtils.isFloat(typeInfo.baseType);
    if (!isBitIndexable) {
      return;
    }
 
    const arrayDimensions = typeInfo.arrayDimensions?.length ?? 0;
    const maxDepth = arrayDimensions + 1;
    if (subscriptOpCount > maxDepth) {
      throw CodeGenErrors.tooManySubscripts(
        line,
        varName,
        typeInfo.baseType,
        arrayDimensions,
      );
    }
  }
}
 
export default SubscriptDepthValidator;