Skip to content

Commit dc1bebb

Browse files
gHashTagPerplexity Computer
andauthored
ci(spec-drift-guard): extend to Zig + C backends; commit gen/zig + gen/c (#38)
Post t27#1337 (ExprCast lowered for Zig and C emitters), specs/wire.t27 can now round-trip through three text backends. This PR commits the generated files and extends spec-drift-guard.yml to enforce byte-identity on all of them. Backends now under drift-guard: - Rust: gen/rust/wire.rs (via t27c gen-rust) — from #35 - Zig: gen/zig/wire.zig (via t27c gen) — new - C: gen/c/wire.c (via t27c gen-c) — new Verified locally with t27c built from t27@3c912d9 (post-#1337 master): - diff -u vs regenerated output on all three: empty. - Zig cast form: @as(u8, @intcast((w >> 24) & 255)) etc. - C cast form: ((uint8_t)(((w >> 24) & 255))) etc. - 0 occurrences of 'unsupported: ExprCast' in Zig or C output. - cargo test --lib: 101 passed / 0 failed. Docs refresh: - docs/T27_FIRST_MIGRATION.md: ExprCast section marked resolved on all four backends (Rust #1320, Zig+C #1337, Verilog pre-#1320). Build section notes drift-guard covers all three text backends. - docs/T27_PORT_STATUS.md: wire.rs row now says 'T27-FIRST (full, multi-target)' and lists all three gen files. Option B closed end-to-end: upstream ExprCast in all four emitters, tri-net multi-target drift-guard live, parse-path delegates to auto-gen u32_be (#37). Anchor: phi^2 + phi^-2 = 3. Co-authored-by: Perplexity Computer <agent@perplexity.ai>
1 parent 0e1f1f2 commit dc1bebb

5 files changed

Lines changed: 268 additions & 12 deletions

File tree

.github/workflows/spec-drift-guard.yml

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@ on:
99
- "build.rs"
1010
- ".github/workflows/spec-drift-guard.yml"
1111

12-
# Rebuild t27c from upstream master and regenerate gen/rust/wire.rs. If the
13-
# committed output disagrees with the freshly generated output, the job fails
14-
# with a diff so drift between specs/wire.t27 (SSOT) and gen/rust/wire.rs
15-
# cannot land silently.
12+
# Rebuild t27c from upstream master and regenerate every committed backend
13+
# under gen/<target>/wire.<ext> from specs/wire.t27. If any committed output
14+
# disagrees with the freshly generated output, the job fails with a diff so
15+
# drift between the SSOT and generated code cannot land silently.
1616
#
17+
# Covered backends:
18+
# - Rust (t27c gen-rust) -> gen/rust/wire.rs
19+
# - Zig (t27c gen) -> gen/zig/wire.zig
20+
# - C (t27c gen-c) -> gen/c/wire.c
21+
#
22+
# ExprCast is lowered in all three via t27#1320 (Rust) and t27#1337 (Zig+C).
1723
# See docs/T27_FIRST_MIGRATION.md for the SSOT contract.
1824
# Anchor: phi^2 + phi^-2 = 3.
1925

2026
jobs:
2127
regenerate-and-diff:
22-
name: regen gen/rust/wire.rs from specs/wire.t27
28+
name: regen gen/<target>/wire.* from specs/wire.t27
2329
runs-on: ubuntu-latest
2430
steps:
2531
- name: Checkout tri-net
@@ -45,19 +51,61 @@ jobs:
4551
working-directory: tri-net
4652
run: |
4753
../t27/target/release/t27c gen-rust specs/wire.t27 > /tmp/wire_regen.rs
48-
echo "=== regenerated head ==="
54+
echo "=== regenerated Rust head ==="
4955
head -20 /tmp/wire_regen.rs
5056
51-
- name: Diff regenerated vs committed
57+
- name: Diff regenerated Rust vs committed
5258
working-directory: tri-net
5359
run: |
5460
if ! diff -u gen/rust/wire.rs /tmp/wire_regen.rs; then
5561
echo ""
56-
echo "::error file=gen/rust/wire.rs::SPEC/GEN DRIFT: gen/rust/wire.rs disagrees with freshly regenerated output from specs/wire.t27."
62+
echo "::error file=gen/rust/wire.rs::SPEC/GEN DRIFT (Rust): gen/rust/wire.rs disagrees with freshly regenerated output from specs/wire.t27."
5763
echo ""
5864
echo "Fix: rebuild t27c from t27 master, then run"
5965
echo " t27c gen-rust specs/wire.t27 > gen/rust/wire.rs"
6066
echo "See docs/T27_FIRST_MIGRATION.md for the full regeneration recipe."
6167
exit 1
6268
fi
6369
echo "gen/rust/wire.rs matches specs/wire.t27 -> t27c gen-rust output. No drift."
70+
71+
- name: Regenerate gen/zig/wire.zig
72+
working-directory: tri-net
73+
run: |
74+
../t27/target/release/t27c gen specs/wire.t27 > /tmp/wire_regen.zig
75+
echo "=== regenerated Zig head ==="
76+
head -20 /tmp/wire_regen.zig
77+
78+
- name: Diff regenerated Zig vs committed
79+
working-directory: tri-net
80+
run: |
81+
if ! diff -u gen/zig/wire.zig /tmp/wire_regen.zig; then
82+
echo ""
83+
echo "::error file=gen/zig/wire.zig::SPEC/GEN DRIFT (Zig): gen/zig/wire.zig disagrees with freshly regenerated output from specs/wire.t27."
84+
echo ""
85+
echo "Fix: rebuild t27c from t27 master, then run"
86+
echo " t27c gen specs/wire.t27 > gen/zig/wire.zig"
87+
echo "See docs/T27_FIRST_MIGRATION.md for the full regeneration recipe."
88+
exit 1
89+
fi
90+
echo "gen/zig/wire.zig matches specs/wire.t27 -> t27c gen output. No drift."
91+
92+
- name: Regenerate gen/c/wire.c
93+
working-directory: tri-net
94+
run: |
95+
../t27/target/release/t27c gen-c specs/wire.t27 > /tmp/wire_regen.c
96+
echo "=== regenerated C head ==="
97+
head -20 /tmp/wire_regen.c
98+
99+
- name: Diff regenerated C vs committed
100+
working-directory: tri-net
101+
run: |
102+
if ! diff -u gen/c/wire.c /tmp/wire_regen.c; then
103+
echo ""
104+
echo "::error file=gen/c/wire.c::SPEC/GEN DRIFT (C): gen/c/wire.c disagrees with freshly regenerated output from specs/wire.t27."
105+
echo ""
106+
echo "Fix: rebuild t27c from t27 master, then run"
107+
echo " t27c gen-c specs/wire.t27 > gen/c/wire.c"
108+
echo "See docs/T27_FIRST_MIGRATION.md for the full regeneration recipe."
109+
exit 1
110+
fi
111+
echo "gen/c/wire.c matches specs/wire.t27 -> t27c gen-c output. No drift."

docs/T27_FIRST_MIGRATION.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,26 @@ Still hand-written in `src/wire.rs`:
3232
- `Header` struct, `FrameKind` enum, `Header::to_bytes`, `Header::parse`.
3333
- These delegate ALL constants, predicates, AND byte-layout to the auto-gen module — they do not re-declare `VERSION`, `HEADER_LEN`, or any per-byte computation.
3434

35-
## Bootstrap history — ExprCast gap (resolved)
35+
## Bootstrap history — ExprCast gap (resolved on all four backends)
3636

3737
The initial partial flip (PR #33 first commit `a6bb0b0`) shipped hand-written `be_byte` / `u32_be` stubs because `t27c-0.1.0` had no `ExprCast` lowering in the Rust, Zig, or C text emitters. Only `gen-verilog` implemented the cast. On the other three backends the AST node fell through the default arm and produced `"()"` — a type error in Rust, a parse error in Zig, and an honest `/* unsupported: ExprCast */` in C.
3838

3939
Initial triage guessed the bug was in bit-shift parsing, because the first `wire.t27` line that failed was `((w >> 24) & 255) as u8`. Isolation showed the shift is a red herring — the cast is what breaks. Repro table and root-cause pointer live in the upstream issue.
4040

41-
**Status**: gen-rust half fixed by [gHashTag/t27#1320](https://github.com/gHashTag/t27/pull/1320) (closes [t27#1314](https://github.com/gHashTag/t27/issues/1314)) — the Rust arm now lives at `bootstrap/src/compiler.rs:8172`. `gen (Zig)` and `gen-c` emitters still fall through their default arms and are tracked separately upstream. `tri-net` only consumes `gen-rust`, so this repo is fully flipped.
41+
**Status**: fully resolved on all four backends.
42+
43+
- Rust arm — [gHashTag/t27#1320](https://github.com/gHashTag/t27/pull/1320) (closes [t27#1314](https://github.com/gHashTag/t27/issues/1314)); lives at `bootstrap/src/compiler.rs:8172`, emits `({operand} as {target})`.
44+
- Zig arm — [gHashTag/t27#1337](https://github.com/gHashTag/t27/pull/1337) (closes [t27#1333](https://github.com/gHashTag/t27/issues/1333)); emits `@as(<T>, @intCast(<operand>))` — narrows and widens, stronger than the `@as`-only suggestion.
45+
- C arm — [gHashTag/t27#1337](https://github.com/gHashTag/t27/pull/1337); emits `((<uintN_t>)(<operand>))` via `Self::type_to_c`.
46+
- Verilog arm — already lowered ExprCast pre-#1320.
47+
48+
`tri-net` now consumes three text backends (`gen-rust`, `gen`, `gen-c`) all under drift-guard.
4249

4350
## Build story
4451

4552
- `gen/rust/wire.rs` is committed. Contributors do not need `t27c` installed to build tri-net.
4653
- `build.rs` will optionally invoke `t27c gen-rust` when `T27C_REGENERATE=1` is set and `t27c` is on `$PATH` (or `T27C=/path/to/t27c`). It prints a warning if the invocation fails and does not fail the build — CI without `t27c` still passes.
47-
- `build.rs` deliberately does not overwrite `gen/rust/wire.rs` automatically — the CI drift-guard (`.github/workflows/spec-drift-guard.yml`) rebuilds t27c from t27 master and diffs the regenerated output against the committed file on every PR, so drift is caught at PR time rather than silently masked by a per-machine local regeneration. Contributors who want a local check can set `T27C=/path/to/t27c T27C_REGENERATE=1 cargo build` and compare stdout manually.
54+
- `build.rs` deliberately does not overwrite `gen/rust/wire.rs` automatically — the CI drift-guard (`.github/workflows/spec-drift-guard.yml`) rebuilds t27c from t27 master and diffs the regenerated output against every committed file under `gen/<target>/wire.*` on every PR, so drift is caught at PR time rather than silently masked by a per-machine local regeneration. The guard covers three backends today: `gen/rust/wire.rs` (via `t27c gen-rust`), `gen/zig/wire.zig` (via `t27c gen`), and `gen/c/wire.c` (via `t27c gen-c`). Contributors who want a local check can set `T27C=/path/to/t27c T27C_REGENERATE=1 cargo build` and compare stdout manually.
4855

4956
## Test story
5057

docs/T27_PORT_STATUS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on the generated Verilog, matching the Rust module's tests.
77

88
| Rust module | T27 spec | Status | Notes |
99
|---|---|---|---|
10-
| `src/wire.rs` | `specs/wire.t27` | ✅ T27-FIRST (full) | Constants, predicates, AND byte-layout auto-generated into `gen/rust/wire.rs` via `t27c gen-rust` (pure raw output, 0 hand-edits after t27#1320 landed the ExprCast Rust arm). `src/wire.rs` consumes the generated module and adds ergonomic wrappers (`Header`, `FrameKind`, `to_bytes`, `parse`) only. CI drift-guard (`.github/workflows/spec-drift-guard.yml`) rebuilds t27c and diffs the regenerated file against the committed copy on every PR. 101 lib tests + 25 M2 pure-logic tests green. See `docs/T27_FIRST_MIGRATION.md`. |
10+
| `src/wire.rs` | `specs/wire.t27` | ✅ T27-FIRST (full, multi-target) | Constants, predicates, AND byte-layout auto-generated into `gen/rust/wire.rs` via `t27c gen-rust` (pure raw output, 0 hand-edits after t27#1320 landed the ExprCast Rust arm). Also emitted into `gen/zig/wire.zig` (`t27c gen`) and `gen/c/wire.c` (`t27c gen-c`) after t27#1337 landed ExprCast for Zig and C. `src/wire.rs` consumes the Rust module and adds ergonomic wrappers (`Header`, `FrameKind`, `to_bytes`, `parse`) only; the parse path delegates to auto-gen `u32_be` (PR #37). CI drift-guard (`.github/workflows/spec-drift-guard.yml`) rebuilds t27c and diffs the regenerated Rust, Zig, and C files against the committed copies on every PR. 101 lib tests + 25 M2 pure-logic tests green. See `docs/T27_FIRST_MIGRATION.md`. |
1111
| `src/modem.rs` (BPSK core) | `t27/specs/fpga/bpsk.t27` | ✅ PORTED | modulator + Barker-13 correlator (on t27 master) |
1212
| `src/modem.rs` (RX DSP: RRC/timing/CFO) || ❌ NOT PORTABLE | floating-point; T27 is integer |
1313
| `src/routing.rs` (ETX metric) | `specs/etx.t27` | ⬜ TODO | integer/fixed math ports; dynamic tables need array/RAM (t27#1258) |

gen/c/wire.c

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/* ============================================================================
2+
Generated from t27 spec: MeshWire
3+
DO NOT EDIT - generated by t27c gen-c
4+
phi^2 + 1/phi^2 = 3 | TRINITY
5+
============================================================================ */
6+
7+
#include <stdint.h>
8+
#include <stdbool.h>
9+
#include <stddef.h>
10+
#include <assert.h>
11+
12+
#ifndef MESHWIRE_H
13+
#define MESHWIRE_H
14+
15+
/* -------------------------------------------------------
16+
Constants
17+
------------------------------------------------------- */
18+
19+
#define VERSION 1
20+
#define KIND_HELLO 0
21+
#define KIND_DATA 1
22+
#define HEADER_LEN 11
23+
24+
/* -------------------------------------------------------
25+
Function prototypes
26+
------------------------------------------------------- */
27+
28+
bool frame_kind_valid(uint8_t k);
29+
uint8_t be_byte(uint32_t w, size_t i);
30+
uint32_t u32_be(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3);
31+
uint8_t header_byte(uint8_t kind, uint32_t src, uint32_t dst, uint8_t ttl, size_t idx);
32+
bool parse_accepts(uint8_t b0, uint8_t b1);
33+
34+
/* -------------------------------------------------------
35+
Function implementations
36+
------------------------------------------------------- */
37+
38+
bool frame_kind_valid(uint8_t k) {
39+
return (k <= KIND_DATA);
40+
}
41+
42+
uint8_t be_byte(uint32_t w, size_t i) {
43+
if ((i == 0)) {
44+
return ((uint8_t)(((w >> 24) & 255)));
45+
} else if ((i == 1)) {
46+
return ((uint8_t)(((w >> 16) & 255)));
47+
} else if ((i == 2)) {
48+
return ((uint8_t)(((w >> 8) & 255)));
49+
} else {
50+
return ((uint8_t)((w & 255)));
51+
}
52+
}
53+
54+
uint32_t u32_be(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) {
55+
return ((((((uint32_t)(b0)) << 24) | (((uint32_t)(b1)) << 16)) | (((uint32_t)(b2)) << 8)) | ((uint32_t)(b3)));
56+
}
57+
58+
uint8_t header_byte(uint8_t kind, uint32_t src, uint32_t dst, uint8_t ttl, size_t idx) {
59+
if ((idx == 0)) {
60+
return VERSION;
61+
} else if ((idx == 1)) {
62+
return kind;
63+
} else if ((idx <= 5)) {
64+
return be_byte(src, (idx - 2));
65+
} else if ((idx <= 9)) {
66+
return be_byte(dst, (idx - 6));
67+
} else {
68+
return ttl;
69+
}
70+
}
71+
72+
bool parse_accepts(uint8_t b0, uint8_t b1) {
73+
if ((b0 == VERSION)) {
74+
return frame_kind_valid(b1);
75+
} else {
76+
return false;
77+
}
78+
}
79+
80+
/* -------------------------------------------------------
81+
Invariants (compile-time assertions)
82+
------------------------------------------------------- */
83+
84+
/* invariant: header_is_11_bytes */
85+
/* _Static_assert(1, "invariant: header_is_11_bytes"); */
86+
87+
/* invariant: kinds_distinct */
88+
/* _Static_assert(1, "invariant: kinds_distinct"); */
89+
90+
91+
/* -------------------------------------------------------
92+
Tests
93+
------------------------------------------------------- */
94+
95+
void test_byte0_is_version(void) {
96+
/* TODO: implement test */
97+
}
98+
99+
void test_byte1_is_kind(void) {
100+
/* TODO: implement test */
101+
}
102+
103+
void test_src_be_first_and_last_byte(void) {
104+
/* TODO: implement test */
105+
}
106+
107+
void test_ttl_is_last_byte(void) {
108+
/* TODO: implement test */
109+
}
110+
111+
void test_src_roundtrips_through_bytes(void) {
112+
/* TODO: implement test */
113+
}
114+
115+
void test_parse_accepts_valid(void) {
116+
/* TODO: implement test */
117+
}
118+
119+
void test_parse_rejects_bad_version(void) {
120+
/* TODO: implement test */
121+
}
122+
123+
void test_parse_rejects_bad_kind(void) {
124+
/* TODO: implement test */
125+
}
126+
127+
128+
#endif /* MESHWIRE_H */

gen/zig/wire.zig

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Generated from t27 spec: MeshWire (module name)
2+
// DO NOT EDIT — generated by t27c
3+
// phi^2 + 1/phi^2 = 3 | TRINITY
4+
5+
const std = @import("std");
6+
7+
const types = @import("types.zig");
8+
9+
const VERSION: u8 = 1;
10+
const KIND_HELLO: u8 = 0;
11+
const KIND_DATA: u8 = 1;
12+
const HEADER_LEN: usize = 11;
13+
fn frame_kind_valid(k: u8) bool {
14+
return k <= KIND_DATA;
15+
}
16+
fn be_byte(w: u32, i: usize) u8 {
17+
if (i == 0) {
18+
return @as(u8, @intCast((w >> 24) & 255));
19+
} else if (i == 1) {
20+
return @as(u8, @intCast((w >> 16) & 255));
21+
} else if (i == 2) {
22+
return @as(u8, @intCast((w >> 8) & 255));
23+
} else {
24+
return @as(u8, @intCast(w & 255));
25+
}
26+
}
27+
fn u32_be(b0: u8, b1: u8, b2: u8, b3: u8) u32 {
28+
return (((@as(u32, @intCast(b0)) << 24) | (@as(u32, @intCast(b1)) << 16)) | (@as(u32, @intCast(b2)) << 8)) | @as(u32, @intCast(b3));
29+
}
30+
fn header_byte(kind: u8, src: u32, dst: u32, ttl: u8, idx: usize) u8 {
31+
if (idx == 0) {
32+
return VERSION;
33+
} else if (idx == 1) {
34+
return kind;
35+
} else if (idx <= 5) {
36+
return be_byte(src, idx - 2);
37+
} else if (idx <= 9) {
38+
return be_byte(dst, idx - 6);
39+
} else {
40+
return ttl;
41+
}
42+
}
43+
fn parse_accepts(b0: u8, b1: u8) bool {
44+
if (b0 == VERSION) {
45+
return frame_kind_valid(b1);
46+
} else {
47+
return false;
48+
}
49+
}
50+
test "byte0_is_version" {
51+
}
52+
test "byte1_is_kind" {
53+
}
54+
test "src_be_first_and_last_byte" {
55+
}
56+
test "ttl_is_last_byte" {
57+
}
58+
test "src_roundtrips_through_bytes" {
59+
}
60+
test "parse_accepts_valid" {
61+
}
62+
test "parse_rejects_bad_version" {
63+
}
64+
test "parse_rejects_bad_kind" {
65+
}
66+
comptime {
67+
// invariant: header_is_11_bytes
68+
@compileLog("invariant: header_is_11_bytes verified");
69+
}
70+
comptime {
71+
// invariant: kinds_distinct
72+
@compileLog("invariant: kinds_distinct verified");
73+
}

0 commit comments

Comments
 (0)