DSL Reference
Complete syntax reference for poly-bench .bench files
The poly-bench DSL is a declarative language for defining cross-language benchmarks. This reference covers the complete syntax for .bench files — every block type, field, option, and value form that the parser accepts.
A .bench file consists of optional standard library imports, an optional file-level globalSetup block, and one or more suite blocks.
1# Optional standard library imports2use std::charting3use std::anvil4
5# Optional file-level global setup6globalSetup {7 anvil.spawnAnvil(fork: "https://eth.llamarpc.com")8}9
10# One or more suites11suite mySuite {12 # Suite configuration, setup blocks, fixtures, benchmarks13}Comments begin with # and extend to the end of the line. They can appear anywhere in the file.
1# This is a full-line comment2
3suite example {4 warmup: 100 # Inline comment after a value5
6 bench myBench {7 go: doSomething() # After expressions too8 }9}Import standard library modules at the top of your file before any other blocks. Multiple imports are allowed.
1use std::charting # Chart generation2use std::anvil # Ethereum node spawning3use std::constants # Mathematical constantsSee the Standard Library reference for module details.
globalSetup BlockThe globalSetup block runs once before all benchmarks in the file. It is the correct place to spawn long-lived services like an Anvil node. It can appear at the file level (before any suite) or inside a suite block.
1use std::anvil2
3globalSetup {4 anvil.spawnAnvil() # Local node, no fork5 anvil.spawnAnvil(fork: "https://rpc.url") # Fork from an RPC endpoint6}anvil.spawnAnvil() is called, the variable ANVIL_RPC_URL is automatically injected into TypeScript setup code as a module-level constant. Go and Rust access it via the environment variable ANVIL_RPC_URL.suite BlockA suite groups related benchmarks with shared configuration, setup code, fixtures, and lifecycle hooks.
1suite <name> {2 # ── Metadata ──────────────────────────────────────3 description: "Human-readable description"4
5 # ── Iteration control ─────────────────────────────6 iterations: 100000 # Fixed iteration count (used when mode: "fixed")7
8 warmup: 1000 # Warmup iterations before timing starts9 mode: "auto" # "auto" (calibrate) or "fixed"10 targetTime: 3000ms # Target wall-clock time for auto calibration11 minIterations: 10 # Floor for auto-calibrated iteration count12 maxIterations: 1000000 # Ceiling for auto-calibrated iteration count13
14 # ── Statistical controls ──────────────────────────15 count: 3 # Number of timed runs per benchmark16 cvThreshold: 5.0 # Target coefficient of variation (%)17 outlierDetection: true # IQR-based outlier removal18
19 # ── Comparison ────────────────────────────────────20 compare: true # Show comparison table in output21 baseline: "go" # Language for speedup ratios ("go", "ts", "rust")22
23 # ── Observability ─────────────────────────────────24 memory: false # Enable memory allocation tracking25 concurrency: 1 # Parallel workers per benchmark26 sink: true # Black-box sink to prevent dead-code elimination27
28 # ── Execution order ───────────────────────────────29 order: sequential # sequential | parallel | random30 timeout: 60000ms # Suite-level timeout31
32 # ── Language requirements ─────────────────────────33 requires: ["go", "ts"] # All benchmarks must implement these languages34
35 # ── Child blocks ──────────────────────────────────36 globalSetup { ... }37 setup go { ... }38 setup ts { ... }39 setup rust { ... }40 fixture myData { ... }41 bench myBench { ... }42 after { ... }43}| Field | Type | Default | Description |
|---|---|---|---|
description | string | — | Human-readable description of the suite |
iterations | number | — | Fixed iteration count; required when mode: "fixed" |
warmup | number | 1000 | Warmup iterations before timing |
mode | string | "auto" | "auto" (calibrate to targetTime) or "fixed" |
targetTime | duration | 3000ms | Target run time for auto calibration |
minIterations | number | 10 | Minimum iterations in auto mode |
maxIterations | number | 1000000 | Maximum iterations in auto mode |
count | number | 1 | Number of timed runs per benchmark |
cvThreshold | number | 5.0 | Target coefficient of variation (%) |
outlierDetection | boolean | true | IQR-based outlier removal |
compare | boolean | false | Show cross-language comparison table |
baseline | string | — | Language for speedup ratios ("go", "ts", "rust") |
memory | boolean | false | Enable memory allocation tracking |
concurrency | number | 1 | Parallel workers per benchmark |
sink | boolean | true | Prevent dead-code elimination via black-box sink |
order | identifier | sequential | sequential, parallel, or random |
timeout | duration | — | Suite-level timeout |
requires | string[] | [] | Languages every benchmark in the suite must implement |
mode: "auto" for most benchmarks. It automatically calibrates the iteration count to reach targetTime, ensuring stable measurements regardless of how fast the operation is.Duration values can be specified with a unit suffix. Plain numbers are treated as milliseconds.
| Unit | Example | Meaning |
|---|---|---|
ms | 3000ms | 3000 milliseconds |
s | 5s | 5 seconds (5000 ms) |
m | 1m | 1 minute (60000 ms) |
1suite timing {2 targetTime: 3000ms # 3 seconds3 targetTime: 3s # same as above4 timeout: 1m # 1 minute5 timeout: 60000ms # same as above6}setup <lang> BlocksSetup blocks define language-specific imports, package-level declarations, one-time initialization, and helper functions. Supported language keywords: go, ts (or typescript), rust.
All four sub-sections are optional and can appear in any order.
1setup go {2 # Grouped import block (Go syntax)3 import (4 "context"5 "crypto/sha256"6 "github.com/ethereum/go-ethereum/ethclient"7 )8
9 # Package-level variable/type/const declarations10 declare {11 var client *ethclient.Client12 var ctx context.Context13 var mu sync.Mutex14 }15
16 # Runs once before any benchmarks17 init {18 ctx = context.Background()19 client, _ = ethclient.Dial("https://eth.llamarpc.com")20 }21
22 # Helper functions available to all bench blocks23 helpers {24 func hashData(data []byte) [32]byte {25 return sha256.Sum256(data)26 }27 }28}| Sub-section | Go syntax | TS syntax | Rust syntax | Description |
|---|---|---|---|---|
import | import ( "pkg" ) | import { import ... } | import { use ...; } | Language imports |
declare | declare { var x T } | declare { let x: T } | declare { static X: T } | Package/module-level declarations |
init | init { ... } | init { ... } or async init { ... } | init { ... } | One-time setup before benchmarks |
helpers | helpers { func f() {} } | helpers { function f() {} } | helpers { fn f() {} } | Helper functions for bench blocks |
async init for asynchronous one-time setup. The await keyword can be used freely inside it. Regular init also works for synchronous setup.fixture BlocksFixtures define shared test data that is passed to benchmark implementations. They ensure all languages operate on identical input.
Provide binary data as a hex-encoded string. The bytes are decoded and passed to each language as its native byte array type.
1fixture shortData {2 hex: "68656c6c6f20776f726c64" # "hello world" in UTF-83}4
5# In bench blocks:6# Go: shortData → []byte{0x68, 0x65, 0x6c, ...}7# TS: shortData → Uint8Array([0x68, 0x65, 0x6c, ...])8# Rust: shortData → &[u8]Load hex data from an external file using @file(...). The path is relative to the .bench file's location.
1fixture largePayload {2 hex: @file("fixtures/sort/sort_1000.hex")3}4
5fixture abiData {6 hex: @file("fixtures/abi/erc20_calldata.hex")7}hex: for small test vectors and hex: @file(...) for larger payloads. This keeps .bench files readable while supporting arbitrarily large fixtures.shape annotationThe optional shape field provides a JSON-like descriptor for documentation and tooling purposes.
1fixture matrixData {2 hex: @file("fixtures/matmul/mat_64.hex")3 shape: { rows: 64, cols: 64, dtype: "float64" }4}description1fixture keccakInput {2 description: "32-byte input for keccak256 hashing"3 hex: "68656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f72"4}bench BlocksBench blocks define the operations to time. Each block specifies an expression or code block for one or more target languages.
1bench <name> {2 # ── Metadata ──────────────────────────────────────3 description: "What this benchmark measures"4 tags: ["crypto", "hashing"]5
6 # ── Iteration overrides (inherit from suite) ──────7 iterations: 1000008 warmup: 10009 mode: "fixed"10 targetTime: 2000ms11 minIterations: 500012 maxIterations: 50000013 timeout: 30000ms14
15 # ── Statistical overrides (inherit from suite) ────16 count: 517 cvThreshold: 5.018 outlierDetection: true19
20 # ── Observability overrides (inherit from suite) ──21 memory: true22 concurrency: 123 sink: true24
25 # ── Lifecycle hooks ───────────────────────────────26 before go: resetCounter()27 before ts: resetCounter()28 each go: incrementCounter()29 each ts: incrementCounter()30
31 # ── Language implementations ──────────────────────32 go: hashData(data)33 ts: hashData(data)34 rust: hash_data(&data)35
36 # ── Post-run hooks ────────────────────────────────37 after go: { _ = setupCounter }38 after ts: { void setupCounter }39
40 # ── Conditional execution ─────────────────────────41 skip: { go: false ts: false }42
43 # ── Result validation ─────────────────────────────44 validate: { go: result != nil ts: result !== null }45}| Field | Type | Default | Description |
|---|---|---|---|
description | string | — | Human-readable description |
tags | string[] | [] | Tags for filtering benchmarks |
iterations | number | suite value | Fixed iteration count |
warmup | number | suite value | Warmup iterations |
mode | string | suite value | "auto" or "fixed" |
targetTime | duration | suite value | Target time for auto mode |
minIterations | number | suite value | Minimum iterations in auto mode |
maxIterations | number | suite value | Maximum iterations in auto mode |
timeout | duration | suite value | Per-benchmark timeout |
count | number | suite value | Number of timed runs |
cvThreshold | number | suite value | Target coefficient of variation (%) |
outlierDetection | boolean | suite value | IQR-based outlier removal |
memory | boolean | suite value | Memory allocation tracking |
concurrency | number | suite value | Parallel workers |
sink | boolean | suite value | Black-box sink |
skip | per-lang bool | — | Skip this benchmark for specific languages |
validate | per-lang expr | — | Validate the return value |
All fields except the language implementations are optional. Numeric/boolean fields inherit from the suite when not specified.
Language implementations can be a single inline expression or a multi-line block:
1bench hashShort {2 go: sha256Sum(data)3 ts: sha256Sum(data)4 rust: sha256_sum(&data)5}You can define benchmarks for a subset of languages. Only languages with a setup block and an implementation line are executed.
1setup go { ... }2setup ts { ... }3# No setup rust block4
5bench operation {6 go: doSomething()7 ts: doSomething()8 # No rust: line — Rust is skipped entirely9}Hooks run at specific points in the benchmark lifecycle. They are defined per-language on individual bench blocks, not at the suite level.
before <lang>Runs once before the timed loop starts for that language. Use it for per-benchmark setup that should not be included in the timing.
each <lang>Runs before each individual iteration, outside the timing window. Use it to reset mutable state between iterations.
after <lang>Runs once after the timed loop ends for that language. Use it for per-benchmark teardown.
Both flat syntax and grouped syntax are supported and equivalent:
1bench withHooks {2 iterations: 100003 mode: "fixed"4
5 before go: resetCounter()6 before ts: resetCounter()7
8 each go: incrementCounter()9 each ts: incrementCounter()10
11 go: sha256Sum(data)12 ts: sha256Sum(data)13
14 after go: {15 _ = setupCounter16 }17 after ts: {18 void setupCounter19 }20}before go: resetState()) or a multi-line block (e.g. after go: { _ = x }). The flat syntax is generally preferred for readability.after { } Block (Suite-level)The suite-level after block runs after all benchmarks complete. It is the correct place for chart generation directives. It requires use std::charting at the top of the file.
1use std::charting2
3suite example {4 # ... benchmarks ...5
6 after {7
8 charting.drawTable(9 title: "Performance Comparison",10 output: "results-bar.svg",11 sortBy: "speedup",12 sortOrder: "desc"13 )14
15
16 charting.drawSpeedupChart(17 title: "Speedup vs Go Baseline",18 output: "results-speedup.svg",19 baselineBenchmark: "hashShort",20 sortBy: "speedup"21 )22 }23}See std::charting for the full parameter reference for each chart function.
| Type | Example | Used by |
|---|---|---|
| String | "hello world" | description, baseline, mode, title, etc. |
| Integer | 5000, 1000000 | iterations, warmup, count, width, height |
| Float | 5.0, 2.5 | cvThreshold, minSpeedup, gridOpacity |
| Duration | 500ms, 3s, 2m | targetTime, timeout |
| Boolean | true / false | compare, memory, sink, outlierDetection |
| String array | ["go", "ts"] | requires, tags, includeBenchmarks |
| Identifier | sequential | order |
| File reference | @file("path/to/file.hex") | hex field in fixtures |
| Code block | { ... } | setup sub-sections, hook bodies, bench implementations |
| Inline expression | sortGo(data) | single-line bench/hook implementations |
A complete .bench file demonstrating all major features:
1use std::charting2
3suite keccakBenchmarks {4 description: "Keccak256 hashing across Go, TypeScript, and Rust"5 warmup: 10006 compare: true7 baseline: "go"8 mode: "auto"9 targetTime: 3000ms10 minIterations: 1011 maxIterations: 100000012 count: 313 cvThreshold: 5.014 outlierDetection: true15 memory: true16 sink: true17
18 setup go {19 import (20 "golang.org/x/crypto/sha3"21 )22
23 declare {24 var runCount int25 }26
27 init {28 runCount = 029 }30
31 helpers {32 func keccak256Go(data []byte) []byte {33 h := sha3.NewLegacyKeccak256()34 h.Write(data)35 return h.Sum(nil)36 }37
38 func resetCount() { runCount = 0 }39 func bumpCount() { runCount++ }40 }41 }42
43 setup ts {44 import {45 import { keccak256 } from 'viem'46 }47
48 declare {49 let runCount = 050 }51
52 helpers {53 function keccak256Ts(data: Uint8Array): Uint8Array {54 return keccak256(data, 'bytes')55 }56
57 function resetCount(): void { runCount = 0 }58 function bumpCount(): void { runCount++ }59 }60 }61
62 setup rust {63 import {64 use tiny_keccak::{Hasher, Keccak};65 }66
67 helpers {68 fn keccak256_rust(data: &[u8]) -> [u8; 32] {69 let mut hasher = Keccak::v256();70 let mut output = [0u8; 32];71 hasher.update(data);72 hasher.finalize(&mut output);73 output74 }75 }76 }77
78 # Inline hex fixture79 fixture shortInput {80 description: "32-byte input"81 hex: "68656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f72"82 }83
84 # File-referenced fixture for larger data85 fixture longInput {86 hex: @file("fixtures/keccak/input_1024.hex")87 }88
89 # Basic benchmark — all three languages90 bench hashShort {91 description: "Hash a 32-byte input"92 go: keccak256Go(shortInput)93 ts: keccak256Ts(shortInput)94 rust: keccak256_rust(&shortInput)95 }96
97 # Benchmark with per-bench overrides98 bench hashLong {99 description: "Hash a 1024-byte input"100 targetTime: 5000ms101 count: 5102 go: keccak256Go(longInput)103 ts: keccak256Ts(longInput)104 rust: keccak256_rust(&longInput)105 }106
107 # Benchmark with lifecycle hooks108 bench hashWithHooks {109 description: "Hash with counter tracking via hooks"110 iterations: 10000111 mode: "fixed"112 before go: resetCount()113 before ts: resetCount()114 each go: bumpCount()115 each ts: bumpCount()116 go: keccak256Go(shortInput)117 ts: keccak256Ts(shortInput)118 after go: { _ = runCount }119 after ts: { void runCount }120 }121
122 # Go-only benchmark (no ts/rust implementation)123 bench goOnly {124 description: "Go-specific operation"125 go: keccak256Go(shortInput)126 }127
128 after {129 charting.drawTable(130 title: "Keccak256 Performance",131 description: "Go vs TypeScript vs Rust",132 output: "keccak-bar.svg",133 sortBy: "speedup",134 sortOrder: "desc",135 showStats: true,136 showMemory: true137 )138
139 charting.drawSpeedupChart(140 title: "Keccak256 Scaling",141 output: "keccak-line.svg",142 xlabel: "Input Size"143 )144 }145}std::anvil, std::charting, std::constants