Skip to content

P1: fix Zig FFI to build and match the Idris2 ABI#33

Merged
hyperpolymath merged 3 commits into
mainfrom
claude/new-session-1fphit
Jun 26, 2026
Merged

P1: fix Zig FFI to build and match the Idris2 ABI#33
hyperpolymath merged 3 commits into
mainfrom
claude/new-session-1fphit

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Problem

zig test src/main.zig -lc (Zig 0.14.0) crashed on the second test:

2/7 main.test.create supervisor and worker... free(): double free detected in tcache 2
error: the following test command crashed

Root cause

Dual ownership of supervision-tree nodes:

  • The library handle keeps a flat nodes: ArrayList(*TreeNode) that owns every node created by otpiser_create_supervisor / otpiser_create_worker.
  • otpiser_add_child also links a child into its parent supervisor's children list.
  • TreeNode.deinit then recursively freed each child (child.deinit() + allocator.destroy(child)).

So on otpiser_free, any node that had been added as a child was freed twice: once via the parent supervisor's recursive deinit, and again via the handle's flat nodes sweep — a classic double free.

Fix

Make children a non-owning reference list. TreeNode.deinit now releases only its own children container and no longer frees the child nodes. The handle's nodes list remains the single owner and frees every node exactly once. This mirrors the alloyiser reference idiom (single flat ownership; handles are plain structs behind ?*T).

Diff is one file, 6 insertions / 5 deletions in src/interface/ffi/src/main.zig.

ABI fidelity (Idris2 is source of truth)

  • No exported C symbol names changed. All 17 C:otpiser_* symbols in Foreign.idr keep their matching export fn.
  • No Result enum integer values changed; the Zig enum still mirrors resultToInt (Ok=0 … MalformedTree=6).
  • All ABI-declared functions are present with matching signatures.

Verification

  • cd src/interface/ffi && zig test src/main.zig -lcall 7 tests pass, 0 errors/warnings
  • cd src/interface/abi && idris2 --build otpiser-abi.ipkgexit 0 (then rm -rf build)
  • Every C:<name> in Foreign.idr has a matching export fn <name>; Result values match resultToInt.

Note: any rust-ci / Hypatia / governance red checks are pre-existing estate-infra issues unrelated to this Zig-only change.

🤖 Generated with Claude Code

https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH


Generated by Claude Code

claude added 3 commits June 26, 2026 21:09
The Zig FFI bridge is half of the ABI-FFI standard, but nothing installed
Zig: .tool-versions only lists it (commented), setup.sh stops at `just`,
and the devcontainer's `postCreateCommand: just deps` referenced a `deps`
recipe that did not exist. Unlike the other toolchain pieces, Zig is not
distributed via GitHub releases, so it must come from ziglang.org.

Add scripts/install-zig.sh: an idempotent, fail-soft installer for the
pinned Zig 0.14.0 (arch/OS-aware, uses the system CA store the agent proxy
populates, never --insecure). If ziglang.org is not on the session's egress
allowlist the download 403s and the script exits 0 with an actionable
message, so it never blocks setup or a session.

Wire it in via the two paths the project already uses: a "Step 1b" in
setup.sh (where the template exposes that step), and a new `deps` Justfile
recipe backing the devcontainer postCreateCommand. Once ziglang.org is
allowlisted, future setups and dev containers install Zig automatically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH
`zig test src/main.zig -lc` crashed with "free(): double free detected"
on the second test (create supervisor and worker).

Root cause: dual ownership of tree nodes. The handle's flat `nodes`
ArrayList owns every node created by otpiser_create_supervisor /
otpiser_create_worker, while otpiser_add_child also links a child into
its parent supervisor's `children` list. TreeNode.deinit then recursively
freed each child (child.deinit() + allocator.destroy(child)), so any node
that was added as a child was freed twice on otpiser_free: once via the
parent's recursive deinit and once via the handle's flat `nodes` sweep.

Fix: make `children` a non-owning reference list. TreeNode.deinit now
releases only its own `children` container and no longer frees the child
nodes. The handle's `nodes` list remains the single owner and frees every
node exactly once. This matches the alloyiser reference idiom (single,
flat ownership; handles are plain structs behind ?*T).

No exported C symbol names or Result-enum integer values were changed.
All 17 C:otpiser_* symbols in Foreign.idr keep their matching export fn,
and the Result enum still mirrors resultToInt (Ok=0 .. MalformedTree=6).

Verification:
- src/interface/ffi: `zig test src/main.zig -lc` -> all 7 tests pass, 0 errors/warnings
- src/interface/abi: `idris2 --build otpiser-abi.ipkg` -> exit 0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH
@hyperpolymath hyperpolymath marked this pull request as ready for review June 26, 2026 22:08
@hyperpolymath hyperpolymath merged commit f7d483d into main Jun 26, 2026
21 of 23 checks passed
@hyperpolymath hyperpolymath deleted the claude/new-session-1fphit branch June 26, 2026 22:09
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.

2 participants