All files / TRANSPILE/1-Analyze DynamicAllocation.ts

100% Statements 11/11
100% Branches 4/4
100% Functions 4/4
100% Lines 9/9

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                                                                                                                            47x                                                         141x 120x 1611x   114x         1x                         47x       8x         8x          
/**
 * Dynamic memory functions C-Next does not admit from C or C++.
 *
 * ADR-003 allows static allocation only. The prohibition is not "C-Next has a
 * `malloc` and forbids it" -- C-Next has no such function at all. What is
 * rejected is IMPORTING one from a C or C++ header into a `.cnx` file, and the
 * diagnostic says so. Code that genuinely needs the heap belongs in C or C++.
 *
 * ## One list, one rule
 *
 * This decision used to be made in two places with different matching, and the
 * two disagreed on real programs (found while regenerating snapshots for #1306):
 *
 *   - a bare `heap_caps_malloc(x);` compiled, while `y <- heap_caps_malloc(x);`
 *     was rejected -- the same call, two answers;
 *   - `u32 y <- myfree(1);` was rejected as `free`, because one path scanned
 *     statement TEXT for `"free("` rather than looking at the callee;
 *   - a declaration reported the same call twice, once from each path.
 *
 * Everything now asks `matches()`, so changing the rule means editing one place.
 *
 * ## Why the suffix rule
 *
 * A name matches when it IS one of these, or ends with `_` followed by one.
 * `k_malloc`/`k_free` (Zephyr) and `heap_caps_malloc` (ESP-IDF) are real
 * allocators spelled that way, and ADR-003 extends the prohibition to "any
 * user-defined equivalents", so a deliberate `my_free` wrapper is caught by
 * design. The separator is what keeps `myfree`, `saferealloc` and
 * `free_list_init` legal: ordinary names that happen to contain these letters.
 *
 * ## What a name rule cannot reach, stated plainly
 *
 * It matches snake_case suffixes and nothing else. FreeRTOS spells its
 * allocators `pvPortMalloc` / `vPortFree`, CMSIS-RTOS `osMemoryPoolAlloc`,
 * ThreadX `tx_byte_allocate`, and newlib's reentrant form is the PREFIXED
 * `_malloc_r`. None of them match, and no widening of a name rule would catch
 * them without also rejecting ordinary code. This is a cheap check for the
 * common spelling, not a guarantee (#1306 review -- an earlier version of this
 * comment claimed vendor allocators are "overwhelmingly" spelled with the
 * separator and offered `pvPort_malloc`, a name that does not exist).
 *
 * It also cannot tell releasing MEMORY from releasing a device: ESP-IDF's
 * `spi_bus_free` matches and is rejected. That follows from the maintainer's
 * rule -- exact, or after an underscore -- and the message stays true, because
 * such a function genuinely is imported from C or C++. An author who needs it
 * calls it from their C or C++ code, which is what ADR-003 asks for anyway.
 *
 * `getline`/`getdelim` are deliberately absent. They reallocate, but are
 * routinely called with a caller-owned buffer, so listing them would reject
 * correct code.
 *
 * ## Where the decision is made -- deliberately not here
 *
 * This class owns the LIST and the MATCH RULE. It does not own the consequence:
 * a match only means E0902 when the callee did not resolve to a C-Next
 * definition, and that is known in exactly one place, the call analyzer's
 * resolution ladder. Asking `matches()` from anywhere else re-derives the
 * consequence from a name alone -- which is how `pool_free` and `slot_is_free`,
 * both written in C-Next three lines above their call, came to be told they had
 * been imported from a header (#1306 review). Sharing the predicate made the two
 * sites agree on the letters and left them disagreeing on the meaning.
 */
const DYNAMIC_MEMORY_FUNCTIONS: ReadonlySet<string> = new Set([
  // C standard
  "malloc",
  "calloc",
  "realloc",
  "free",
  "aligned_alloc",
  // POSIX and common extensions
  "reallocarray",
  "posix_memalign",
  "memalign",
  "valloc",
  "pvalloc",
  // Allocating string/format helpers -- the caller owns the result and must
  // free it, which is the same ownership problem by another name.
  "strdup",
  "strndup",
  "asprintf",
  "vasprintf",
]);
 
class DynamicAllocation {
  /**
   * Whether calling `name` means allocating or releasing heap memory.
   *
   * Exact match, or the name ends with `_` plus an exact match. See the header
   * comment for why the separator is load-bearing.
   */
  static matches(name: string): boolean {
    if (DYNAMIC_MEMORY_FUNCTIONS.has(name)) return true;
    for (const forbidden of DYNAMIC_MEMORY_FUNCTIONS) {
      if (name.endsWith(`_${forbidden}`)) return true;
    }
    return false;
  }
 
  /** The listed names, for tests and for anything that must enumerate them. */
  static names(): readonly string[] {
    return [...DYNAMIC_MEMORY_FUNCTIONS];
  }
 
  /**
   * The one diagnostic for this condition, reported from one place.
   *
   * It used to be two, split by whether the header was included -- with
   * `#include <stdlib.h>` the call resolved and NULL-check analysis reported it,
   * without it call analysis got there first. The author's mistake does not
   * change because a header happened to be present, and neither does the
   * diagnostic; what the split actually bought was a second site with no idea
   * whether the callee was C-Next code.
   */
  static readonly CODE = "E0902";
 
  /** What the user did wrong. The IMPORT is forbidden, not a C-Next function. */
  static message(name: string): string {
    return `Importing dynamic memory function '${name}' from C/C++ is forbidden`;
  }
 
  /** What to do instead. */
  static helpText(name: string): string {
    return `C-Next has no dynamic memory allocation (ADR-003). Keep '${name}' in your C or C++ code; a .cnx file cannot call it.`;
  }
}
 
export default DynamicAllocation;