Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tecpatl

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.


The problem this is aimed at

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.


The double barrier: behaves, and can be built

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.


Eight findings

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.


Results so far

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.)

What each model managed

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.

Where failed attempts died

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.

Two axes: difficulty, and how much the spec says

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 assumption_mismatch that has not appeared

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.


Two things that went wrong, and what they cost

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.

1. The testbench that accused a correct design

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 task
counter_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.

2. The feedback path that was blamed for the wrong thing

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.


Stopping when the model stops correcting itself

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.


The third gate: would anyone manufacture it?

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 gate is tiered, and says which tier answered

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.

The task, and the defect only gate 3 can see

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.

From a verified design to a project that can be built

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_wrapper

The 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 mould works: a real GDS

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.

What the gate-level run found, which nothing else could

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 neuron, built in layers

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 decisions the spec had to pin down

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.

The pin protocol

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.

Area and timing, measured locally

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.

The gate that rejected a good design

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 in silicon

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

What was built

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.

Timing: the multiplier is fine, the pins are not

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.

The pre-flight that stopped a repeat of day 6

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.

Closing the timing

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.

It closes

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.

But it closes by 71 picoseconds

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 max-slew violations: not from this, and not going away

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.

Giving it margin

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.

The clock: all the timing, none of the area

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.

Two things about area that had to be got right

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.

The result

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.

Tasks

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.

How assumption_mismatch is decided

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.


Requirements

  • 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 point TECPATL_OSS_CAD_ROOT at it. Nothing needs to go on your system PATH; Tecpatl handles that itself.
  • A model backend: a Claude Code login, a free Gemini key, or a local Ollama.

Setup

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 found

Usage

python 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 Tapeout
task     : 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.

A note on preflight

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.

LLM backends

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.

Traces

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.

Development

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 model

test_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.

Repository layout

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

Adding a task

  1. Create tasks/<name>/ with spec.md, test_<name>.py and task.json.
  2. Write the spec with exact port names, widths and directions. Ambiguity in the spec shows up as flaky results.
  3. Create tests/mutants/<name>/ with a correct design, several broken ones, and a manifest.json naming the expected verdict and the test that should catch each.
  4. Run python -m tests.validate_testbenches <name> until it is clean.
  5. Only then point the agent at it.

What this is not

  • 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.

Roadmap

  • 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_mismatch is 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.

Name

Tecpatl is the Nahuatl word for flint — the stone that was knapped into blades and drills. Silicon, worked by hand.

License

Apache-2.0.

About

An open-source agent that designs and verifies digital hardware, recording every attempt as an open dataset of Verilog design and repair.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages