All files / TRANSPILE/3-Render/codegen/helpers StringOperationsHelper.ts

100% Statements 17/17
100% Branches 18/18
100% Functions 3/3
100% Lines 16/16

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                                                                                                          89x 53x       36x 25x 25x 20x       16x                                   9x       9x         9x 3x     6x                                                                             8x       8x   5x 5x                    
/**
 * StringOperationsHelper - what ADR-045's string operations render as.
 *
 * Extracted from CodeGenerator to reduce file size.
 *
 * ADR-045: String type support
 *
 * ## It asks a declaration, not the tree (#1445)
 *
 * Every question here is answered from a NAME and the declared type it binds
 * to where the expression is (#1668, C7: the caller binds it through the
 * program): how long is this literal, how wide is this declared string. The
 * tree navigation that used to sit in front of those questions --
 * "is this expression a two-operand `+`", "is it an identifier with one
 * subscript" -- moved to `ExpressionUnwrapper`, which is what that utility is
 * for and which keeps both shapes unit-tested.
 *
 * That split is also an ORDERING, not just a relocation. Generating an index
 * expression can allocate a C++ temp and queue its declaration, so the caller
 * asks `getStringExprCapacity` -- which is what makes `s[i]` a substring
 * rather than an array index -- BEFORE it generates anything it might discard.
 * A helper that took the generated code and then decided would have had the
 * decision arrive too late to be free.
 */
 
import ISubstringOps from "../types/ISubstringOps";
import IStringConcatOps from "../types/IStringConcatOps";
import StringUtils from "../../../../utils/StringUtils";
import BareIdentifier from "../../../../utils/BareIdentifier";
import type TTypeInfo from "../../../../types/TTypeInfo";
 
/**
 * Helper for string operation rendering.
 * All methods are static - uses CodeGenState for shared state.
 */
class StringOperationsHelper {
  /**
   * Get the capacity of a string expression.
   * For string literals, capacity equals content length.
   * For a string variable, capacity is its declaration's.
   *
   * ADR-045: String capacity resolution for concatenation and bounds checking.
   *
   * @param exprCode - Expression code text (e.g., "hello" or varName)
   * @param declaredType - #1668 (C7): a bare name's declared type where the
   *   expression is, bound by the caller that holds the node
   * @returns Capacity in characters, or null if not a string
   */
  static getStringExprCapacity(
    exprCode: string,
    declaredType: (name: string) => TTypeInfo | undefined,
  ): number | null {
    // String literal - capacity equals content length
    if (exprCode.startsWith('"') && exprCode.endsWith('"')) {
      return StringUtils.literalLength(exprCode);
    }
 
    // A string variable, by its declaration
    if (BareIdentifier.matches(exprCode)) {
      const typeInfo = declaredType(exprCode);
      if (typeInfo?.isString && typeInfo.stringCapacity !== undefined) {
        return typeInfo.stringCapacity;
      }
    }
 
    return null;
  }
 
  /**
   * The operands of a string concatenation, given the two addition operands.
   *
   * ADR-045: concatenation detection for strncpy/strncat generation. Null
   * unless BOTH operands are strings -- `str + 5` is not a concatenation, and
   * neither is `1 + 2`.
   *
   * @param leftText - source text of the left operand
   * @param rightText - source text of the right operand
   */
  static getStringConcatOperands(
    leftText: string,
    rightText: string,
    declaredType: (name: string) => TTypeInfo | undefined,
  ): IStringConcatOps | null {
    const leftCapacity = StringOperationsHelper.getStringExprCapacity(
      leftText,
      declaredType,
    );
    const rightCapacity = StringOperationsHelper.getStringExprCapacity(
      rightText,
      declaredType,
    );
 
    if (leftCapacity === null || rightCapacity === null) {
      return null;
    }
 
    return {
      left: leftText,
      right: rightText,
      leftCapacity,
      rightCapacity,
    };
  }
 
  /**
   * The operands of a substring extraction, or null when `sourceName` is not a
   * string -- which is what makes `s[i]` a substring rather than an array
   * index.
   *
   * ADR-045: safe string slicing. Issue #140: `source[i]` is sugar for
   * `source[i, 1]`, which is the whole difference between the one-index and
   * two-index forms -- the grammar admits no other arity.
   *
   * ## The indexes arrive as a thunk, and that is the point
   *
   * Generating an expression is not free, and it cannot be taken back: it
   * can queue a pending temp declaration into the enclosing block --
   * `ArgumentGenerator.createCppMemberConversionTemp` for a C++ member
   * conversion, and the float bit-range shadow union in the postfix generator
   * both do. So an index generated for an expression that turns out NOT to be
   * a substring leaks a declaration for a value nothing reads.
   *
   * Taking generated strings and deciding afterwards would put that decision
   * one step too late, and taking the capacity as a parameter would move the
   * decision to the caller. Taking a thunk keeps the decision here and makes
   * the order impossible to get wrong at a call site. It is asserted rather
   * than remembered: a unit test counts the invocations and fails if the
   * lookup stops coming first -- measured, because reordering it reddens 0 of
   * the 1254 integration fixtures.
   */
  static getSubstringOperands(
    sourceName: string,
    generateIndexes: () => readonly string[],
    declaredType: (name: string) => TTypeInfo | undefined,
  ): ISubstringOps | null {
    const sourceCapacity = StringOperationsHelper.getStringExprCapacity(
      sourceName,
      declaredType,
    );
    if (sourceCapacity === null) return null;
 
    const indexCodes = generateIndexes();
    return {
      source: sourceName,
      start: indexCodes[0],
      lengthExpression: indexCodes[1] ?? "1",
      sourceCapacity,
    };
  }
}
 
export default StringOperationsHelper;