Skip to content

test(runtime): Zig-native C-ABI tests for lower.zig (issue #14) - #28

Merged
adilfaisal01 merged 4 commits into
mainfrom
feat/zig-native-lower-tests
Sep 28, 2026
Merged

adilfaisal01 merged 4 commits into
mainfrom
feat/zig-native-lower-tests

Conversation

@adilfaisal01

Copy link
Copy Markdown
Member

Closes #14.

What

Zig-native C-ABI tests for the comptime graph VM (lower.zig), reducing reliance on the Python ctypes oracle. The issue's blocker (lower.zig importing the generated graph_data.zig directly) is resolved two ways:

  • comptime injection — lower.zig is now graph-agnostic (pub fn Vm(Ctx)); the deployed graph binding and the shinro_step export moved into a thin entry.zig.
  • fixture graphs — tests/lower_fixture_graph.zig (hand-authored) and tests/graphs/ (5 generated, frozen fixtures).

Commits

  1. 6528fd7 — Zig-native VM tests; split the C-ABI export into entry.zig.
  2. e08d9f2 — frozen multi-graph fixtures: KF+LQR, toy LSTM, the real Unitree Go2 MLP, and the real eco-drone GRU. Weights are a raw little-endian f32 .bin embedded with @embedFile (~5x smaller than hex literals).
  3. 278c7c8 — drive the frozen fixtures through the exported shinro_step (ctypes).
  4. 4967159 — native mpc (.solve_qp) fixture; route the Zig-native tests through the exported C-ABI.

Coverage

  • zig build test: 56/56 — linalg kernels (48), hand-fixture VM ABI (2), 5 frozen fixtures (KF+LQR, toy LSTM, Go2 MLP, eco-GRU, KF+MPC .solve_qp), emosqp solver (1). All through the exported shinro_step except the hand fixture (VM directly).
  • make test-zig: 96 passed, 2 skipped — the 5 fixtures are also validated through a built .so.
  • No VM op is left without Zig-native coverage (solve_qp was the last).

Production unchanged

The shipped ReleaseFast .so stays byte-identical (7d42e97b...). The entry.zig split relocates the graph binding + export; it does not change the compiled kernel. Fixtures are test-only and never enter the binary.

Notes

  • Fixtures are generated once by scripts/gen_lower_fixtures.py (needs the sibling shinro-bench checkout for the real policies) and committed; deliberately not wired into make zig-gen/CI so they never churn.
  • The wheel grows ~1.2 MB because the fixtures live under the already-packaged runtime/tests/; that can be excluded separately if desired.

….zig

Address issue #14: shinro_step's path previously had only the Python ctypes
oracle. Add a Zig-native test that compiles the comptime VM against a small
committed fixture graph and drives it in-process — no .so, no ctypes, no
Python — so a VM regression fails in the plain `zig build test` step.

To let the VM take any graph, separate the deployment binding from the VM:

- lower.zig is now graph-agnostic: `pub fn Vm(Ctx)` wraps the VM, with `step`
  marked `inline` so entry.zig's exported wrapper compiles to exactly the
  pre-split body (the shipped ReleaseFast .so is byte-identical).
- entry.zig (new): the only file that imports graph_data (plus solver_meta/qp
  for QP graphs), assembles Ctx, and exports shinro_step.
- build.zig roots the shared library at entry.zig; the VM test binds its own
  fixture graph instead of relying on a module-level graph_data import.
- README.md / docs/codegen.md updated for entry.zig and the generic VM.

Verified: `zig build test` 51/51; `make test-zig` 91 passed, 2 skipped (ctypes
oracle matches interpret() for KF+LQR, MPC/.solve_qp, ONNX, recurrent). The
pre-refactor ReleaseFast .so is byte-identical (sha256 7d42e97b...).
…Go2, GRU)

Extend the Zig-native VM tests (issue #14) to a menu of real graph types, frozen
once and committed so the oracle never churns. scripts/gen_lower_fixtures.py
lowers each composed graph, compacts its f32 weight blob into a raw
little-endian .bin re-exposed via @embedfile + bytesAsSlice (an
align(@Alignof(f32)) copy keeps it safe for the kernel's vector weight loads),
and emits interpret() vectors on 8 seeded samples.

Fixtures under runtime/tests/graphs/: KF+LQR (classical), toy_lstm (fused LSTM),
the real Unitree Go2 MLP, and the real eco-drone GRU. tests/lower_graph.zig is a
single shared driver compiled once per fixture; build.zig gains a graph_fixtures
table, so adding a graph type is one row + one generator entry. Payload ~1.2 MB
vs ~6 MB as hex-literal Zig.

Generated deliberately — not wired into make zig-gen or CI. Test-only: the
library wiring (entry.zig / lower.zig / -Dgraph) is untouched and the shipped
ReleaseFast .so stays byte-identical.

Verified: zig build test 55/55; make test-zig 91 passed, 2 skipped.
…ymbol

TestFrozenFixturesCAbi builds a .so per committed fixture and calls the exported
shinro_step via ctypes, checked against the same committed vectors. This covers
the export / port-packing surface the in-process Vm(Ctx).step oracle bypasses,
plus the compiled path for the compact @embedfile graph format. Self-contained:
reads only the committed fixtures (no shinro-bench, no ONNX).

Verified: make test-zig 95 passed, 2 skipped (was 91).
…he C-ABI

Close the last VM op without Zig-native coverage: add an `mpc` frozen fixture
(KF + MPC_LTI, has_solve_qp=true, tol 1e-3) and route the frozen-fixture driver
through the exported `shinro_step` instead of `Vm(Ctx).step`. Each fixture gets
its own `entry` module (rooted at entry.zig) so `graph_data` resolves to that
fixture; the QP one also compiles the baked OSQP solver. entry.zig's
shinro_step becomes `pub export` (symbol name unchanged).

The generator gains a per-fixture tolerance and runs `zig fmt` on its output so
regeneration is a no-op; TestFrozenFixturesCAbi gains `mpc`.

Verified: zig build test 56/56; make test-zig 96 passed, 2 skipped.
@adilfaisal01
adilfaisal01 merged commit 09de567 into main Sep 28, 2026
7 checks passed
@adilfaisal01
adilfaisal01 deleted the feat/zig-native-lower-tests branch September 28, 2026 15:52
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.

Zig-native C-ABI tests for lower.zig

1 participant