An open agent that designs digital hardware, checks its own work with real tools, and writes down every mistake it makes along the way.
Tecpatl reads a written specification of a circuit, asks a language model for
the Verilog, and then puts that Verilog through two independent gates: a
reference testbench that checks it behaves correctly, and a synthesiser
that checks it could actually be built out of gates and flip-flops. When
either gate rejects the design, the tool's own error message is handed back
to the model and it tries again. Every attempt — the prompt, the broken
Verilog, the exact assertion that rejected it, and the change that followed —
is appended to a JSONL trace in traces/.
The designs are not the point. The traces are.
Two facts sit next to each other and explain most of why language models are worse at hardware than at software.
First: in chip design, checking the work is the work. Writing a module is the small part. Proving it is correct — before a mask set costs millions and a respin costs months — is where the effort goes. The industry's own rule of thumb, repeated in EDA surveys for two decades, puts verification at roughly 70% of the effort on a chip project; teams routinely employ more verification engineers than design engineers. (That is a widely cited figure from the industry, not something this project measured. It is here to say where the difficulty lives, not to be a statistic you can lean on.)
Second: none of that verification knowledge is public. Software escaped this problem decades ago by accident: every bug report, failing test, Stack Overflow answer and fix commit is a public record of a mistake and its repair, and models learned from millions of them. Hardware has no equivalent. The tools cost more than a house, the reference designs live under NDA, and the actual knowledge — what breaks, what the simulator says when it breaks, what you change next — stays inside a handful of companies. You can find plenty of correct Verilog on the internet. You can barely find a single public record of Verilog that was wrong, the exact message the toolchain produced, and the specific edit that fixed it.
That pair — failure, tool verdict, repair — is what Tecpatl records. It is one small, reproducible attempt at building the public record that hardware never got.
A design is only counted as verified when it clears both gates.
tasks/<task>/spec.md ──► model ──► Verilog ──┬──► gate 1 cocotb + Icarus Verilog
▲ │ does it behave?
│ │
│ ├──► gate 2 Yosys
│ │ can it be built?
│ │
│ └──► gate 3 Tiny Tapeout contract
│ would anyone make it?
│ (only for tasks that
│ ask to be taped out)
│ │
└──────── failure report ◄───────────┘
(up to 5 attempts)
every attempt ──► traces/<task>/*.jsonl
Gate 3 is described further down; it applies only to a task that declares it wants to be manufacturable, because most tasks here are exercises in logic rather than submissions.
The second gate is not belt-and-braces. Verilog is a simulation language first, and it will happily simulate a circuit that cannot physically exist. The clearest example lives in this repository as a test fixture: a 4-bit counter that passes all six behavioural tests and is still rejected, because its combinational block does not assign a value on every path, so the synthesiser has to invent four latches to remember the old one. Nobody asked for those latches. In simulation you would never know.
Gate 2 catches that class of defect:
| Kind | What it means |
|---|---|
inferred_latch |
Memory without a clock, created because combinational logic left a path unassigned. |
combinational_loop |
A signal depends on itself with no register between. Hardware cannot settle. |
multiple_drivers |
One wire driven from two places: a short circuit. |
implicit_declaration |
A signal used without being declared, silently created as one bit. |
width_mismatch |
Different widths joined, so bits are dropped or zero-extended. |
An inferred latch is detected from the synthesised cells ($_DLATCH_*),
not from a log warning — Yosys does not consider a latch an error, so
check -assert lets it through and grepping the log for "latch" is fragile.
Synthesis is skipped when simulation fails: a design that does not behave is not worth asking whether it could be built, and the behavioural failure is the more useful feedback anyway.
Everything below is measured from the committed traces in traces/, or from
the CI artifacts of a real build. These four are the results the project has
actually produced so far. Three of them are negative, and they are stated as
they came out.
1. The 7B model has a capacity ceiling, and no error message reaches past
it. Four of the nine recorded compile failures are the same invalid
construct: assign result = case (op) ... endcase. case is a statement in
Verilog and cannot appear on the right-hand side of a continuous assignment.
Shown its own code and iverilog's exact message naming the line, four times
over, qwen2.5-coder:7b wrote the construct back unchanged every time. This is
not a model that was under-informed. It does not know the rule, and one turn
of feedback cannot teach it. Improving the feedback path cannot fix a failure
of this kind, and it did not.
2. Zero of 55 failed attempts were repaired by the attempt that followed. Every design Tecpatl has verified so far passed on its first try; every design that failed its first try never recovered. The repair loop is implemented, exercised and tested, and the corpus of successful repairs it exists to collect is still empty. The failure/verdict half of the record is real; the repair half is not there yet.
3. replay measures the effect of a prompt change by isolating the
model's non-determinism. Re-running a task cannot tell you whether a prompt
change helped, because the second run may simply have started from a different
first design. runner/replay.py rebuilds both retry prompts from the same
recorded failure — identical specification, identical rejected Verilog,
identical underlying failure, differing only in how that failure is described
— sends each to the model, and measures whether the reply compiles. Applied to
the one prompt change this project made on the belief that it mattered, the
answer was that it made no measurable difference. That is the method's first
use and it argued against the change that motivated it.
4. A mutation-tested reference testbench still carried a hidden assumption,
and only real silicon exposed it. The tt_wrapper testbench rejects all six
of its deliberately broken designs and passed every RTL run. It also sampled
outputs one nanosecond after the clock edge, which is only valid where
propagation is instantaneous. Run against the netlist extracted from the built
layout — where the clock tree alone is five cells deep — it failed every
behavioural test on a design that was correct. Nothing in this repository
could have found that. The consequence is worth stating plainly: an RTL pass
in this dataset is not evidence that a design would work as built, and only
one task has been checked that far.
5. A calibration constant applied outside the regime it was measured in rejected a good design twice. The area check scaled Yosys's mapped cell area by 4.39x — a ratio measured on a fifteen-cell counter — to estimate built area. Applied to an 850-cell neuron it predicted 132% of the tile and failed the design, twice, costing two model calls on a problem that was not there. The mapped area was 30% of the tile. The rule now is fail on what is proven, warn on what is estimated: a mapped floor over budget is proof a design does not fit; an extrapolated estimate over budget is not proof of anything.
6. The neuron is correct, manufacturable, fits the tile -- and misses setup
timing at the slow corner, because of the pin protocol rather than the
arithmetic. The built layout passes every manufacturability check and every
gate-level test, and every register-to-register path closes, including through
the multiplier. What does not close is input-to-register: all 19 violating
paths run from ui_in[1] to an accumulator flip-flop. The protocol feeds
din straight into
the multiplier's second operand, so an input pin drives the whole multiplier
and 25-bit saturating adder within one cycle, on top of the 4 ns of external
input delay the flow assumes. Worst slack is -1.39 ns against a 20 ns
period. Nothing in simulation or synthesis could have said this.
7. The repair loop that works runs through the specification, not through an error message. Finding 2 still stands: no failed attempt in this dataset was ever repaired by handing the model its own error and asking again. But the neuron's timing failure was repaired, on the first attempt, by a different loop — measure the built layout, work out what the measurement means, change the specification, and have the agent redesign from it. The model never saw a timing report. It saw a protocol that said both multiplier operands must come from registers, and why. That is the loop this project can actually show working end to end.
8. Relaxing the clock bought timing and not one square micron of area. Doubling the clock period took the worst corner from +0.071 ns to +20.071 ns of setup slack and left standard-cell area, instance count and slew violations byte-identical. The expectation had been that a slacker constraint would let the resizer downsize cells; it did not, because the resizer only ever repairs violations and there were none to repair at either period. Area here is set by synthesis. Shrinking the design needed a synthesis option — Booth encoding for the multiplier, 27% smaller — not a placement or timing knob.
Each of these is worked through with its evidence below: the ceiling and the replay method in Two things that went wrong, the repair count in Where failed attempts died, the testbench assumption in The third gate, the calibration in The neuron, the timing in The neuron in silicon, the fix in Closing the timing, and the margin in Giving it margin.
Eleven tasks, 42 runs across two of the three supported backends. Everything
below comes from the committed traces in traces/; regenerate any of it with
python runner.py stats. (No Gemini runs yet — the backend works, nobody has
harvested with it.)
| Model | Runs | Verified | Failed attempts | Cost per verified design |
|---|---|---|---|---|
claude / sonnet |
25 | 23 | 3 | $0.042 |
claude / haiku |
2 | 2 | 0 | $0.055 |
ollama / qwen2.5-coder:14b |
2 | 0 | 10 | — (local, nothing verified) |
ollama / qwen2.5-coder:7b |
13 | 4 | 42 | $0.00 (local) |
Read that table honestly: the frontier models solve every task here on the first attempt, and contribute nothing to the dataset. Every failure/repair pair Tecpatl has collected so far came from a 7-billion-parameter model running on a laptop GPU. That is a poor result for the task suite and a useful one for the harness — it says the tasks are not yet hard enough, which is the top item on the roadmap.
Costs from the claude provider are reference API prices for the tokens
used. These runs were made on a Claude subscription, so no money changed
hands; the figure is a comparable unit of work, not a bill. Traces record
this as billed: false rather than quietly implying a charge.
| Label | Meaning | Count |
|---|---|---|
logic_bug |
Compiled, and contradicted something the spec stated. | 42 |
syntax_error |
Never compiled; no test ran. | 9 |
synth_fail |
Behaved correctly, but could not be built as gates. | 0 |
assumption_mismatch |
Coherent, but built to a different reading of an underspecified point. | 0 |
no_self_correction |
Shown its own failure, returned the same bytes. | 1 |
Two of those zeros are worth sitting with rather than explaining away. No
synth_fail has yet occurred in a real run — the only ones in this
repository are the hand-written fixtures used to prove the gate works. And no
assumption_mismatch either, despite terse specs written specifically to
provoke one, and despite a deliberate attempt to hunt one down (below).
One more number that is not in any table: across all 52 failed attempts, 0 were repaired by the attempt that followed. Every design Tecpatl has verified so far passed on its first try; every design that failed its first try never recovered. The repair loop is exercised and tested, but the corpus of successful repairs it exists to collect is still empty.
A task is hard for two independent reasons, so Tecpatl separates them.
Difficulty is about the circuit. A counter is easy; an arbiter that must
not starve anyone is hard. Each task declares easy, medium or hard.
Specification level is about the prose. Some tasks ship two specs: a
full one that spells out every case, and a terse one that gives the ports,
the operations and little else. The reference testbench is byte-identical
for both. So the gap between the rows of
python runner.py stats --by-spec-level is not a difference in what the
design had to do — it is a measure of how much a model can reconstruct once
the prose stops telling it.
| Spec level | Runs | Verified | Failed attempts |
|---|---|---|---|
full |
35 | 27 | 30 |
terse |
7 | 2 | 25 |
The terse specs exist to catch a specific phenomenon: a design that is coherent and defensible but built to a different reading of something nobody wrote down. No run has produced one. The suspicion was that the sample was badly chosen at both ends — frontier models simply know the conventions, and a 7B model breaks things too indiscriminately for "coherent but different" to apply — so the obvious test was a model in between.
qwen2.5-coder:14b, run against the terse specs of fifo_8x8 and alu_8bit:
ten attempts, ten failures, still no assumption_mismatch.
It came within one test of it. On fifo_8x8 attempt 2, four tests failed;
three were on the task's declared ambiguity list, and the fourth was
test_reset_is_synchronous. The terse spec says, in as many words,
"rst is synchronous and active high" — and the design was written with
always @(posedge clk or posedge rst). So it contradicted something the spec
did state, which makes it a logic_bug by the rule, and the rule is right:
a design that also breaks a stated requirement is not merely reading an
ambiguity differently.
The honest reading is that a mid-size model was the wrong hypothesis. Failing on what a spec leaves open, while getting everything it states correct, is a narrower target than "a weaker model", and the way to hit it is probably a task whose ambiguity is sharper — one defensible fork, everything else pinned — rather than a different model.
Worth noting for anyone reproducing this: 14B parameters is not a free upgrade. On a 4 GB GPU the model runs mostly out of system RAM at about 3.4 tokens per second, so each attempt takes two to four minutes, and finding this out is what forced the Ollama timeout up from 180 seconds — at the old value a slow model looked exactly like a crashed harness.
Neither of these was a bug in a design. One was a bug in the instrument doing the measuring; the other was a bug in what the measurements were believed to say. Both are here because they are the actual lessons of the project so far.
The first reference testbench written for this project was wrong, and wrong
in the most dangerous direction: it reported a fault in a design that was
fine. A stray en=1 left over from an earlier step meant the counter kept
counting through a test that was supposed to be holding it still.
That would have quietly poisoned the dataset. Every trace produced with it would have recorded a good design as a failure, and anything learning from those traces would learn to "fix" code that was already correct.
So every reference testbench here is mutation-tested, and that discipline is runnable and committed rather than promised in a document. Each task ships hand-written designs with known verdicts: one correct, and several broken in specific, named ways. The harness checks not only that each lands on the right verdict, but that a broken one was rejected by the test that should have caught it — a mutant that fails for an unrelated reason proves nothing.
python -m tests.validate_testbenches # 53 designs, 7 tasks
python -m tests.validate_testbenches counter_4bit # one taskcounter_4bit -- 4-bit counter with enable and synchronous reset
------------------------------------------------------------------------
PASS PASS a correct design passes both gates
PASS SIM_FAIL asynchronous reset is caught by the timing check
PASS SIM_FAIL saturating at 15 is caught by the wrap check
PASS SIM_FAIL ignoring enable is caught by the hold check
PASS SIM_FAIL wrong priority is caught by the priority check
PASS SYNTH_FAIL behaves correctly, but is not buildable
A testbench that has never rejected a wrong design is not a testbench yet.
The first real harvest produced six attempts that never compiled, and not one of them was repaired. The diagnosis at the time was that the feedback was useless.
When a design fails to compile, cocotb's runner raises a
CalledProcessError whose text is the iverilog command line. That
exception was what the retry prompt led with. iverilog's own diagnostics —
alu_8bit.v:11: syntax error — were sitting in the captured log. So
runner/diagnostics.py was written to pull those file:line: message lines
out, strip the temporary path that changes on every attempt, and put them
first in the prompt.
Two things then turned out to be wrong with that diagnosis.
The first is that the diagnostics were never actually missing. The old report
appended the raw log tail after the exception, and the compiler's messages
were in there — under a JSON result blob, a DeprecationWarning, and four
copies of an escaped Windows temporary path. The model was not being kept in
the dark. It was being handed the answer at the bottom of a page of noise.
The honest question is therefore not "shown or not shown" but "does promoting
the message and stripping the noise change whether the next design compiles?"
The second is that re-running the affected tasks cannot answer that question
at all. The model is not deterministic: one re-run of alu_8bit passed on
attempt 1 having never hit a syntax error, which tells you about that
sample and nothing about the prompt.
So runner/replay.py runs the narrow experiment instead. For each recorded
compile failure it rebuilds both retry prompts from the same trace —
identical specification, identical rejected Verilog, identical underlying
failure, differing only in how that failure is described — sends each to the
model, and hands the reply to iverilog. The measurement is whether it
compiles.
python runner.py replay --provider ollama --repeats 3| Model | Compiled with the old message | Compiled with the diagnostics |
|---|---|---|
claude / sonnet (2 repeats) |
10 / 10 | 13 / 13 |
ollama / qwen2.5-coder:7b (3 repeats) |
8 / 21 | 9 / 21 |
The change made no measurable difference. Sonnet repairs every one of these failures either way. The 7B model repairs the same ones either way and fails the same ones either way; one extra success out of twenty-one is noise at this sample size. (Claude's denominators differ because three calls in the "before" arm failed inside the Agent SDK and are not scored against the prompt they were testing. Whether the noisier prompt provoked that is not something seven cases can answer.)
So what did cause six unrepaired compile failures? The traces answer it directly. Four of the six are the same broken construct, and it opens like this:
assign result = case (op)
3'b000: a + b, // ADD
3'b001: a - b, // SUB
...
endcase;case is a statement in Verilog. It cannot appear on the right-hand side
of a continuous assignment. Shown that code and the exact message naming line
11, six times over, qwen2.5-coder:7b wrote the same invalid construct back
every single time. It is not a model that was under-informed. It is a model
that does not know this rule and cannot be told it in one turn.
The change to the feedback path is still right on principle — there is no
argument for leading with a Python traceback when the compiler's own message
is in hand, and the failures report is far more readable for it. But it did
not fix what it was believed to fix, and the belief that it had was itself a
measurement error. That is why replay exists and why this table is in the
README rather than a commit message.
Reading the same traces turned up a second, cheaper problem — and it is the
same six failures again. Attempts 3, 4 and 5 of the alu_8bit run were
byte-identical: the model was shown its own failing design and the tool's
complaint, and returned exactly the same code, three times, each costing a
model call and a full simulation that could only reach the verdict already
sitting one line above it in the trace.
The loop now hashes each returned design and compares it with the previous
one. On a match it records the attempt as NO_PROGRESS / no_self_correction
and ends the run, because re-simulating identical bytes can only re-derive a
verdict already in the trace. run_end records stopped_early and
attempts_saved, so the waste is measured rather than merely avoided. The
first live run to trigger it, fifo_8x8 on the 7B model, stopped at attempt
2 and saved 3.
The comparison is on the exact bytes, deliberately. A model that only reflows whitespace has still done something, and calling that "no progress" would be a judgement rather than an observation.
Simulation asks whether a design behaves. Synthesis asks whether it can be built out of gates. Neither asks the question that decides whether a design becomes a physical chip, which is narrower and completely unforgiving: does it satisfy the fab's contract, and does it fit in the space bought for it?
Tiny Tapeout is the route by which a design this
size can actually be manufactured, and its contract is fixed. The top module
must be named tt_um_* and have exactly eight ports — ui_in, uo_out,
uio_in, uio_out, uio_oe at 8 bits each, plus ena, clk and rst_n —
none optional, none resizable. rst_n is active low, the opposite of
every other task in this repository. uio_oe is per bit, where 1 drives a
pin and 0 releases it. Every output bit must be driven, because an
unassigned output does not read as 0 in silicon; it floats and picks up
whatever the neighbouring project is doing. And the whole thing must fit in
one tile: 161.00 × 111.52 µm, which is 17,954.7 µm² for the cells, the
power grid and every wire between them.
docs/tinytapeout.md documents all of it, with every rule and number traced
to the file it was read from in ttsky-verilog-template and
tt-support-tools rather than recalled.
The three gates are genuinely independent, and the counter proves it: it
passes gates 1 and 2 and would fail gate 3 outright, because its ports are
clk, rst, en, count and Tiny Tapeout will not instantiate that.
The checks differ by orders of magnitude in cost. Reading a port list takes
milliseconds; a real place-and-route takes a PDK, a container runtime and
tens of minutes. Collapsing both into one TT_FIT would make the verdict a
claim of unknown strength, so runner/ttfit.py records which tier produced
it and names every tier it skipped, with the reason.
| Tier | What it proves | Needs |
|---|---|---|
contract |
The port list, that every output bit has a driver, and that the project metadata satisfies tt-support-tools. |
Yosys |
area |
Standard-cell area in µm² from the PDK's own liberty file, against the tile. | the SKY130 liberty |
build |
That it actually places, routes and passes the precheck. | a PDK and a container runtime |
The area tier is a lower bound and is documented as one: it counts cells
and not the routing between them, so it can prove a design is too big and can
never prove it fits. Only the build tier decides that. A trace that says
"tier": "contract" and a trace that says "tier": "build" are both
TT_FIT, and they are not the same claim.
Undriven output bits are found through Yosys rather than by reading the
source: a bit is driven if it is a constant, the output of some cell, or an
input port, and anything else reaching an output is floating. That resolves
correctly through assign statements, module instances and optimisation,
which a regular expression over the Verilog would not.
tasks/tt_wrapper hands the agent a finished 4-bit counter and asks it to
wrap it: invert the reset, put the enable on ui_in[0], the count on
uo_out[3:0], drive the other twenty unused output bits low. Its reference
testbench is mutation-tested like every other, against six designs:
tt_wrapper -- Tiny Tapeout wrapper for a 4-bit counter
------------------------------------------------------------------------
PASS PASS a correct wrapper passes both gates
PASS SIM_FAIL not inverting rst_n is caught by the reset polarity check
PASS SIM_FAIL the count on the wrong nibble is caught by the unused-bit check
PASS SIM_FAIL a resized output port is caught by the port contract check
PASS SIM_FAIL leaving unused output bits floating is caught by the same check
PASS SIM_FAIL driving unused bidirectional pins is caught by the uio_oe check
Worth being straight about what that table shows: a strict enough testbench catches most contract violations by itself. Five of the six mutants are rejected by gate 1, not gate 3. So the argument for a third gate is not that the first cannot be made to cover this — it is that the third generalises where the first does not. The testbench is nine hand-written tests for one task; the gate is the shuttle's rules, applied to any task, including the rules no simulation can see: the module's name, the project metadata, and the tile.
There is one defect that only gate 3 catches, and the offline test is built around it: a wrapper that is correct in every way but has one extra output port. The reference testbench drives the eight contract ports and every assertion holds. Yosys is content, since an extra output is perfectly buildable. Only the rule "exactly eight ports, no more" rejects it:
attempt 1 correct counter, correctly wrapped, one extra port
-> SIM_PASS, SYNTH_PASS, then TT_FAIL / tt_contract
attempt 2 the same design without it -> PASS
That is also the first failure in this repository that the repair loop fixes on the next attempt — scripted rather than harvested, so it belongs in the test suite and not in the results table above.
runner/ttproject.py packages a design that cleared all three gates into the
directory Tiny Tapeout's own tooling expects:
python runner.py tt-emit tasks/tt_wrapperThe Verilog is lifted from the trace, not retyped. What gets packaged is
the exact bytes that passed the gates, so there is no step in which a
verified design quietly becomes a slightly different unverified one. The
scaffolding it cannot generate — src/config.json, test/tb.v,
test/Makefile, the CI workflows — is vendored unchanged from the official
template, with the shuttle tag it came from recorded in the file.
The emitted project was pushed to
tecpatl-tt-counter,
where Tiny Tapeout's own workflow built it. All three CI jobs pass:
| Job | What it does | Result |
|---|---|---|
gds |
LibreLane hardens the design against sky130A |
success |
precheck |
Magic and KLayout DRC, boundary, layers, power pins, cell names | success |
gl_test |
Re-runs the cocotb tests against the netlist extracted from the built layout | 9/9 pass |
The numbers the flow reports, which are the first in this repository that describe a physical object rather than a simulation:
| Metric | Value |
|---|---|
| Die area | 0.0 0.0 161.0 111.52 µm — 17,954.7 µm² |
| Standard cells | 631.9 µm², 3.8% utilisation |
| Instances placed | 4,637 (mostly decap, fill and tap cells) |
| Setup slack, worst corner | +14.14 ns against a 20 ns target |
| Hold slack, worst corner | +0.14 ns |
| Lint errors, slew/fanout/cap violations | 0 |
That die bounding box is worth a second look: 161.0 × 111.52 is exactly the
1x1 tile size this README quotes from tt-support-tools, which means the
number documented in docs/tinytapeout.md and the number the flow actually
used are the same number. The documentation was checked against the tool, not
just against the source it was read from.
gl_test passing is the strongest check in the whole project. Gate 1 tests
what the model wrote. This tests what was built — the same nine assertions,
against a netlist of sky130_fd_sc_hd cells with real delays.
Nothing has been submitted. Building a GDS and entering a shuttle are different actions, and only the first has happened.
It did not pass first time. The first build succeeded and the precheck
passed, and then gl_test failed every behavioural test — while the design
was correct.
The testbench read uo_out one nanosecond after each rising clock edge. At
RTL that is fine, because propagation is instantaneous. The gate-level
netlist is compiled with -DUNIT_DELAY=#1, so every cell costs a nanosecond,
and the built design's clock tree turned out to be up to five cells deep —
3x clkbuf_16, 2x clkdlybuf4s25_1 — before the flip-flops see the edge at
all. Reading at +1 ns returned the previous value, or X before the flops
had ever been clocked. The fix was to sample at +16 ns of a 40 ns period,
a figure measured by counting the clock tree in the netlist from the CI
artifact rather than guessed at.
A reference testbench can be strict, mutation-tested, and still wrong. This one rejects all six of its mutants and passed every RTL run, and it carried a hidden assumption — zero propagation delay — that nothing in this repository was capable of exposing. Only running it against a real netlist did. That is the same lesson as the counter testbench that accused a correct design, arriving from the opposite direction: the first was too strict, this one was not testing what it claimed to.
The other seven testbenches share the assumption. They are left alone, because
no netlist is ever built for them and their traces are already committed — but
the limitation is real, and it means an RTL pass in this dataset is not
evidence that the design would work as built. Only tt_wrapper has been
checked that far.
The first design in this repository aimed at actually being manufactured is an artificial neuron: a weighted sum of inputs followed by a non-linearity. It was built in four layers, each one verified before the next was written, and each one handed to the agent as a given it must not modify.
| Layer | Task | What the agent was asked for | Gates cleared |
|---|---|---|---|
| 1 | mac_8bit |
The arithmetic: signed 8x8 multiply, accumulate, saturate | SIM 9/9, SYNTH |
| 2 | neuron_relu |
A ReLU on the given MAC | SIM 9/9, SYNTH |
| 3 | tt_neuron |
The given neuron on Tiny Tapeout pins, with a protocol | SIM 12/12, SYNTH, TT_FIT |
Every layer passed on its first attempt. Layers 1 and 2 are not tt_um_
modules, so gate 3 does not apply to them — the contract is a property of the
top-level submission, not of an internal block. Only layer 3 faces all three.
The operands are signed. A ReLU that only ever sees positive numbers is
not doing anything. Signed also makes the arithmetic sharper than it looks:
the largest-magnitude 8x8 product is -128 × -128 = +16384, which is
positive, and two's complement is not symmetric.
The accumulator is 24 bits. A single product needs 16. Accumulating needs
headroom, and 24 bits gives 8388607 / 16384 — more than 512 accumulations
of the largest possible product before saturation is even reachable.
Overflow saturates; it must not wrap. This is the decision that matters
most, and it matters because of the ReLU. If a large positive total wrapped
to a negative number, the ReLU downstream would output 0 — the strongest
possible activation reported as no activation at all, silently. Saturation
keeps a too-large result too large. The spec says so, and the testbench drives
the accumulator to both limits and past them to check it.
Two 8-bit operands, a command and a 24-bit result do not fit on 8 input pins and 8 output pins in one cycle, however they are arranged. So operands are loaded by turns and the result is read back a byte at a time:
| Pins | Name | Meaning |
|---|---|---|
ui_in[7:0] |
din |
Data byte; its meaning depends on cmd |
uio_in[1:0] |
cmd |
00 NOP, 01 LOAD_A, 10 MAC, 11 CLEAR |
uio_in[3:2] |
sel |
Result byte: 0 low, 1 middle, 2 high, 3 reads zero |
uo_out[7:0] |
dout |
The selected byte of the activation |
One multiply-accumulate is two cycles — LOAD_A with a, then MAC with
b — and repeated MAC commands reuse the stored a, so a neuron summing
many products loads its weight once. All eight bidirectional pins are
released (uio_oe = 0); they carry only control in.
There is no acknowledge line, and none is needed. The design is synchronous to the same clock the commands arrive on, so a command sampled on an edge has taken effect by the next one. A request/acknowledge handshake would only be necessary across clock domains, and inventing one here would be protocol for its own sake.
Cell areas come from the SKY130 liberty file — the same tt_025C_1v80 corner
the build uses — and the delay is ABC's figure for the longest combinational
path through the mapped logic.
| Design | Cells | Mapped area | % of tile | % of the 60% budget | Logic path |
|---|---|---|---|---|---|
tt_wrapper (counter, for scale) |
15 | 143.9 µm² | 0.8% | 1.3% | 0.22 ns |
mac_8bit |
642 | 4,787.1 µm² | 26.7% | 44.4% | 6.51 ns |
neuron_relu |
665 | 4,930.0 µm² | 27.5% | 45.8% | 5.42 ns |
tt_neuron |
720 | 5,420.0 µm² | 30.2% | 50.3% | 6.03 ns |
A 1x1 tile is 17,954.7 µm², of which the flow aims to fill 60% (10,772.8 µm²).
Headroom on area: the neuron's mapped cells are half the density budget. The multiplier dominates — 4,787 of the 5,420 µm² is the MAC, so the ReLU and the whole pin protocol together cost about 12% of the design.
Headroom on timing: the logic path is 6.03 ns against a 20 ns period at 50 MHz. Adding the 5.63 ns of clock-tree, wire and setup overhead measured on the built counter gives roughly 11.7 ns, leaving about 8 ns of slack. Read that as an order of magnitude and not a number: the overhead was measured on a fifteen-cell design and will be larger here.
Both figures are floors, and neither has been built. Generating the GDS is the next step and is deliberately separate.
This did not go smoothly, and the fault was in the instrument.
The area check originally scaled the mapped area by 4.39x to estimate the built area — a ratio measured honestly, but measured on the fifteen-cell counter, where a clock tree and twenty tie cells account for most of the expansion. Applied to an 850-cell neuron it predicted 132% of the tile and failed the design. Twice. Two model calls spent on a problem that did not exist, with feedback telling the model to use fewer gates when its mapped area was 30% of the tile.
The mistake was not the measurement; it was using an extrapolation as grounds for a hard failure. The rule now is:
- Fail on what is proven. If the mapped area alone is over the budget, the design cannot fit, because mapped area is a floor. That is a real rejection.
- Warn on what is estimated. A scaled estimate over budget is reported prominently and does not fail the gate. Only the build settles it.
Timing is reported and never gated on at all, for the same reason.
There is a general lesson here that cost real attempts to learn: a calibration constant is only evidence inside the regime it was measured in. The 4.39x figure is still in the code, still documented, still useful for saying "this will grow considerably" — it just no longer gets to reject anything on its own.
The neuron was pushed to
tecpatl-tt-neuron
and built by Tiny Tapeout's own flow. All three CI jobs pass:
| Job | Result |
|---|---|
gds — LibreLane hardens the design against sky130A |
success |
precheck — 15 manufacturability checks |
success, 0 DRC violations |
gl_test — the 12 tests against the extracted netlist |
12/12 |
| Metric | Neuron | Counter, for scale |
|---|---|---|
| Standard cells | 9,475.3 µm² | 631.9 µm² |
| Utilisation | 57.4% | 3.8% |
| Instances placed | 3,222 (1,121 standard cells) | 4,637 |
| Die | 161.0 × 111.52 µm | same |
It fits, with 2.6 points to spare against the 60% density the flow targets.
That number also settles the argument from the previous section. Locally the design mapped to 5,420 µm²; built, it is 9,475 µm² — an expansion of 1.75x, not the 4.39x measured on the fifteen-cell counter. The ratio falls steeply with size, exactly as predicted, because the expansion is mostly the fixed cost of a clock tree and tie cells. Had the gate kept failing on the extrapolated 4.39x estimate, it would have rejected a design that fits.
This is the part worth reading carefully, because the headline is bad and the detail is not.
| Corner | Setup slack | Hold slack |
|---|---|---|
ff_n40C_1v95 (fast) |
+10.23 ns | +0.11 ns |
tt_025C_1v80 (typical) |
+7.16 ns | +0.31 ns |
ss_100C_1v60 (slow) |
−1.14 ns | +0.82 ns |
| Worst across all | −1.39 ns | +0.11 ns |
Setup does not close at the slow corner: 57 violating endpoints, −18.5 ns of total negative slack. Hold closes everywhere, with zero violations.
But the question that matters is which paths fail. The post-layout report lists the 40 worst paths at the slow corner, of which 19 actually violate, and they are all the same shape:
19 violating ui_in[1] -> an accumulator flip-flop
0 violating from any flip-flop
0 violating sel -> uo_out (+9.16 ns of slack)
0 violating cmd -> accumulator
Not one violation starts at a flip-flop. Register-to-register timing closes completely, through the multiplier and the saturating adder included — the arithmetic that looked like the risk is not the problem. Nor is the byte-select read path, which has nine nanoseconds to spare.
The problem is a decision in the pin protocol. din is wired straight to the
multiplier's second operand, so an input pin drives the entire 8×8 multiplier,
the 25-bit adder and the saturation logic within a single cycle. The worst
path reads:
Startpoint: ui_in[1] (input port clocked by clk)
Endpoint: _1506_ (rising edge-triggered flip-flop)
4.000 ns input external delay <- assumed, before the pin
17.350 ns through the multiplier and adder to the flop
21.350 ns data arrival, against a 20 ns period
A fifth of the budget is gone before the signal reaches the chip: the flow assumes 4 ns of external input delay. The remaining 17.35 ns is the combinational path the protocol created.
The fix is architectural, not arithmetic. Registering din into a b
operand register — making MAC latch the operand and accumulate on the
following cycle — would turn this input-to-register path into a
register-to-register one, and those already close with 7 ns to spare at the
typical corner. It costs one cycle of latency per multiply-accumulate and a
change to the protocol, which is a design trade-off rather than a bug fix.
Also recorded: 254 max-slew violations, and no fanout or capacitance violations. The counter had none of any kind.
None of this is visible from simulation or synthesis. It took a real place-and-route against a real PDK, at three process corners, to find out that the thing to fix is the interface.
Day 6 lost a CI run to a testbench that sampled too early for gate-level delays. This time the period was checked before pushing, by measuring the mapped netlist directly:
yosys ... ltp -noff
counter 13 logic levels
neuron 269 logic levels
Under -DUNIT_DELAY=#1 every cell costs a nanosecond, so the neuron's path
takes about 269 ns in gate-level simulation — while taking about 6 ns in
silicon. Unit-delay gate-level simulation is not a timing simulation; it
is a functional check with artificial delays, and a deep-but-fast design looks
catastrophically slow in it. The counter's 40 ns period would have failed
every test on a correct design. The period was set to 800 ns from that
measurement, and gl_test passed first time.
Nothing has been submitted. Building a GDS and entering a shuttle remain different actions, and only the first has happened.
The diagnosis from the built layout said the problem was the pin protocol, not
the arithmetic. So the protocol changed: MAC now captures din into a b
register and arms the multiply, and the accumulator updates on the following
edge from two registered operands. The path through the multiplier became
register-to-register — which had always closed with seven nanoseconds to spare.
The spec, the reference testbench and the mutant set were rewritten for the new protocol and the agent redesigned from them. No Verilog was hand-patched. It cleared all three gates on the first attempt.
Rebuilt on the pipelined branch, all three CI jobs pass again — and setup now
closes at every corner:
| Corner | v1 setup slack | v2 setup slack |
|---|---|---|
ff_n40C_1v95 (fast) |
+10.23 ns | +10.83 ns |
tt_025C_1v80 (typical) |
+7.16 ns | +9.79 ns |
ss_100C_1v60 (slow) |
−1.14 ns | +0.37 ns |
| Worst across all corners | −1.39 ns | +0.07 ns |
| Setup violations | 57 | 0 |
| Setup TNS | −18.55 ns | 0 |
| Hold violations | 0 | 0 |
The critical path is now _1533_ → _1522_ — flip-flop to flip-flop. Not one
path in the report starts at a pin any more, where the previous build had 19
violations all running from ui_in[1] into the accumulator. The transformation
did exactly what the post-layout data said it would.
The honest headline is not "timing closed", it is "timing closed with almost
nothing to spare". The worst corner (max_ss_100C_1v60, slow process with
pessimistic RC) has +0.0715 ns of slack on a 20 ns period — 0.36% margin.
Utilisation is tight in the same way: 59.7% against the 60% the flow aims for. The pipeline cost one 8-bit register and one bit of state, taking the design from 9,475 to 9,852 µm².
Both numbers are inside their limits and neither has room for another feature. A second accumulator, a wider operand, or anything else added to this tile would need something else removed.
The rebuild left them essentially unchanged — 254 before, 249 after — which is itself the answer. They are not caused by the input path, so fixing the input path did nothing for them.
Where they actually come from is visible per corner:
| Corner | Max-slew violations |
|---|---|
ff_n40C_1v95 (fast) |
0 |
tt_025C_1v80 (typical) |
11 |
ss_100C_1v60 (slow) |
219–249 |
They are a slow-corner phenomenon: at 100 °C and 1.60 V transitions are
simply slower, and 249 pins exceed the 0.75 ns limit, the worst reaching
1.27 ns. They also cluster into groups sharing a slew almost to the picosecond
— 1.26690, 1.26694, 1.26691, … — which is the signature of a handful of
high-fanout nets, not 249 independent problems. The operand registers
broadcast each bit into the multiplier's partial-product rows, and the flow has
already inserted its own buffers (fanout57/X appears among the violators).
Three things say to report these rather than chase them:
- Max-cap and max-fanout violations are both zero. The flow does not consider the loading itself illegal; it is the transition time at the worst corner.
- Timing already accounts for them. The +0.0715 ns of slack was computed with these actual slews, not with ideal ones. They are not hidden risk.
- Fixing them means editing
src/config.json, which the template says in capitals not to edit. Buying slew margin by changing the hardening configuration is a different kind of decision from changing a design.
They are a real signoff warning on a design that is dense and runs a wide datapath at the slow corner, and they are written down as one.
Nothing has been submitted. The design now closes timing at every corner, which is the precondition for that conversation — not the conversation itself.
The pipelined neuron closed timing by 71 picoseconds and sat at 59.7% utilisation against a 60% target. Both inside their limits, neither with room for anything. Two changes were made to buy margin, and only one of them did what was expected.
CLOCK_PERIOD went from 20 ns to 40 ns — 50 MHz to 25 MHz. This is the one
knob the template invites a project to turn; its own comment says to increase
it when setup is violated.
It worked completely for timing, and did nothing whatsoever for area:
| 20 ns | 40 ns | |
|---|---|---|
| Setup slack, worst corner | +0.071 ns | +20.071 ns |
| Standard-cell area | 9,851.95 µm² | 9,851.95 µm² |
| Utilisation | 59.7% | 59.7% |
| Max-slew violations | 249 | 249 |
Byte-identical to the last digit. The expectation going in was that a relaxed constraint would let the resizer stop upsizing for drive strength and the design would shrink as a side effect. That expectation was wrong. The area of this design is set by synthesis, not by the timing constraint: the resizer only ever needed to fix violations, and there were none to fix at either period.
Lowering PL_TARGET_DENSITY_PCT cannot lower utilisation. Utilisation is
standard-cell area over core area, and the core is fixed absolutely by the
tile. The density target tells the placer how tightly to pack, not how big the
cells are. Setting it below the actual utilisation would make global placement
fail — which is exactly what the config's own comment warns about from the
other direction.
So the design had to get smaller, and the lever came from reading what the
flow actually accepts rather than guessing. resolved.json in the build
artifact lists every configuration variable in force, and two of them trade
delay for area — precisely the trade the relaxed clock had just made
affordable:
SYNTH_STRATEGY = AREA 0 already the most area-oriented setting
SYNTH_MUL_BOOTH = False Booth encoding for multipliers, off
SYNTH_ADDER_TYPE = YOSYS a ripple-carry adder would be smaller
Booth encoding was measured locally against the same standard-cell library before spending a CI run on it: 5,530 → 4,013 µm², 744 → 502 cells, 27% smaller.
| Metric | Day 9 | Relaxed clock | + Booth |
|---|---|---|---|
| Setup, fast corner | +10.83 ns | +22.83 ns | +22.93 ns |
| Setup, typical corner | +9.79 ns | +22.33 ns | +22.47 ns |
| Setup, slow corner | +0.37 ns | +20.37 ns | +20.38 ns |
| Setup, worst of nine corners | +0.07 ns | +20.07 ns | +20.18 ns |
| Setup violations | 0 | 0 | 0 |
| Hold violations | 0 | 0 | 0 |
| Standard-cell area | 9,851.95 µm² | 9,851.95 µm² | 6,960.43 µm² |
| Utilisation | 59.7% | 59.7% | 42.2% |
| Max-slew violations | 249 | 249 | 138 |
Timing: 71 ps of margin became 20.18 ns — half the clock period. The
critical path is still flip-flop to flip-flop (_0989_ → _0963_); 24 of the 49
reported paths start at a pin and not one of them violates.
Area: 59.7% → 42.2%, comfortably under the 55% asked for, with 6,960 µm² of a 16,493 µm² core. Instance count went up, from 3,130 to 3,577, because the freed space is filled with decap and fill cells — that is what filling a tile looks like, not a regression.
Slew: 249 → 138, and where they are matters:
| Corner | Max-slew violations |
|---|---|
ff_n40C_1v95 (fast) |
0 |
tt_025C_1v80 (typical) |
0 |
ss_100C_1v60 (slow) |
83–138 |
The typical corner is now clean, where it had 11 before. What remains is entirely the slow corner — 100 °C at 1.60 V, where transitions are inherently slower. Max-cap and max-fanout violations are zero at every corner, and the +20.18 ns of slack was computed with these actual slews, so they are a signoff warning rather than hidden risk.
And gl_test is green. Booth encoding is a different way to compute the same product, and the twelve behavioural assertions re-run against the extracted netlist are what holds that claim to account rather than asserting it.
The cost, stated plainly: maximum clock frequency halved, 50 MHz to 25 MHz. The protocol is cycle-based and synchronous, so that changes how fast the neuron runs and never what it computes.
Nothing has been submitted.
A task is a directory with three files:
| File | Written by | Purpose |
|---|---|---|
spec.md |
a human | The specification. The only thing the agent ever sees. |
test_<name>.py |
a human | The reference cocotb testbench. Ground truth. |
task.json |
a human | Module name, test module, timescale, difficulty, spec levels, and optionally the Tiny Tapeout metadata that opts the task into gate 3. |
The agent is never shown the testbench, so it cannot write code that targets the checks instead of the specification.
| Task | Difficulty | What it exercises |
|---|---|---|
counter_4bit |
easy | Synchronous reset, enable, priority, wrap-around. |
shift_reg_8bit |
easy | Serial-in/parallel-out shifting, plus a combinational output that must not be registered. |
seq_detect_1011 |
easy | A state machine with overlapping matches and a registered output. |
bcd_counter_2 |
medium | Two decimal digits, each bounded at 9, with a carry between them and a combinational carry out. |
fifo_8x8 |
hard | A queue whose pointers coincide for two opposite reasons, refused reads and writes at the boundaries, and wrap-around. |
alu_8bit |
hard | Carry versus overflow, the subtraction borrow convention, and arithmetic flags that must stay clear during logic operations. |
arbiter_rr4 |
hard | One grant per cycle, rotating priority, no starvation, and a rotation that survives an idle cycle. |
tt_wrapper |
medium | Wrapping a given design in Tiny Tapeout's tt_um_ interface: an inverted reset, a fixed eight-port list, and every unused output bit driven. |
mac_8bit |
medium | Signed 8x8 multiply-accumulate into a 24-bit accumulator that saturates rather than wraps. Signed arithmetic, headroom, and the asymmetry of two's complement. |
neuron_relu |
medium | A ReLU on the accumulator: sign read from the top bit, threshold exactly zero, output combinational rather than registered. |
tt_neuron |
hard | The neuron on Tiny Tapeout pins, with a command protocol that loads operands by turns and reads a 24-bit result back one byte at a time. |
Whether a failure is "a bug" or "a different but defensible reading" is a
judgement about intent, and no simulator log carries intent. So it is not
inferred. Each task declares, under ambiguity in its task.json, which
of its own tests probe points a given spec level leaves open. An attempt
earns the label only when every failing test is on that declared list; if
it also broke something the spec did state, it is a logic_bug. Under a
full spec the list is empty by construction.
The label is therefore a claim by the task author, sitting in a file where a reviewer can argue with it, rather than a guess the tooling invented.
- Python 3.10+
- The OSS CAD Suite,
which bundles Icarus Verilog, Yosys and GTKWave in one portable directory.
Extract it to
~/oss-cad-suite, or pointTECPATL_OSS_CAD_ROOTat it. Nothing needs to go on your systemPATH; Tecpatl handles that itself. - A model backend: a Claude Code login, a free Gemini key, or a local Ollama.
python -m venv .venv
# Windows: .venv\Scripts\activate
# Linux/macOS: source .venv/bin/activate
pip install -r requirements.txt
python runner.py --check-toolchain # confirm the hardware tools were foundpython runner.py tasks/counter_4bit # design and verify one task
python runner.py tasks/fifo_8x8 --spec-level terse
python runner.py sweep # every task, every backend
python runner.py stats # summarise the dataset
python runner.py failures # every failure and its repair
python runner.py replay # the feedback-path experiment
python runner.py preflight --model qwen2.5-coder:14b --size-gb 9
python runner.py tt-emit tasks/tt_wrapper # package it for Tiny Tapeouttask : seq_detect_1011 (easy, full spec)
provider : claude (sonnet)
trace : traces/seq_detect_1011/20260830T064623Z-claude-full.jsonl
attempts : up to 5
--- attempt 1/5 ---
generating Verilog ...
gate 1/2 simulating ...
SIM_PASS (9/9 tests)
gate 2/2 synthesising ...
SYNTH_PASS (16 cells)
PASS on attempt 1 (SIM_PASS + SYNTH_PASS)
A task that declares Tiny Tapeout metadata faces the third gate as well:
--- attempt 1/3 ---
generating Verilog ...
gate 1/3 simulating ...
SIM_PASS (9/9 tests)
gate 2/3 synthesising ...
SYNTH_PASS (11 cells)
gate 3/3 checking the Tiny Tapeout contract ...
TT_FIT (tier: contract, 4 checks)
PASS on attempt 1 (SIM_PASS + SYNTH_PASS + TT_FIT (contract tier))
| Sub-command | What it does |
|---|---|
<task dir> |
One run. --provider, --model, --spec-level, --max-attempts. |
sweep |
The providers × tasks × spec-levels matrix. A backend with missing credentials is skipped with a reason instead of aborting the harvest. |
stats |
--by-task (default), --by-provider, --by-spec-level. Reports today's spend separately, which is what you watch when your model access is a monthly allowance. |
failures |
Groups by label and provider. --task, --provider, --kind, --spec-level, --verdict, --diff-lines N, --json. |
replay |
Re-asks recorded compile failures with both styles of feedback. Writes no traces. |
preflight |
Where Ollama actually stores models, and whether there is disk room for another. |
tt-emit |
Packages a design that cleared all three gates as a Tiny Tapeout project directory. Emits; never submits. |
It exists because of a specific trap. OLLAMA_MODELS is read by the Ollama
server process at startup, not by the shell you type ollama pull into. If
you point it at a roomier drive after the server is already running, nothing
changes and the download lands on your system disk. preflight asks the
running server where its models actually are rather than trusting the
environment variable, and refuses to say "OK" without the free space to back
it.
Chosen with TECPATL_PROVIDER or --provider. All three implement the same
one-method interface in agent/provider.py; adding a fourth is one subclass
and one registry line.
| Provider | Authentication |
|---|---|
claude (default) |
Your Claude Code CLI login. Run claude once to sign in. |
gemini |
GEMINI_API_KEY from Google AI Studio (free tier). |
ollama |
None. Talks to a local Ollama server; qwen2.5-coder:7b by default. |
On ANTHROPIC_API_KEY. The Claude Code CLI prefers that variable over
your subscription login, without saying so, and the run gets billed to the
API account instead. The claude provider therefore refuses to start when it
is set. Unset it, or set TECPATL_ALLOW_API_KEY=1 if API billing is what you
want.
Other variables: TECPATL_CLAUDE_MODEL, TECPATL_GEMINI_MODEL,
TECPATL_OLLAMA_MODEL, TECPATL_OLLAMA_HOST,
TECPATL_OLLAMA_TIMEOUT_S (default 1800).
A local model that does not fit in your GPU generates from system RAM at a few tokens a second, so a single module can take many minutes. The Ollama backend therefore waits far longer than the cloud ones do — waiting on a model running on your own machine costs nothing, and a short timeout turns "slow" into "crashed" with a stack trace that blames the wrong thing.
One JSONL file per run under traces/<task>/: a run_start, one attempt
per try, then a run_end. Each attempt carries the prompt, the raw reply, the
extracted Verilog, token usage and cost, the per-test simulation results, and
the synthesis verdict with its cell counts.
docs/trace-schema.md is the full contract, and is
where to start if you want to consume the dataset. traces/ is committed on
purpose: the data is the deliverable.
Two suites, neither of which needs model credits:
python -m tests.validate_testbenches # 53 hand-written designs, known verdicts
python -m tests.test_repair_loop # the full loop, with a scripted modeltest_repair_loop swaps the model for a stand-in that returns a design that
fails simulation, then one that passes simulation but fails synthesis, then a
correct one — so both gates, the feedback path, the trace schema and the
failures report are exercised against the real toolchain. A second scripted
model never changes its answer, which exercises the no_self_correction stop.
agent/ provider.py — swappable LLM backends, token and cost accounting
designer.py — prompts, retry prompts, Verilog extraction
runner/ toolchain.py — locates and activates the OSS CAD Suite
sim.py — gate 1, run as a child process
synth.py — gate 2, Yosys
ttfit.py — gate 3, the Tiny Tapeout contract and tile
ttproject.py — package a verified design as a TT project
orchestrator.py — the generate/verify/repair loop
trace.py — append-only JSONL
classify.py — what kind of failure it was
diagnostics.py — the compiler's own error messages
stats.py — dataset summary
failures.py — error/repair pairs
replay.py — the feedback-path experiment
preflight.py — disk room for a local model
runner.py command-line entry point
tasks/ one directory per task
tests/ offline tests, and the mutant designs behind them
traces/ the dataset
docs/ architecture.md, trace-schema.md, tinytapeout.md
- Create
tasks/<name>/withspec.md,test_<name>.pyandtask.json. - Write the spec with exact port names, widths and directions. Ambiguity in the spec shows up as flaky results.
- Create
tests/mutants/<name>/with a correct design, several broken ones, and amanifest.jsonnaming the expected verdict and the test that should catch each. - Run
python -m tests.validate_testbenches <name>until it is clean. - Only then point the agent at it.
- It is not a verification methodology. There is no coverage, no constrained random stimulus, no formal proof. The testbenches are directed tests written by hand.
- It is not a benchmark you should rank models with. Eleven tasks, tens of runs, one person's specs. The numbers above describe this repository, not the field.
- It is not going to design your chip. The tasks are small on purpose, because a dataset of repairs is only worth anything if every verdict in it is trustworthy.
- It has not taped anything out. The Tiny Tapeout path is built and tested end to end, up to and including a GDS built by the shuttle's own flow, but nothing has been submitted to a shuttle and submitting is a separate decision that no code here can take.
- Tasks that good models actually fail. Every task here is solved on the first attempt by every frontier model tried, which is a poor result for a dataset built out of repairs.
- A task with one sharp ambiguity, everything else pinned down, to find
out whether
assumption_mismatchis a real category or a label with no referent. Ten attempts from a mid-size model did not produce one. - Multi-module designs, simple bus interfaces, pipelines.
- Coverage and constrained-random stimulus in the reference testbenches.
- Formal equivalence checking, as a fourth gate.
- Tiny Tapeout tasks with a real area budget, where the tile is the binding constraint rather than a formality. The counter uses a fraction of one tile; nothing here has yet been rejected for being too big.
- A published, versioned trace dataset.
Tecpatl is the Nahuatl word for flint — the stone that was knapped into blades and drills. Silicon, worked by hand.
Apache-2.0.