nasm/tools/testgen
H. Peter Anvin (Intel) 35016d4880 x86/insns.pl, tools/testgen: share cc/scc condition-code tables
Extract the condition-code suffix table (%conds, @conds, and the
c_ccmask/c_nd/c_cc/c_scc bitmasks) out of x86/insns.pl's
conditional_forms() into a new shared x86/insns-cc.ph module, adding
cc_suffix_list($is_scc) to return the suffix list applicable to a
'...cc'-family (Jcc, SETcc, CMOVcc, CFCMOVcc) or '...scc'-family
(CCMPscc, CTESTscc, CMPccXADD, SETccZU) placeholder mnemonic. insns.pl
is refactored to use it (behavior-preserving: verified byte-identical
x86/insnsa.c, insnsb.c, insnsd.c, insnsi.h, insnsn.c, iflag.c,
iflaggen.h, asm/tokhash.c, asm/tokens.h before/after).

tools/testgen/gen-insn-tests.pl now requires the same module instead
of hand-rolling a 16-suffix 'cc'-only expansion table, closing the gap
where the APX 'scc'-suffix families (CCMPscc/CTESTscc/CMPccXADD/
SETccZU) were dropped as unsupported. The generator now detects and
expands any '...cc'/'...scc' placeholder mnemonic the same way
insns.pl's conditional_forms() does (case-sensitive /s?cc/ match and
substitution), so future new cc/scc families need no generator
changes. Mnemonics with generated tests: 2432 -> 2606 (+174, one per
newly-expanded scc-family condition); dropped: 20 -> 16.

Regenerated travis/insns/ (174 new mnemonic dirs for the APX scc
families; existing 2432 regenerated byte-identically, confirming
determinism). Full suite validated both via
tools/travis/nasm-t.py --directory=./travis run (4427 total, 4426
PASS, 1 pre-existing SKIP, 0 FAIL) and via make -j32 travis (all
tests PASS, ~28s).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 00:05:47 -07:00
..
gen-insn-tests.pl x86/insns.pl, tools/testgen: share cc/scc condition-code tables 2026-07-02 00:05:47 -07:00
README.md x86/insns.pl, tools/testgen: share cc/scc condition-code tables 2026-07-02 00:05:47 -07:00

gen-insn-tests.pl

Pseudorandom test-case generator for NASM instruction patterns. For every non-PSEUDO mnemonic in x86/insns.xda, it generates a travis/<mnemonic>/ test directory (.asm source, .json descriptor, golden .bin16/32/64[.t]/.stderr16/32/64[.t] files) that plugs directly into the existing tools/travis/nasm-t.py harness. This gives every instruction-set pattern regression coverage without hand-writing thousands of test cases.

The generated tree currently checked in lives at travis/insns/ (2606 mnemonic subdirectories as of the last full run).

Usage

# Regenerate everything into travis/insns (run from repo root, after
# ./nasm has been built and x86/insns.xda is up to date):
perl tools/testgen/gen-insn-tests.pl --outdir travis/insns

# Regenerate just one mnemonic, useful while iterating on the generator:
perl tools/testgen/gen-insn-tests.pl --outdir /tmp/tg-scratch --only vaddps --verbose

# Validate the full existing+generated suite (there's no generated
# Makefile in a fresh checkout without running configure, so invoke the
# harness directly rather than via `make travis`):
python3 tools/travis/nasm-t.py --nasm=./nasm --directory=./travis run

Options (perl tools/testgen/gen-insn-tests.pl --help equivalent, see the GetOptions block at the top of the script for the authoritative list):

Option Default Meaning
--xda FILE x86/insns.xda source instruction-template file
--nasm PATH ./nasm nasm binary used to probe/assemble and to run nasm-t.py update
--outdir DIR travis where to write <mnemonic>/ subdirectories
--seed N 1 base seed for the deterministic per-mnemonic PRNG
--per-mnemonic N 4 max distinct operand-arity templates sampled per mnemonic
--variants N 2 concrete random instruction instances generated per sampled template
--only MNEMONIC (all) restrict generation to a single mnemonic, for iterating on the generator
--no-undoc off exclude UNDOC-flagged templates (not recommended — see below)
--verbose off print per-mnemonic progress as it generates

Generation is deterministic (fixed default seed), so re-running with no source changes reproduces byte-identical .asm/.json files. It should only need to be re-run when x86/insns.dat (and hence the regenerated x86/insns.xda) changes — day-to-day travis test runs use the existing harness and scale via make -jN travis as normal; generation is a separate, infrequent step.

Design: why x86/insns.xda and not insnsa.c/insns.pl hooks

Three data sources were considered:

  1. Parse the generated insnsa.c/insnsb.c directly. Rejected: these encode operands as opaque bitmask constants and index into the shared nasm_bytecodes[] array, which x86/bytecode.txt explicitly documents as unstable ("byte codes can be moved around and recycled at any time"). Parsing this from outside insns.pl would be fragile across NASM releases and high-effort for no real benefit.

  2. Add new hooks to x86/insns.pl to emit a purpose-built intermediate format. Rejected: it would duplicate information insns.xda already provides, adding maintenance risk to a build-critical code generator for no real gain.

  3. Parse x86/insns.xda (chosen). This file is already generated by insns.pl from insns.dat as part of a normal build, with macros fully expanded, one instruction template per line, in a small stable text grammar:

    MNEMONIC   optype1,optype2,...   [enc: bytecode...]   FLAGS
    

    Crucially, we never need the bytecode/encoding field — NASM's own assembler selects the encoding from mnemonic + operand syntax at assemble time. So the generator only needs to produce syntactically valid operand text per operand-type token; it doesn't need to understand or replicate NASM's instruction encoding at all. This means the generated tests are regression tests — validated against a golden captured from a known-good nasm build — not independent correctness oracles. That's the same philosophy already used by the rest of travis/.

Handling of UNDOC / OBSOLETE / NEVER instructions

These are included, not excluded (only PSEUDO templates — which aren't real ISA instructions, e.g. internal directives — are skipped). Verifying that OBSOLETE/NEVER-flagged instructions produce the expected warning (typically -w+obsolete-removed at the assembler's default/highest CPU level) is itself part of the intended test coverage, not something to suppress. When the generator's probe step detects nonempty stderr for a template, it adds a "stderr" target to the .json descriptor so the warning text becomes part of the golden. UNDOC alone does not currently trigger a warning by default (verified against SALC), so most UNDOC mnemonics just get ordinary output-only targets.

Condition-code mnemonic families

insns.xda leaves condition-code instruction families as literal, unexpanded placeholders: Jcc, SETcc, CMOVcc, CFCMOVcc (the "cc" family) and the newer APX CCMPscc, CTESTscc, CMPccXADD, SETccZU (the "scc" family). Unlike $bwdq-style width macros (which insns.pl does pre-expand into concrete lines in .xda), NASM's parser expands condition codes against its own cc-table at parse time — and x86/insns.pl's own conditional_forms() performs the equivalent expansion for the C-code generators, but only after insns.xda has already been written by preinsns.pl, so the placeholders reach this tool unexpanded either way.

The condition-code table (%conds, plus the $c_cc/$c_scc masks distinguishing which suffixes are valid for which family) is shared between insns.pl and this generator via x86/insns-cc.ph, a small Perl module required by both — this guarantees the two expansions never drift out of sync, since there's only one copy of the suffix/mask data. cc_suffix_list($is_scc) (defined in that module) returns the applicable suffix list for either family, and the generator mirrors insns.pl's own detection ($mnem =~ /s?cc/, case-sensitive) and substitution ($mnem =~ s/s?cc/\U$suffix/) exactly, so e.g. Jcc expands to JE/JNE/JGE/... , CCMPscc expands to CCMPE/ CCMPNE/... (excluding the "cc"-only suffixes pe/po/p/np, which aren't valid for scc-family instructions), and SETccZU expands to SETNEZU/... . Any future new "cc"/"scc" family added to insns.dat is picked up automatically without changes to this script.

Bit-width (16/32/64) handling

Bit-mode support isn't derived from CPU/mode flags in insns.xda — the same generated .asm text is simply probed under --bits 16/32/64, and only the modes that actually assemble successfully become targets in the final .json (mirroring the pre-existing travis/jmpxx/-style pattern). Two wrinkles this created and how they're handled:

  • 64-bit-only tokens poisoning 16/32 probes. A single sampled template using a 64-bit-only operand (e.g. reg64, imm64) would otherwise make the whole file fail to assemble at 16/32-bit, even though other templates in the same mnemonic don't need 64-bit. The generator classifies operand tokens via a %needs64_token table and renders two asm bodies per mnemonic — a "narrow" one (16/32-safe) and a "full" one (includes 64-bit-only forms) — reusing one file when they'd be identical.
  • Memory/vsib addressing across bit widths. A fixed base-register name (rax vs eax vs ax) isn't valid syntax across all three bit widths. The generator sidesteps this by using base-register-free addressing: bare-displacement memory operands ([0x1234]) and base-free scaled vsib index forms ([xmm0*1]). This sacrifices coverage of true [base+index*scale+disp] forms — a known, documented limitation, not a bug.

Integration with nasm-t.py

Golden capture is delegated entirely to the existing harness:

python3 tools/travis/nasm-t.py --nasm=./nasm update -t <outdir>/<mnemonic>

rather than hand-rolling nasm invocation, renaming output binaries, or hand-writing stderr golden files. This matters because nasm-t.py's stderr/stdout comparison is an exact byte-for-byte string match with no path normalization — NASM embeds the literal source-path argument it was invoked with into warning/error text, so hand-rolled golden capture is easy to get subtly wrong. update uses its own internal, consistent path construction, so the generator's own cheap throwaway pre-probe (used only to decide per-bit-width viability and whether a "stderr" target is needed) doesn't need to match those paths exactly. If nasm-t.py update fails for a mnemonic (should only happen if the pre-probe's viability check was wrong), the generator drops that mnemonic's directory entirely rather than committing a broken test.

Output naming convention

Multiple bit-width variants generated from one source share the established repo convention: <mnemonic>.bin16 / .bin32 / .bin64 (binary; see e.g. travis/jmpxx/jmpxx-ox.bin16), with .stderr16 / .stderr32 / .stderr64 analogously for expected-warning goldens — not <mnemonic>16.bin.

Known limitations / coverage gaps

The generator prints a coverage summary at the end of each run (mnemonics generated, mnemonics dropped, and a frequency table of unsupported operand-type tokens), so gaps are self-documenting. As of the last full run:

  • 2606 / 2622 mnemonics generate at least one assemblable template (includes all concrete expansions of the cc/scc condition-code families — see above).
  • 16 mnemonics produce zero output and are dropped, mostly:
    • HINT_NOP0 .. HINT_NOP63 placeholder mnemonics,
    • LOADALL / LOADALL286,
    • a handful of exotic AMX-transpose / APX instructions whose full operand grammar wasn't targeted (e.g. T2RPNTLVWZ0*, TCONJT*, TTMMULTF32PS).
  • Memory/vsib operands only exercise base-register-free addressing forms (see above) — true base+index+scale+disp combinations aren't covered by generated tests.
  • ~70+ distinct operand-type tokens have generator support (see the %fixed/%gen tables at the top of the script); any new/renamed token introduced by a future insns.dat change that isn't in those tables will show up in the "unsupported operand-type tokens" summary and cause the affected templates (not necessarily the whole mnemonic) to be skipped.

Extending coverage for any of the above is a matter of adding entries to the %fixed/%gen operand-generator tables or the @cc_suffixes expansion loop — no changes to the parser or driver loop are needed.

Validation

Full-suite validation after the last generation run (existing travis/ tests + travis/insns/):

python3 tools/travis/nasm-t.py --nasm=./nasm --directory=./travis run

4427 tests total, 4426 PASS, 1 pre-existing SKIP (time, an intentional/known skip unrelated to this tool), 0 FAIL, 0 ABORT.