All files / TRANSPILE/2-Plan SubscriptDepthValidator.ts

100% Statements 15/15
100% Branches 14/14
100% Functions 3/3
100% Lines 14/14

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                                                                                702x                                                 1273x 1273x 702x 551x   151x   1273x                                       1371x 497x       874x   1371x 244x     630x         1371x                
/**
 * 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 invariant from "../../utils/invariant";
 
/**
 * A planned postfix operation, which STATES its kind.
 *
 * #1445 review: there were two shapes here -- this one and an
 * `IPostfixOpLike { expression(): unknown[] }` for raw nodes -- chosen between
 * by a `"kind" in op` probe. In the one module whose stated purpose is that
 * the read and write paths "cannot diverge on what counts as a subscript",
 * that was the question being answered from two representations. Both callers
 * pass planned ops now.
 *
 * The kind is the literal union rather than `string`, so a planner emitting a
 * fourth kind is a compile error here instead of silently counting as
 * not-a-subscript.
 */
interface IPlannedOpLike {
  readonly kind: "member" | "subscript" | "call";
}
 
function isSubscript(op: IPlannedOpLike): boolean {
  return op.kind === "subscript";
}
 
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 IPlannedOpLike[],
    startIndex = 0,
  ): number {
    let count = 0;
    for (let index = startIndex; index < ops.length; index++) {
      if (!isSubscript(ops[index])) {
        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.
   *
   * #1322: the `line` parameter went with the throw. It existed only to be
   * spelled into the message, which is what a diagnostic's position is for.
   */
  static validate(
    typeInfo: TTypeInfo | undefined,
    subscriptOpCount: number,
    varName: string,
  ): 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;
    // #1322: ADR-036/ADR-007's depth limit is E0856 in pass 2.1, decided from
    // the declaration's own dimensions. The throw here built `Error at line N:`
    // into its message -- a position carried as prose, which is what the
    // relocation removes.
    invariant(
      subscriptOpCount <= arrayDimensions + 1,
      `'${varName}' is subscripted no deeper than its shape allows -- E0856 rejects this in pass 2.1, before this runs`,
    );
  }
}
 
export default SubscriptDepthValidator;