Write hardware tests without F# code. The .verifrog format is a line-oriented test description language that covers ~75% of typical block-level verification tests.
Use .verifrog files for tests that are stimulus-check sequences: set signals, step the simulation, check results. This covers smoke tests, basic unit tests, register write-read, memory load-verify, and simple inference completion checks.
Use F# for tests that need control flow: error recovery workflows, golden model comparisons, multi-inference analysis, computed expected values, or fork/sweep exploration.
Both run in the same test suite under verifrog test.
# counter.verifrog
test "counter reaches 10" [Smoke]:
write enable = 1
step 10
expect count == 10
That's it. No imports, no boilerplate, no semicolons.
Equivalent F#:
test "counter reaches 10" {
use sim = SimFixture.create ()
sim.Write("enable", 1L) |> ignore
sim.Step(10)
Expect.signal sim "count" 10L "count should reach 10"
}test "test name" [Category]:
- Name: quoted string
- Category: optional, in brackets. One of:
Smoke,Unit,Parametric,Integration,Stress,Golden,Regression - Line must end with
: - Everything indented below belongs to this test
All commands are indented under a test header.
write enable = 1
write load_value = 0x2A
write data_in = 255
Multiple signals on one line:
write load_value = 42, load_en = 1
Supports decimal and 0x hex values.
step 10
step 1
expect count == 10
expect overflow == 1
expect status != 0
Operators: == and !=.
expect data_ram[0][42] == 0xAB
Format: expect <memory>[<bank>][<addr>] == <value>. Memory names come from verifrog.toml.
Inline data:
load data_ram bank=0 [0xAB, 0xCD, 0xEF, 0x01]
Writes values to consecutive addresses starting at 0.
From a hex file ($readmemh format):
load weight_sram bank=0 from weights.hex
The hex file uses the same format as Verilog's $readmemh: hex values separated by whitespace, with optional @address directives.
run-until done == 1, max = 10000
run-until fsm_state == 5, max = 50000
Steps the simulation until the signal equals the value, or the max cycle count is reached. If the max is reached, the test fails with a timeout message.
force clk_en = 0
step 10
release clk_en
force holds a signal at a value across clock steps. release removes the override and lets the design drive the signal again.
checkpoint before_test
step 100
restore before_test
checkpoint saves the entire simulation state. restore snaps back to it. Named checkpoints are scoped to the test.
Lines starting with # are comments:
# This is a comment
test "my test" [Smoke]:
# Set up stimulus
write enable = 1
step 10
expect count == 10
# counter.verifrog — declarative tests for the counter sample
test "counter starts at zero" [Smoke]:
expect count == 0
test "counter reaches 10" [Smoke]:
write enable = 1
step 10
expect count == 10
test "counter wraps after 255" [Smoke]:
write enable = 1
step 256
expect count == 0
test "load value" [Unit]:
write load_value = 0x2A
write load_en = 1
step 1
write load_en = 0
expect count == 0x2A
test "load then count" [Unit]:
write load_value = 42
write load_en = 1
step 1
write load_en = 0
write enable = 1
step 5
expect count == 47
test "force holds count" [Unit]:
write enable = 1
step 5
expect count == 5
force enable = 0
step 10
expect count == 5
release enable
test "checkpoint and restore" [Unit]:
write enable = 1
step 5
checkpoint mid
step 5
expect count == 10
restore mid
expect count == 5
test "run-until overflow" [Unit]:
write enable = 1
write load_value = 250
write load_en = 1
step 1
write load_en = 0
run-until overflow == 1, max = 20
step 1
expect count == 0
Declarative:
test "starts at zero" [Smoke]:
expect count == 0
F#:
test "starts at zero" {
use sim = SimFixture.create ()
Expect.signal sim "count" 0L "count should be 0"
}Declarative:
test "load then count" [Unit]:
write load_value = 42, load_en = 1
step 1
write load_en = 0, enable = 1
step 5
expect count == 47
F#:
test "load then count" {
use sim = SimFixture.create ()
sim.Write("load_value", 42L) |> ignore
sim.Write("load_en", 1L) |> ignore
sim.Step(1)
sim.Write("load_en", 0L) |> ignore
sim.Write("enable", 1L) |> ignore
sim.Step(5)
Expect.signal sim "count" 47L "should be 47"
}Declarative:
test "checkpoint and restore" [Unit]:
write enable = 1
step 5
checkpoint mid
step 5
expect count == 10
restore mid
expect count == 5
F#:
test "checkpoint and restore" {
use sim = SimFixture.create ()
sim.Write("enable", 1L) |> ignore
sim.Step(5)
let cp = sim.SaveCheckpoint("mid")
sim.Step(5)
Expect.signal sim "count" 10L "count reaches 10"
sim.RestoreCheckpoint("mid")
Expect.signal sim "count" 5L "back to 5"
}Fork/sweep — needs closures:
let result = sim.Fork(fun s ->
s.Write("mode", 3L) |> ignore
s.Step(50)
s.ReadOrFail("output"))Golden model comparison — needs computed values:
let expected = GoldenModel.run config weights activations
Expect.equal actual expected "should match golden"Error recovery — needs conditional control flow:
sim.RunUntilSignal("fsm_state", 3L, 5000) |> ignore // Wait for COMPUTE
sim.Write("abort", 1L) |> ignore // Inject abort
sim.Step(1)
sim.Write("abort", 0L) |> ignore
sim.RunUntilSignal("fsm_state", 0L, 1000) |> ignore // Should return to IDLE
Expect.signal sim "error_flag" 0L "abort is not an error"[test]
output = "build"
tests = "tests" # directory containing .verifrog filesCreate a file with the .verifrog extension in your tests directory.
One line in a DeclarativeLoader.fs file (generated by verifrog init):
open Expecto
open Verifrog.Runner.Declarative
[<Tests>]
let declarativeTests = discoverFromToml (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "..", "verifrog.toml"))This reads [test].tests from the TOML, scans for .verifrog files, parses them, and generates Expecto test cases. It also runs a validation pass that checks all signal names against the sim before running tests.
Alternative: If you need more control, use the lower-level functions:
let tests = loadTests "path/to/tests" // No TOML config
let tests = loadTestsWithConfig "path/to/tests" config // With config
let tests = discover __SOURCE_DIRECTORY__ // Auto-scan from source dir
let tests = discoverWithConfig __SOURCE_DIRECTORY__ config // Auto-scan with configverifrog test # Runs both F# and declarative tests
verifrog test --category Smoke # Filters work across both
verifrog test --report # Report includes bothWhen a declarative test fails, the error points to the .verifrog file:
Expected count == 10 (0xA), got 5 (0x5)
at counter.verifrog:7
Parse errors also include file and line:
counter.verifrog:12: Invalid write: bad syntax