|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-License-Identifier: MPL-2.0 |
| 3 | +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 4 | +# |
| 5 | +# abi-ffi-gate.sh — fail (exit 1) if the Zig FFI does not conform to the Idris2 |
| 6 | +# ABI. The Idris2 ABI is the source of truth. Bash port of the former |
| 7 | +# abi-ffi-gate.py (Python is banned estate-wide). No toolchain needed — only |
| 8 | +# coreutils + grep/awk/sed. Checks: |
| 9 | +# |
| 10 | +# 1. the Zig FFI carries no unrendered `{{...}}` template tokens; |
| 11 | +# 2. every `%foreign "C:<name>"` symbol declared anywhere in the ABI .idr |
| 12 | +# sources is exported by the Zig FFI (`export fn <name>`); |
| 13 | +# 3. the Zig `Result = enum(c_int)` and the Idris `resultToInt` agree on BOTH |
| 14 | +# names and integer values (the `Error`/`err` spelling is treated as one). |
| 15 | +# |
| 16 | +# Usage: bash scripts/abi-ffi-gate.sh [repo_root] (defaults to cwd) |
| 17 | +set -uo pipefail |
| 18 | + |
| 19 | +root="${1:-.}" |
| 20 | +name="$(basename "$(cd "$root" 2>/dev/null && pwd || echo "$root")")" |
| 21 | +abi_dir="$root/src/interface/abi" |
| 22 | +zig_path="$root/src/interface/ffi/src/main.zig" |
| 23 | + |
| 24 | +# canon(name): camelCase -> snake_case, lowercase, err -> error |
| 25 | +canon() { |
| 26 | + printf '%s' "$1" \ |
| 27 | + | sed -E 's/([a-zA-Z0-9])([A-Z])/\1_\2/g' \ |
| 28 | + | tr '[:upper:]' '[:lower:]' \ |
| 29 | + | sed -E 's/^err$/error/' |
| 30 | +} |
| 31 | + |
| 32 | +idr_files="$(find "$abi_dir" -name '*.idr' -not -path '*/build/*' 2>/dev/null | sort)" |
| 33 | +if [ -z "$idr_files" ]; then |
| 34 | + echo "ABI-FFI GATE: SKIP ($name) — no Idris2 ABI .idr files under $abi_dir" |
| 35 | + exit 0 |
| 36 | +fi |
| 37 | +if [ ! -f "$zig_path" ]; then |
| 38 | + echo "ABI-FFI GATE: FAIL ($name) — no Zig FFI at $zig_path" |
| 39 | + exit 1 |
| 40 | +fi |
| 41 | + |
| 42 | +idr="$(cat $idr_files)" |
| 43 | +zig="$(cat "$zig_path")" |
| 44 | +errs="" |
| 45 | + |
| 46 | +# 1. unrendered template tokens |
| 47 | +toks="$(printf '%s\n' "$zig" | grep -oE '\{\{[A-Za-z0-9_]+\}\}' | sort -u | tr '\n' ' ')" |
| 48 | +if [ -n "${toks// /}" ]; then |
| 49 | + errs="${errs} - Zig FFI has unrendered template tokens: ${toks} |
| 50 | +" |
| 51 | +fi |
| 52 | + |
| 53 | +# 2. foreign C symbols must be exported |
| 54 | +csyms="$(printf '%s\n' "$idr" | grep -oE 'C:[A-Za-z0-9_]+' | sed 's/^C://' | sort -u | grep -v '^$')" |
| 55 | +exports="$(printf '%s\n' "$zig" | grep -oE 'export fn [A-Za-z0-9_]+' | awk '{print $3}' | sort -u | grep -v '^$')" |
| 56 | +missing="$(comm -23 <(printf '%s\n' "$csyms") <(printf '%s\n' "$exports") | tr '\n' ' ')" |
| 57 | +ncsyms="$(printf '%s\n' "$csyms" | grep -vc '^$' || true)" |
| 58 | +if [ -n "${missing// /}" ]; then |
| 59 | + errs="${errs} - ABI function(s) not exported by the Zig FFI: ${missing} |
| 60 | +" |
| 61 | +fi |
| 62 | + |
| 63 | +# 3. result-code map (names + values) must agree |
| 64 | +idr_rc="$(printf '%s\n' "$idr" \ |
| 65 | + | grep -oE 'resultToInt +[A-Za-z0-9]+ *= *[0-9]+' \ |
| 66 | + | sed -E 's/resultToInt +([A-Za-z0-9]+) *= *([0-9]+)/\1 \2/' \ |
| 67 | + | while read -r nm val; do echo "$(canon "$nm") $val"; done | sort -u)" |
| 68 | +nrc="$(printf '%s\n' "$idr_rc" | grep -vc '^$' || true)" |
| 69 | + |
| 70 | +# Parse each `enum (c_int) { ... }` block separately (variants up to the first |
| 71 | +# `}`), tagged by a block id. Then in shell, canonicalise each block and pick |
| 72 | +# the one whose `ok == 0` with the most variants — mirrors Python find_result_enum. |
| 73 | +zig_raw="$(printf '%s\n' "$zig" | awk ' |
| 74 | + /enum[ \t]*\([ \t]*c_int[ \t]*\)/ { cap=1; bid++ } |
| 75 | + cap { |
| 76 | + s=$0 |
| 77 | + while (match(s, /@?"?[A-Za-z_][A-Za-z0-9_]*"?[ \t]*=[ \t]*[0-9]+/)) { |
| 78 | + seg=substr(s, RSTART, RLENGTH); s=substr(s, RSTART+RLENGTH) |
| 79 | + gsub(/[@"\t ]/,"",seg) |
| 80 | + eq=index(seg,"="); k=substr(seg,1,eq-1); v=substr(seg,eq+1) |
| 81 | + print bid, k, v |
| 82 | + } |
| 83 | + if ($0 ~ /\}/) cap=0 |
| 84 | + } |
| 85 | +')" |
| 86 | + |
| 87 | +zig_rc_final=""; best_n=-1 |
| 88 | +for bid in $(printf '%s\n' "$zig_raw" | awk 'NF{print $1}' | sort -un); do |
| 89 | + cb="$(printf '%s\n' "$zig_raw" | awk -v b="$bid" '$1==b{print $2" "$3}' \ |
| 90 | + | while read -r nm val; do [ -n "$nm" ] && echo "$(canon "$nm") $val"; done | sort -u)" |
| 91 | + if printf '%s\n' "$cb" | grep -qx 'ok 0'; then |
| 92 | + cnt="$(printf '%s\n' "$cb" | grep -vc '^$')" |
| 93 | + if [ "$cnt" -gt "$best_n" ]; then best_n="$cnt"; zig_rc_final="$cb"; fi |
| 94 | + fi |
| 95 | +done |
| 96 | + |
| 97 | +if [ "$nrc" -gt 0 ] && [ -z "$zig_rc_final" ]; then |
| 98 | + errs="${errs} - no Zig enum(c_int) Result block (with ok = 0) found to compare result codes |
| 99 | +" |
| 100 | +elif [ "$nrc" -gt 0 ] && [ -n "$zig_rc_final" ] && [ "$idr_rc" != "$zig_rc_final" ]; then |
| 101 | + errs="${errs} - Result-code map differs (name or value): |
| 102 | + Idris resultToInt: $(printf '%s' "$idr_rc" | tr '\n' ',') |
| 103 | + Zig Result enum: $(printf '%s' "$zig_rc_final" | tr '\n' ',') |
| 104 | +" |
| 105 | +fi |
| 106 | + |
| 107 | +if [ -n "$errs" ]; then |
| 108 | + echo "ABI-FFI GATE: FAIL ($name)" |
| 109 | + printf '%s' "$errs" |
| 110 | + exit 1 |
| 111 | +fi |
| 112 | +echo "ABI-FFI GATE: OK ($name) — ${ncsyms} ABI functions exported, ${nrc} result codes match" |
| 113 | +exit 0 |
0 commit comments