Skip to content

Port gHashTag/trinity:fpga/openxc7-synth/d_oscillator.v (Verilog, 1 module) to specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 #5877

Description

@github-actions

Port gHashTag/trinity:fpga/openxc7-synth/d_oscillator.v (Verilog, 1 module) to specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27

Roadmap stage 7 of 9: Silicon: Verilog from .t27 (goal #4549). Target of the stage: t27c gen-verilog.

fpga/openxc7-synth/d_oscillator.v (gHashTag/trinity) is 31 lines of hand-written Verilog. Re-author it as specs/port/trinity/fpga/openxc7-synth/d_oscillator.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/d_oscillator.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/d_oscillator.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 32467a72a05f, 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/d_oscillator.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:

  1. line 7 - 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

    1. test -f specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 && echo present prints present (today the file does not exist)
    1. grep -cE '^\s*(pub )?module trinity_top\b' specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 prints 1 - the .t27 module carries the original's name
    1. t27c gen-verilog specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 | grep -cE '^module trinity_top ?\(' prints 1 - the generated Verilog is a module of the same name
    1. t27c spec-status specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 does not print NOPARSE - the file parses
    1. grep -cE '^[[:space:]]*test[[:space:]]+("|[A-Za-z_])' specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 prints at least 1
    1. t27c test-report specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 2>&1 | grep -c BLOCKED prints 0 - the generated code must COMPILE and its tests must run

User Scenarios & Testing

  • Given specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 does not exist and fpga/openxc7-synth/d_oscillator.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/d_oscillator.t27 MUST declare every module listed above under the name given, carrying the original's decisions as code; only data moving in or out may be an undefined; body, and no test may call one.
  • FR-002: the file MUST parse and compile: t27c parse specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 and t27c test-report specs/port/trinity/fpga/openxc7-synth/d_oscillator.t27 are 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 true ports 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 dev is 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 src 1,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.
  • 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 under specs/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-status said
    IMPLEMENTED for 24 of them. Only test-report is 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 fn to export
  • const, var, let (same as const), x += 1, casts n as i64
  • if (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..n only when n is usize; over an i32 it does not compile
  • switch (c) { .Red => 1, .Green => 2, else => 3, } - arms are .Variant, a number, a char, or else
  • 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, } and Point { x: 3, y: -4 }; enum Color { Red, Green, }
  • ?T with null; tuple returns fn divmod(a: u32, b: u32) -> (u32, u32) and const (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; or module 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 error
  • print(...), println!, format!, console.log: undeclared; std.debug.print compiles 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 in specs/ builds today
  • match: parses, and is then DROPPED - the test fails with no error; use switch
  • "\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 test
  • use a::b::fn_name; does not resolve; use a::b; (the file specs/a/b.t27) does. Prefer not to import: say in your report what you would reuse
  • a local string compared with == to a const string: 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

  1. t27c parse <spec> > /dev/null && echo parses - it prints the whole tree on success.
  2. t27c test-report <spec> - read the words, not the exit code, which is 0 either
    way: every test pass, nothing FAIL, no BLOCKED.
  3. t27c coverage <spec> - every function has a test.
  4. 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 with use 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-REPORTS has no test or invariant: measured 2026-09-20 over 60 specs it printed 677 of those where coverage found 190 untested functions, disagreeing on 59 of the 60 - it warns about bit_to_trit_pair in specs/base/ternary_encoding.t27, which test bit_to_trit_pair_zero calls on line 249. Read it as a hint; coverage is the answer.
  • t27c typecheck <spec> - types, before generation. Prints Typecheck 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. BLOCKED means 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/d_oscillator.v

gHashTag/trinity at 32467a72a05f, 31 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| // Using STARTUPE2 internal oscillator
 7| module trinity_top (
 8|     input  wire clk,   // Not used!
 9|     output wire led
10| );
11|     // Internal clock via STARTUPE2
12|     wire clk_int;
13|     reg [23:0] counter = 24'd0;
14|
15|     // Try to use internal configuration clock
16|     // CCLK is available during configuration but may not run after
17|
18|     // Even simpler: just toggle on a slow LUT chain
19|     reg [3:0] slow_chain = 4'b0000;
20|
21|     // Combinatorial "toggle" using LUT cascade
22|     wire [3:0] next_chain;
23|     assign next_chain[0] = ~slow_chain[0];
24|     assign next_chain[1] = slow_chain[0] ^ slow_chain[1];
25|     assign next_chain[2] = slow_chain[1] ^ slow_chain[2];
26|     assign next_chain[3] = slow_chain[2] ^ slow_chain[3];
27|
28|     // Try to make it oscillate combinatorially
29|     assign led = next_chain[0];
30| endmodule

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions