|
| 1 | +#!/usr/bin/env bash |
| 2 | +# scripts/check_repr_decode.sh — collection representation-encapsulation guard. |
| 3 | +# |
| 4 | +# Born from Discussion #12: `valueSetToForm` decoded a set's backing map as an |
| 5 | +# ArrayMap unconditionally, so a 9+-element set literal (promoted to a HashMap |
| 6 | +# backing) segfaulted macroexpansion. The same misread was then found live in |
| 7 | +# multimethod hierarchy dispatch (silently gone at the 9th defmethod), the |
| 8 | +# http client (9-entry :headers rejected) and the http server (>8 response |
| 9 | +# headers dropped). One bug, five sites, three symptom faces. |
| 10 | +# |
| 11 | +# The class: code OUTSIDE src/runtime/collection/ decoding a collection's |
| 12 | +# backing-representation struct directly. The representation (array vs HAMT, |
| 13 | +# promotion boundaries) is the collection module's private business; everyone |
| 14 | +# else iterates via the generic accessors (`map.forEachEntry`, |
| 15 | +# `set.forEachElem`, `count`, `get`, …), which handle every backing. |
| 16 | +# |
| 17 | +# What it does: |
| 18 | +# 1. HARD: flag any `decodePtr(*const <mod>.<ReprStruct>)` outside |
| 19 | +# src/runtime/collection/ whose line (or the line above) does not carry a |
| 20 | +# `repr-decode-ok:` waiver. Violations beyond BASELINE fail --gate. |
| 21 | +# 2. WARN (informational, never fails): a lone one-representation tag test |
| 22 | +# (`== .array_map` with no `hash_map` nearby) — the guard-shape that is |
| 23 | +# memory-safe but drops semantics past the promotion boundary. |
| 24 | +# |
| 25 | +# Waiver form (line above or same line; keep it greppable): |
| 26 | +# // repr-decode-ok: <why this site may know the representation> [refs: …] |
| 27 | +# |
| 28 | +# Modes: |
| 29 | +# bash scripts/check_repr_decode.sh informational; always exits 0 |
| 30 | +# bash scripts/check_repr_decode.sh --strict exit 1 on any violation |
| 31 | +# bash scripts/check_repr_decode.sh --gate exit 1 if violations > BASELINE |
| 32 | + |
| 33 | +set -euo pipefail |
| 34 | + |
| 35 | +BASELINE=0 |
| 36 | +MODE="${1:-info}" |
| 37 | + |
| 38 | +cd "$(dirname "$0")/.." |
| 39 | + |
| 40 | +# Representation structs private to src/runtime/collection/. Matching on the |
| 41 | +# TYPE name (not the import alias) is deliberate: files alias the module as |
| 42 | +# map_collection / map_mod / set_mod / …, but the struct names are unique in |
| 43 | +# the tree today. |
| 44 | +REPR_TYPES='ArrayMap|PersistentHashMap|HamtMapNode|HashCollisionMapNode|PersistentHashSet|SortedMap|SortedSet|RbNode|Vector|TailNode|HamtNode' |
| 45 | + |
| 46 | +violations=0 |
| 47 | +while IFS= read -r hit; do |
| 48 | + file="${hit%%:*}" |
| 49 | + rest="${hit#*:}" |
| 50 | + line="${rest%%:*}" |
| 51 | + # Waiver on the flagged line or the line above. |
| 52 | + if sed -n "$((line > 1 ? line - 1 : 1)),${line}p" "$file" | grep -q 'repr-decode-ok:'; then |
| 53 | + continue |
| 54 | + fi |
| 55 | + echo "REPR-DECODE: $hit" |
| 56 | + violations=$((violations + 1)) |
| 57 | +done < <(grep -rnE "decodePtr\(\*(const )?[A-Za-z_]+\.(${REPR_TYPES})\)" src \ |
| 58 | + --include='*.zig' | grep -v '^src/runtime/collection/' || true) |
| 59 | + |
| 60 | +if [ "$violations" -gt 0 ]; then |
| 61 | + echo "check_repr_decode: $violations un-waived representation decode(s) outside src/runtime/collection/." |
| 62 | + echo " Iterate via the generic accessors (map.forEachEntry / set.forEachElem / count / get)" |
| 63 | + echo " — they handle every backing, incl. past the ArrayMap→HashMap promotion at 9 entries." |
| 64 | + echo " A site that legitimately must know the representation takes an inline waiver:" |
| 65 | + echo " // repr-decode-ok: <why> [refs: …]" |
| 66 | +else |
| 67 | + echo "check_repr_decode: no un-waived representation decodes outside src/runtime/collection/" |
| 68 | +fi |
| 69 | + |
| 70 | +# Secondary, WARN-only: single-representation tag tests with no dual-backing |
| 71 | +# awareness on the same line. Catches the guard-shape (semantics silently |
| 72 | +# dropped past promotion) once the decode itself is gone. Deliberately not a |
| 73 | +# gate: printers/serializers legitimately scope by tag. |
| 74 | +warns=$(grep -rnE '[=!]= *\.array_map' src --include='*.zig' \ |
| 75 | + | grep -v '^src/runtime/collection/' | grep -v 'hash_map' | grep -v 'repr-decode-ok:' || true) |
| 76 | +if [ -n "$warns" ]; then |
| 77 | + echo |
| 78 | + echo "WARN (informational) — lone .array_map tag tests (no hash_map on the line):" |
| 79 | + printf '%s\n' "$warns" | sed 's/^/ /' |
| 80 | +fi |
| 81 | + |
| 82 | +case "$MODE" in |
| 83 | + --strict) [ "$violations" -eq 0 ] || exit 1 ;; |
| 84 | + --gate) [ "$violations" -le "$BASELINE" ] || exit 1 ;; |
| 85 | + *) ;; |
| 86 | +esac |
| 87 | +exit 0 |
0 commit comments