Skip to content

feat(ffi): contract-test the idris2↔zig token-buffer ABI (seams)#334

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/ffi-seams
Jun 29, 2026
Merged

feat(ffi): contract-test the idris2↔zig token-buffer ABI (seams)#334
hyperpolymath merged 1 commit into
mainfrom
claude/ffi-seams

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Seam-contract tests for the idris2↔zig token buffer

Adapts boj-server's seam pattern (ffi/zig/src/seams.zig + the proof-debt.md axiom/external-validation discipline, standards#203) to ephapax's lexer FFI boundary: tokbuf.zigtokbuf.hZigBuffer.idr's %foreign bindings.

The headline

eph_tokbuf_str_ptr ends in a @ptrCast that asserts (does not check) the NUL sentinel — the // SAFETY: comment argues it holds because copyStr allocates len+1 and writes a trailing 0. That's a class-(J) assumption (true by construction, unprovable at the cast site). The str_ptr seams externally validate it: push real strings, read them back through the cast, assert byte-round-trip + NUL-termination. A regression in copyStr now fails a seam loudly instead of becoming UB at an Idris String read. → turns the zig_ptr_cast finding from suppressed into tested.

10 seams (all green locally, zig 0.15.2)

  • @ptrCast/NUL-sentinel invariant ×3 (incl. the "" empty-string sentinel path)
  • marshalling round-trips: tag (u32), bool, line/col incl. negatives
  • i32 → usize clamping: new(cap ≤ 0), str_len < 00 + "" (no over-read)
  • realloc growth (push past cap) + new/free lifecycle

Changes

File What
tokbuf.zig export fnpub export fn ×10 so seams.zig can @import. Purely additive — emitted C symbols unchanged.
seams.zig (new) the 10 tests + methodology + an ABI finding in its header. Run: zig test -lc idris2/ffi/zig/seams.zig
ffi-seams.yml (new) CI gate; setup-zig 0.15.2, zig test -lc. Dedicated workflow (not abi-verify.yml, which is Idris2-container/src/abi-scoped). Follows the estate no-path-filter changes+needs+if convention so it can't strand PRs.

Finding (documented, not auto-fixed)

Integer type-width drift across the three layers — tag/bool_val/kind declared Int (Idris) / int32_t (.h) / u32|u8 (zig). Functions for current value ranges but not type-clean (sharpest: bool_val, 1-byte vs declared 4-byte). Recommend regenerating tokbuf.h from the zig sigs + Bits32/Bits8 in ZigBuffer.idr — left for owner triage (an FFI sig edit can shift the binding).

Notes

  • Out of scope: the Rust-side from_raw (ephapax-runtime/-vram-cache) + ephapax-interp unsafe/as_ptr — different boundary, already // SAFETY:-documented.
  • The methodology lives in the seams.zig header rather than a CC-BY-SA-4.0 SEAMS.adoc because this repo's .git/hooks/pre-commit is the pre-fix version that demands MPL-2.0 even for .adoc (the standards hook was fixed to allow CC-BY-SA-4.0 for docs; ephapax's copy is stale). Minor follow-up: sync ephapax's hook to the standards version.

🤖 Generated with Claude Code

Adds `idris2/ffi/zig/seams.zig` — 10 integration-contract tests over the
tokbuf C-ABI (tokbuf.zig ↔ tokbuf.h ↔ ZigBuffer.idr's %foreign bindings),
adapting the boj-server seam pattern (standards#203 trusted-base reference).

The headline: `eph_tokbuf_str_ptr` ends in a `@ptrCast` that *asserts* (not
checks) the NUL-sentinel. The str_ptr seams push real strings and read them
back, asserting the bytes round-trip and stay NUL-terminated — turning the
`// SAFETY:` comment's assertion into a tested invariant. A regression in
`copyStr` (drop the +1 / the trailing NUL) now fails loudly instead of
becoming UB at an Idris `String` read. Other seams cover tag/bool/line/col
marshalling (incl. negatives), i32→usize clamping (`new(cap≤0)`,
`str_len<0`), and realloc-growth + new/free lifecycle.

Changes:
- `tokbuf.zig`: `export fn` → `pub export fn` (×10) so seams.zig can
  `@import` them. Purely additive — the emitted C symbols are unchanged.
- `seams.zig` (new): the 10 contract tests, with the methodology + an ABI
  finding documented in its header (integer type-width drift across the three
  layers: tag/bool_val/kind declared Int / int32_t / u32|u8 — functions for
  current ranges but not type-clean; flagged for owner triage, not auto-fixed).
  Run: `zig test -lc idris2/ffi/zig/seams.zig`.
- `ffi-seams.yml` (new): CI gate. A dedicated workflow rather than extending
  abi-verify.yml (Idris2-container + src/abi-scoped — wrong env); follows the
  estate no-path-filter `changes`+`needs`+`if` convention so it can't strand
  PRs. setup-zig 0.15.2, runs `zig test -lc`.

Verified locally: all 10 seams pass under zig 0.15.2. Retires the
`zig_ptr_cast` finding's assurance gap (tested, not just suppressed). The
Rust-side from_raw/unsafe boundary is separate and out of scope.

(Methodology folded into the seams.zig header rather than a CC-BY-SA-4.0
SEAMS.adoc because this repo's pre-commit hook is the pre-fix version that
still demands MPL-2.0 even for .adoc — flagged as a follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hyperpolymath
hyperpolymath marked this pull request as ready for review June 29, 2026 12:22
@hyperpolymath
hyperpolymath merged commit cac5c4c into main Jun 29, 2026
20 checks passed
@hyperpolymath
hyperpolymath deleted the claude/ffi-seams branch June 29, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant