Repository navigation
Port gHashTag/trinity:fpga/openxc7-synth/riscv_blink.v (Verilog, 1 module) to specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27 #8264
Description
Port gHashTag/trinity:fpga/openxc7-synth/riscv_blink.v (Verilog, 1 module) to specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27
Roadmap stage 7 of 9: Silicon: Verilog from .t27 (goal #4549). Target of the stage: t27c gen-verilog.
fpga/openxc7-synth/riscv_blink.v (gHashTag/trinity) is 101 lines of hand-written Verilog. Re-author it as specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27, so that the code is generated from .t27 instead of written by hand. The original stays where it is: this issue adds the .t27 source it should have been written in.
Boundary
specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27
Only this file
Write this one file and nothing else. Anything you need to try out goes under /tmp, never into the repository: a scratch file, a notes file or a debug_output.txt in the worktree is outside the boundary, it is reported, and it stops the work being published.
Read the original first
fpga/openxc7-synth/riscv_blink.v is in gHashTag/trinity, not in this checkout - do not look for it on disk, it is not there. Its full text, as of 8723fe0a725a, is quoted at the end of this issue, one line per numbered row: the number and the | are not part of the source.
What to write
Create specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27 as module trinity_top { ... }: its constants, structs, enums and functions describe what the original computes, and t27c gen-verilog turns it into the module. Quoted verbatim from the original:
- line 17 -
module trinity_top (
Look at specs/fpga/apb_bridge.t27 and specs/fpga/cts.t27 for the shape the Verilog backend lowers. Add test blocks for the functions that compute the module's outputs, asserting on the values the original produces for inputs you choose.
Acceptance criteria
-
test -f specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27 && echo presentprintspresent(today the file does not exist)
-
grep -cE '^\s*(pub )?module trinity_top\b' specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27prints1- the.t27module carries the original's name
-
t27c gen-verilog specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27 | grep -cE '^module trinity_top ?\('prints1- the generated Verilog is a module of the same name
-
t27c spec-status specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27does not printNOPARSE- the file parses
-
grep -cE '^[[:space:]]*test[[:space:]]+("|[A-Za-z_])' specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27prints at least1
-
t27c test-report specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27 2>&1 | grep -c BLOCKEDprints0- the generated code must COMPILE and its tests must run
User Scenarios & Testing
- Given
specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27does not exist andfpga/openxc7-synth/riscv_blink.v(gHashTag/trinity) is quoted below,
when its 1 module are ported and the commands under Acceptance criteria are run from the repository root,
then each of those commands prints the value stated beside it.
Requirements
- FR-001:
specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27MUST declare every module listed above under the name given, carrying the original's decisions as code; only data moving in or out may be anundefined;body, and no test may call one. - FR-002: the file MUST parse and compile:
t27c parse specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27andt27c test-report specs/port/trinity/fpga/openxc7-synth/riscv_blink.t27are the checks. - FR-003: the change MUST be this one file - no other spec, no scratch file, no edit to the original.
- FR-004: each test MUST assert something the original does; a test that asserts
trueports nothing.
The goal of the game
Everything below the interface is written once, in .t27, and generated to its
target: Rust for servers; Zig, C and Verilog for the core and the silicon. That is
law L0 (docs/T27-CONSTITUTION.md). Two exceptions are named and nothing else is
exempt: the seed (t27c itself, bootstrap/, stays hand-written Rust) and the
interface (Swift for trios, TSX for the web), which stage 8 has to decide.
The roadmap walks the stack in eight stages. Each stage is a goal issue; the work is
filed as one task per file, like the one you are reading.
| Stage | Goal | What | Where it lives | Rewrite | Generated to |
|---|---|---|---|---|---|
| 1 | #4543 | the swarm's own tools | gHashTag/t27 | Python, shell | t27c gen-rust |
| 2 | #4544 | the Queen and her bees (api.t27.ai) | gHashTag/BrowserOS | TypeScript, Go | t27c gen-rust |
| 3 | #4545 | the app and the bot (app.t27.ai) | gHashTag/999-multibots-telegraf | TypeScript, JavaScript, Rust | t27c gen-rust |
| 4 | #4546 | VIBEE services on Fly.io | gHashTag/vibee-gleam | TypeScript, JavaScript, Gleam | t27c gen-rust |
| 5 | #4547 | the Zig core | gHashTag/trinity, gHashTag/trios | Zig, C | t27c gen, gen-c |
| 6 | #4548 | trios Rust rings | gHashTag/trios, gHashTag/trios-railway | Rust | t27c gen-rust |
| 7 | #4549 | silicon | gHashTag/t27, gHashTag/trinity | Verilog | t27c gen-verilog |
| 8 | #4550 | interfaces | trinity, trios, the sites | HTML, CSS, Swift | undecided |
| 9 | #4858 | the endgame | all of gHashTag/BrowserOS, and every third-party dependency | C++ and the rest | t27c gen-rust, gen-c |
The endgame goes past our own code. When stage 8 is done the comb keeps
widening: the whole BrowserOS browser, not only its trios/agent-server, and every
third-party library the stack runs in another language, rewritten in .t27. Goal
#4858 holds what is measured so far and what is not:
-
BrowserOS
devis 5.48 MB of source. -
The browser is Chromium 146.0.7680.31, which BrowserOS patches and fetches at build time. It was counted by the same rules as the stack count:
Part Size Chromium src1,144.7 MB V8 145.6 MB Skia 51.4 MB Total 1,341.7 MB That total is seven times the whole stack count (192.2 MB).
- Blink is not in it: 132.4 MB outside its tests, under
third_party/, which the rules skip. - The other 258 DEPS repositories are not in it either.
- Blink is not in it: 132.4 MB outside its tests, under
-
The lockfiles of the running services hold 2,187 npm packages, 650 crates and 35 Go modules.
No task is filed against the endgame until stage 8 is done and the goal's questions are answered. It is where the game ends.
Progress is measured, not declared. apps/website/scripts/roadmap-stack.mjs in
gHashTag/trinity counts the bytes of every hand-written language in every repository
and publishes them at t27.ai/roadmap/stack.json, drawn by the ROADMAP tab. A stage
closes when its languages are gone from that count, or what remains is a written
exception.
How one file moves. A task names ONE original and ONE target,
specs/port/<repo>/<path>.t27. You write that .t27. The Queen's review runs the
task's commands exactly as written, compiles the generated code and runs your tests;
the publisher then opens a pull request from your branch. Swapping the generated code
in for the original, in its own repository, is a later step and not yours: the
original is never edited by a port.
What a lost turn looks like - each of these was measured on the swarm, and each
costs the task: it is held, and later filed again under a new number.
- An empty branch. Nothing written, or everything written somewhere else. A
partial port that parses, with a few passing tests and the missing functions named
in your report, is worth more than nothing. - Files outside the boundary.
debug_output.txt,stdout.txt,test.t27, a copy
of the original, an edit to another spec: each is reported and the work is not
published. Scratch goes under/tmp. - Another language inside a
.t27. Of the 35 files underspecs/port/tools/on
2026-09-27, 3 built: 7 did not parse and 25 generated code that did not compile,
mostly because their bodies were Zig or Rust pasted in.spec-statussaid
IMPLEMENTED for 24 of them. Onlytest-reportis proof. - Looking for the original on disk. If it is in another repository, it is not in
your checkout. The task quotes it in full.
How to rewrite code into .t27
Port the decision, not the plumbing
The ports that build share one design (specs/port/tools/check_fix_carries_source.t27,
6 of 6 tests pass). The logic that DECIDES becomes pure .t27 functions. Everything
that touches the world - files, network, subprocess, database, HTTP framework, clock,
randomness, environment - is either a parameter the caller passes in, or a declared
function with an undefined; body that no test reaches. That is not a shortcut: the
decision is the part that can be generated and tested, and it is the part that was
worth porting.
| In the original | In .t27 |
|---|---|
exit codes, thrown errors, Result, Err |
const EXIT_FAIL: i32 = 1; and a verdict function returning i32; or struct { ok: bool, value: T }; or ?T with null |
| a dict, an object, a JSON record | a named struct with typed fields - only the facts the logic reads |
a list that grows (append, push) |
a fixed-capacity struct { items: [N]T, len: usize }, or a count |
| a list of strings as a parameter | lines: []str; a test passes &lines where var lines: [2][]const u8 = ["a", ""]; |
| a regex match, a parsed file, a subprocess result | the ANSWER as a bool or a count parameter |
print, logging |
return a marker string or a code; the caller prints |
argv, main() |
a verdict function taking argc: u32 and the flags |
async/await, promises, callbacks |
the synchronous decision; waiting is the caller's job |
| a class with methods | a struct plus free functions Type_method(self: Type, ...) |
a union or enum type, Rust match |
enum and switch with .Variant => arms, or if/else |
a Verilog module |
module Name { ... } in .t27; t27c gen-verilog emits module Name ( |
Keep every name the task lists: the name is how the port is checked. Keep the
behaviour: a test asserts what the original returns for inputs you choose. When a
construct cannot be expressed, port the rest, and name the missing piece and its line
in your report rather than faking it.
What compiles today
Verified with t27c test-report:
- functions:
fn f(x: i64) -> i64 { ... }(->,→or no arrow);pub fnto export const,var,let(same asconst),x += 1, castsn as i64if (c) { } else if (c) { } else { }, also as an expression:const w = if (x < 0) "neg" else "pos";while (i < n) { i += 1; },break,continue,for x in xs { },for (xs) |x| { }for i in 0..nonly whennisusize; over ani32it does not compileswitch (c) { .Red => 1, .Green => 2, else => 3, }- arms are.Variant, a number, a char, orelse- integers
u8..u64,usize,i8..i64,f32,f64,bool;0xFF,0b101,1_000 - strings:
str(becomes[]const u8); escapes are only\n \t \\ \" ==on strings when one side is a literal, a string parameter or a string struct field- arrays
[4]u32 = [2, 3, 5, 7], slices[]const u32,.len,[_]u32{0} ** 16 struct Point { x: i32, y: i32, }andPoint { x: 3, y: -4 };enum Color { Red, Green, }?Twithnull; tuple returnsfn divmod(a: u32, b: u32) -> (u32, u32)andconst (q, r) = divmod(7, 2);- tests:
test name { assert(f(1) == 2); },assert_eq(a, b);invariant name { ... }is checked at compile time - comments:
//,/* */,# - a module line:
module port::name;ormodule Name { ... }
What does not, and what you see instead
- generics (
<T>,comptime T), closures, traits,impl: parse error or wrong code Result,Ok,Err,Option,Some,unwrap,!T,error.X,try,catch: undeclared identifier or parse errorprint(...),println!,format!,console.log: undeclared;std.debug.printcompiles in Zig only and breaks the C and Rust outputs- string concatenation (
"a" + b), f-strings, template literals: does not compile - growable lists (
ArrayList,.push,[]literals that grow), maps: do not compile; no map or list library inspecs/builds today match: parses, and is then DROPPED - the test fails with no error; useswitch"\r"in a string is not a carriage return; the test fails; use the char'\r'fn f() -> T;with no body: parse error; write{ undefined; }, and never call it from a testuse a::b::fn_name;does not resolve;use a::b;(the filespecs/a/b.t27) does. Prefer not to import: say in your report what you would reuse- a local string compared with
==to aconststring: does not compile; compare with a literal
A complete port that passes
module port::clamp_count;
pub fn clamp(x: i64, lo: i64, hi: i64) -> i64 {
if (x < lo) {
return lo;
}
if (x > hi) {
return hi;
}
return x;
}
// Python's `line.strip()` is falsy exactly when every character is whitespace.
pub fn is_blank(line: []const u8) -> bool {
for c in line {
if (c != ' ' and c != '\t' and c != '\r' and c != '\n') {
return false;
}
}
return true;
}
pub fn count_nonempty(lines: []str) -> usize {
var n: usize = 0;
for line in lines {
if (!is_blank(line)) {
n += 1;
}
}
return n;
}
test clamp_edges {
assert(clamp(-3, 0, 10) == 0);
assert(clamp(5, 0, 10) == 5);
assert(clamp(42, 0, 10) == 10);
}
test is_blank_cases {
assert(is_blank(" \t\n"));
assert(!is_blank(" x "));
}
test count_nonempty_mixed {
var lines: [5][]const u8 = ["a", "", " ", "b\n", "\t"];
assert(count_nonempty(&lines) == 2);
}
It ports def clamp(x, lo, hi) and def count_nonempty(lines) and passes
t27c test-report with every test green.
Before you report
t27c parse <spec> > /dev/null && echo parses- it prints the whole tree on success.t27c test-report <spec>- every testpass, nothingFAIL, noBLOCKED. Since
t27c test-report exits 0 when tests FAIL or the report is BLOCKED #7370 it exits 1 on aFAILand 2 onBLOCKED; read the words too.t27c coverage <spec>- every function has a test.git status --short- one line: the file in the Boundary.
The language and the instruments
.t27 is not Rust and not Zig, and a bee fluent in either writes one by
accident. Measured 2026-09-20: a bee filled eight bodies in
specs/file/watcher.t27 with return Ok(());, Err(FileError::WatcherNotFound)
and for i in 0..watchers.length. The parse ratchet refused the whole file --
Unexpected token in expression: RParen, line 123 -- and a spec that does not
parse generates nothing, so every test it already carried stopped running.
At the top level the parser accepts exactly these eight forms, with an optional
pub, and no others:
const var fn enum struct test invariant bench
There is no trait, no impl, no type X = ..., no generics, no macro. What
a bee reaches for from another language, and what happens:
- Rust's result sugar (
Ok(()),Err(E::V),?) is not a construct here. - Rust macros (
println!,format!,vec!) are not constructs here. - A range loop (
for i in 0..n) is Rust and Zig, not this language. - Zig builtins (
catch unreachable,@intCast,@constCast) are not constructs here. - Cross-module reuse (
use other::fn;) parses and then generates a comment and an unqualified call, so the Zig fails withuse of undeclared identifier(use a::b; generates no import, so no spec can reuse a function from another spec #4298). It does not work yet.
t27c parse <spec> answers in one line whether what you wrote is the language.
Run it before you report, every time.
t27c spec-status <spec>- the compiler's one-word verdict: IMPLEMENTED, PARTIAL, UNWRITTEN, NOPARSE, NOFN. Exit code is 0 whatever it says, so read the word.t27c symbols <spec>- every name the file declares, with its kind. Answers "does this already exist here?" before you add it.t27c outline <spec>- per function: its locals, what it calls, what it returns. The contract you are implementing against.t27c coverage <spec>- which functions have a test and which do not. The issue asks for tests; this is how you check you wrote them.t27c lint <spec>- style and shape warnings. It OVER-REPORTShas no test or invariant: measured 2026-09-20 over 60 specs it printed 677 of those wherecoveragefound 190 untested functions, disagreeing on 59 of the 60 - it warns aboutbit_to_trit_pairinspecs/base/ternary_encoding.t27, whichtest bit_to_trit_pair_zerocalls on line 249. Read it as a hint;coverageis the answer.t27c typecheck <spec>- types, before generation. PrintsTypecheck OK (0 errors, 0 warnings)or the errors.t27c test-report <spec>- builds this spec and runs its own tests, which is what the oracle does.BLOCKEDmeans the generated Zig does not compile, with the error beside it.python3 tools/dupe_scan.py --name <function>- where that function already lives, if it does. 576 of 4021 bodies here are byte-identical copies.
The original: fpga/openxc7-synth/riscv_blink.v
gHashTag/trinity at 8723fe0a725a, 101 lines, quoted verbatim behind a line-number gutter.
1| //! Strand III: Language \& Hardware Bridge
2| //!
3| //! FPGA component for Trinity S³AI — synthesizable Verilog module.
4| //!
5|
6| `default_nettype none
7|
8| // ═══════════════════════════════════════════════════════════════════════════════
9| // TRINITY RISC-V BLINK — Simple State Machine LED Blinker
10| // ═══════════════════════════════════════════════════════════════════════════════
11| //
12| // Demonstrates autonomous FPGA operation without external programming.
13| // Uses a simple state machine to blink LED in multiple patterns.
14| //
15| // ═══════════════════════════════════════════════════════════════════════════════
16|
17| module trinity_top (
18| input wire clk, // 50 MHz oscillator @ U22
19| output wire led // LED D5 @ T23 (active LOW)
20| );
21|
22| //==========================================================================
23| // STATE MACHINE
24| //==========================================================================
25| localparam STATE_IDLE = 3'd0;
26| localparam STATE_BLINK_ON = 3'd1;
27| localparam STATE_BLINK_OFF = 3'd2;
28| localparam STATE_PATTERN = 3'd3;
29| localparam STATE_CHAOTIC = 3'd4;
30|
31| reg [2:0] state, next_state;
32|
33| //==========================================================================
34| // COUNTERS
35| //==========================================================================
36| // 50 MHz / 2^26 = ~0.75 Hz (slow blink)
37| // 50 MHz / 2^24 = ~3 Hz (medium blink)
38| // 50 MHz / 2^20 = ~48 Hz (fast blink)
39|
40| reg [25:0] counter;
41| reg [19:0] fast_counter;
42| reg [31:0] lfsr; // Linear Feedback Shift Register for pseudo-random
43|
44| // LFSR for chaotic mode (32-bit Xorshift variant)
45| wire [31:0] lfsr_next;
46| assign lfsr_next = {lfsr[30:0], lfsr[31] ^ lfsr[21] ^ lfsr[1] ^ lfsr[0]};
47|
48| always @(posedge clk) begin
49| counter <= counter + 1'd1;
50| fast_counter <= fast_counter + 1'd1;
51| lfsr <= lfsr_next;
52| end
53|
54| //==========================================================================
55| // MODE DETECTION
56| //==========================================================================
57| // Auto-cycle through modes every ~2 seconds (50MHz * 2^26)
58| wire mode_change = (counter == 26'd0);
59| reg [1:0] mode;
60| always @(posedge clk) begin
61| if (mode_change)
62| mode <= mode + 1'd1;
63| end
64|
65| //==========================================================================
66| // STATE MACHINE LOGIC
67| //==========================================================================
68| reg led_int;
69|
70| always @(*) begin
71| case (mode)
72| 2'd0: begin // Slow blink (~0.75 Hz)
73| led_int = counter[25];
74| end
75| 2'd1: begin // Fast blink (~48 Hz)
76| led_int = fast_counter[19];
77| end
78| 2'd2: begin // Chaotic (LFSR-based)
79| led_int = lfsr[20] ^ lfsr[10] ^ lfsr[5];
80| end
81| default: led_int = 1'b0;
82| endcase
83| end
84|
85| //==========================================================================
86| // LED OUTPUT (active LOW)
87| //==========================================================================
88| assign led = ~led_int; // Invert for active-low LED
89|
90| //==========================================================================
91| // INITIALIZATION
92| //==========================================================================
93| initial begin
94| state = STATE_IDLE;
95| counter = 26'd0;
96| fast_counter = 20'd0;
97| lfsr = 32'hDEAD_BEEF; // Seed value
98| end
99|
100| endmodule
Activity
docs: the coordination entry this branch needs to land
gHashTag commentedon Oct 11, 2026
Completed by #8469 (merged 2026-10-10T08:55:29Z into master as 7f19d7e). That PR lists this issue in its closing references, but GitHub did not close it automatically.
Closed with evidence from #9021, so the Queen tick stops re-reading it as a
completedskip.