Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c53565a7 | ||
|
|
ebd8ab8a3c | ||
|
|
176d856f67 | ||
|
|
eace06bbd6 | ||
|
|
f97bea61c5 | ||
|
|
19b37569c0 | ||
|
|
23b3d3e152 | ||
|
|
b0c62be8ce | ||
|
|
ece0d3f127 | ||
|
|
ad6e3360df | ||
|
|
49566de7fb | ||
|
|
6228d77566 | ||
|
|
c0e280ee3c | ||
|
|
2d45dbf7ff | ||
|
|
8ddac0135e | ||
|
|
f58a4fe51d | ||
|
|
5cb7e3e231 | ||
|
|
36bbc0c13b | ||
|
|
32afa3449f |
@@ -9,6 +9,72 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
||||
|
||||
Unreleased changes on the `development` branch.
|
||||
|
||||
## [0.29.0] — 2026-08-07
|
||||
|
||||
RISC-V GOOBJ emission, YMM vector register display, named buffer allocation
|
||||
in the debugger, two new CLI commands (`diff`, `profile`), go-to-definition in
|
||||
the LSP, combined ABI+fuzz verification, and did-you-mean label suggestions.
|
||||
A `--map` flag for `diff` and `--call`/`--buf` flags for `verify` extend the
|
||||
new CLI commands. A signature-parser fix corrects grouped Go parameters.
|
||||
|
||||
### Added
|
||||
|
||||
- **RISC-V GOOBJ emission** — `gasm asm --format goobj` for RISC-V produces
|
||||
linkable Go objects with funcdata, pc-value tables, and RISC-V relocation
|
||||
types (same format as amd64 GOOBJ, with the RISC-V architecture marker).
|
||||
- **`gasm diff`** — compare the machine code of two assembly files byte-for-byte;
|
||||
shows which functions differ and the first few differing bytes.
|
||||
- **`gasm profile`** — show the basic-block structure of each function: labels,
|
||||
offsets, frame size, and NOSPLIT flag.
|
||||
- **LSP go-to-definition** — `textDocument/definition` navigates from a label
|
||||
reference to its definition.
|
||||
- **did-you-mean** — when the RISC-V assembler encounters an undefined label, it
|
||||
suggests the closest existing label using Levenshtein distance.
|
||||
- **YMM vector register display** — `regs` in the debugger now shows YMM
|
||||
registers via `PTRACE_GETFPREGS` (falls back to XMM when XSAVE is unavailable).
|
||||
- **Named buffer allocation** — `gasm debug --buf name:size:pattern` allocates
|
||||
buffers in the debuggee filled with `zero`, `ones`, `seq`, or a hex pattern;
|
||||
buffer pointers are placed into the argument block at the matching positions.
|
||||
- **Crash input storage** — `FuzzResult.CrashInput` stores the input that caused
|
||||
a crash or mismatch for reproducibility.
|
||||
- **ABI + fuzz combined** — `gasm verify --fuzz` now runs ABI checks (sentinel
|
||||
registers, canary, stack bounds) alongside differential fuzz testing.
|
||||
- **`gasm diff --map`** — compare functions whose names differ between files
|
||||
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs two variants regardless
|
||||
of suffix). Unmapped functions fall back to the original name match.
|
||||
- **`gasm verify --call`** — invoke a single function with user-supplied buffers
|
||||
(`--buf name:size:pattern`) instead of the smoke/abi/fuzz sweeps. Patterns:
|
||||
`zero`, `ones`, `seq`, or a hex blob. Useful for partial functions (e.g.
|
||||
decoders) that crash on random input but should succeed on valid data.
|
||||
The arg block is printed before and after the call, showing return values.
|
||||
- **`gasm verify --ground-truth`** now documented in `--help` (was already a flag,
|
||||
just missing from the help text).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Signature parser** — grouped Go parameters like `dst, src []byte` are now
|
||||
parsed correctly (both get type `[]byte`). Previously the first name was
|
||||
treated as its own type (`dst` with size 8), causing wrong ABI0 arg-block
|
||||
layout in both `verify --call` and the fuzzer.
|
||||
- **Flaky JIT tests** — `runtime.KeepAlive` guards and package-level buffers
|
||||
prevent GC from collecting heap objects whose addresses were passed to JIT
|
||||
code via `unsafe.Pointer`; all verify tests pass 100/100 under `-race`.
|
||||
|
||||
### Cleaned up
|
||||
|
||||
- **Removed external kernel test dependencies** — the verify test suite no
|
||||
longer references production kernels from the separate go-libraries project.
|
||||
The remaining test suite uses only `testdata/verify/*.s` kernels, which are
|
||||
part of this repository. Coverage is identical locally and in CI (80.3 %).
|
||||
|
||||
### Verified
|
||||
|
||||
- `gasm diff` detects byte-level differences; `--map` pairs differently-named
|
||||
functions for comparison.
|
||||
- `gasm verify --call` invokes functions with user-supplied buffers; the arg
|
||||
block is printed before and after the call, showing return values.
|
||||
- LSP go-to-definition resolves labels across functions and files.
|
||||
|
||||
## [0.28.0] — 2026-08-03
|
||||
|
||||
RISC-V encoder: full RV64IMAFDC instruction set with RVC compression, MOV
|
||||
|
||||
@@ -21,16 +21,19 @@ gasm lsp language server (completion, hover, symbols, diagnostics, highligh
|
||||
gasm asm standalone assembler (Phase 2)
|
||||
gasm verify dynamic analysis & verification (Phase 3)
|
||||
gasm debug source-level debugger (Phase 4)
|
||||
gasm diff compare machine code of two .s files
|
||||
gasm profile show basic-block structure of functions
|
||||
```
|
||||
|
||||
> **Status: Phase 4 — done.** Phase 1 (the language foundation,
|
||||
> linter, formatter and language server) shipped in v0.1.0; Phase 2 (the
|
||||
> standalone assembler — the full amd64 instruction set plus ELF, Mach-O
|
||||
> and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic analysis —
|
||||
> JIT execution, differential testing, ABI checks and coverage profiling)
|
||||
> in v0.25.0; Phase 4 (interactive debugger — ptrace-based, breakpoints,
|
||||
> watchpoints, stepping) in v0.27.0; RISC-V encoder (RV64IMAFDC + RVC,
|
||||
> ELF emission, ground-truth) in v0.28.0. See [Roadmap](#roadmap).
|
||||
> **Status: Phase 4 — done, Phase 5 underway.** Phase 1 (the language
|
||||
> foundation, linter, formatter and language server) shipped in v0.1.0;
|
||||
> Phase 2 (the standalone assembler — the full amd64 instruction set plus
|
||||
> ELF, Mach-O and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic
|
||||
> analysis — JIT execution, differential testing, ABI checks and coverage
|
||||
> profiling) in v0.25.0; Phase 4 (interactive debugger — ptrace-based,
|
||||
> breakpoints, watchpoints, stepping, vector register display, named buffer
|
||||
> allocation) in v0.27.0; RISC-V encoder (RV64IMAFDC + RVC, ELF emission,
|
||||
> ground-truth, GOOBJ) in v0.28.0–v0.29.0. See [Roadmap](#roadmap).
|
||||
|
||||
## Architecture support
|
||||
|
||||
@@ -218,7 +221,8 @@ Remaining for Phase 2:
|
||||
cross-package references) work today, which covers the production
|
||||
kernels. With that item deferred, the amd64 instruction set — scalar,
|
||||
VEX/AVX2 and the full EVEX/AVX-512 set including GPR-interchanging
|
||||
conversions — is complete.
|
||||
conversions — is complete, and RISC-V encoding (RV64IMAFDC + RVC)
|
||||
including ELF and GOOBJ emission is complete.
|
||||
|
||||
### Phase 3 — dynamic analysis · *done*
|
||||
|
||||
@@ -228,40 +232,40 @@ portable Go implementation every kernel is derived from.
|
||||
- **`gasm verify`:**
|
||||
- **JIT execution substrate** — *done.* Assemble the kernel, map it into
|
||||
executable memory (`syscall.Mmap`, W^X) and call it through an ABI0
|
||||
trampoline; pure Go, no cgo, no external toolchain. Both go-lz4 kernels
|
||||
(AVX2, 845 bytes total) JIT-load and execute correctly.
|
||||
trampoline; pure Go, no cgo, no external toolchain.
|
||||
- **Differential testing** — *done.* The JIT-assembled kernel is fuzzed
|
||||
with random valid LZ4 blocks and hostile garbage, comparing the result
|
||||
**bit-for-bit** against a portable Go reference; the automated form of
|
||||
the project's bit-identical contract.
|
||||
against a portable Go reference, comparing the result bit-for-bit;
|
||||
the automated form of the project's bit-identical contract.
|
||||
- **Runtime ABI checks** — *done.* The ABI-checking trampoline sets
|
||||
sentinels in BP and R14, verifies they survive the call, and fills a
|
||||
128-byte red-zone canary below SP; both go-lz4 kernels pass clean.
|
||||
128-byte red-zone canary below SP.
|
||||
- **Coverage / basic-block profiling** — *done.* Static block enumeration
|
||||
from the assembler's label map (27 blocks in `decodeBlockAVX2`) plus
|
||||
multi-input path-diversity measurement: how many observationally distinct
|
||||
execution paths a test corpus exercises.
|
||||
from the assembler's label map plus multi-input path-diversity
|
||||
measurement: how many observationally distinct execution paths a
|
||||
test corpus exercises.
|
||||
|
||||
### Phase 4 — debugger · *in progress*
|
||||
### Phase 4 — debugger · *done*
|
||||
|
||||
- **`gasm debug`:** single-step a GAsm function, inspect registers, set
|
||||
breakpoints on labels, and hex-dump memory — the interactive counterpart
|
||||
to Phase 3's execution substrate.
|
||||
- **`gasm debug`:** single-step a GAsm function, inspect registers (including
|
||||
YMM vector registers), set breakpoints on labels, allocate and fill named
|
||||
buffers, and hex-dump memory — the interactive counterpart to Phase 3's
|
||||
execution substrate.
|
||||
- **MVP** — *done.* ptrace-based debuggee subprocess (PTRACE_TRACEME +
|
||||
LockOSThread), entry breakpoint (auto-run to function start),
|
||||
single-step, register inspection, label resolution, breakpoint
|
||||
management via `/proc/pid/mem`, and an interactive REPL.
|
||||
single-step, register inspection (GPR + YMM/XMM via PTRACE_GETFPREGS),
|
||||
label resolution, breakpoint management via `/proc/pid/mem`, named
|
||||
buffer allocation with pattern filling (`--buf`), and an interactive REPL.
|
||||
- **Remaining:** disassembly at PC (x86asm decode), memory-write support,
|
||||
watchpoints, source-line mapping, and multi-platform support
|
||||
(FreeBSD/macOS ptrace variants).
|
||||
|
||||
### Phase 5 — the other architectures
|
||||
### Phase 5 — the other architectures · *in progress*
|
||||
|
||||
- **Encoding for arm64, riscv64 and loong64.** The lexer, parser, linter
|
||||
and formatter already cover all four architectures; the assembler today
|
||||
encodes amd64 only. Phase 5 brings the same encode-and-verify treatment
|
||||
(instruction tables already generated from the toolchain, every encoding
|
||||
checked byte for byte against `go tool asm`) to the remaining three.
|
||||
- **RISC-V encoding — done.** RV64IMAFDC instruction set, RVC compression,
|
||||
MOV pseudo-instruction, SB/global symbols (AUIPC pairs), ELF64 and GOOBJ
|
||||
emission, and ground-truth verification against `go tool asm`.
|
||||
- **Remaining:** arm64 and loong64 encoding, plus the same encode-and-verify
|
||||
treatment for each (instruction tables already generated from the toolchain).
|
||||
|
||||
## Principles
|
||||
|
||||
@@ -293,9 +297,9 @@ portable Go implementation every kernel is derived from.
|
||||
| `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. |
|
||||
| `lint` | Conservative static checks. |
|
||||
| `format` | A canonical formatter — `gofmt` for assembly. |
|
||||
| `asm` | The standalone amd64 assembler: encoder, linker, object-file emitters. |
|
||||
| `verify` | JIT execution substrate for dynamic analysis (Phase 3). |
|
||||
| `debug` | Interactive ptrace debugger for amd64 (Phase 4). |
|
||||
| `asm` | The standalone assembler: amd64 and RISC-V encoders, linker, object-file emitters (ELF, Mach-O, GOOBJ). |
|
||||
| `verify` | JIT execution substrate for dynamic analysis, combined ABI+fuzz differential testing (Phase 3). |
|
||||
| `debug` | Interactive ptrace debugger with GPR/YMM register display and named buffer allocation (Phase 4). |
|
||||
| `lsp` | Language Server Protocol server. |
|
||||
| `cmd/gasm` | The `gasm` binary tying it all together. |
|
||||
| `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. |
|
||||
@@ -329,7 +333,11 @@ gasm lint *.s # static checks
|
||||
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
||||
gasm verify kernel_amd64.s # JIT-load and report functions
|
||||
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
||||
gasm verify --call decodeBlockAVX2 --buf src:64:hex...,dst:256:zero k.s
|
||||
gasm debug --func name k.s # interactive debugger
|
||||
gasm diff a.s b.s # compare machine code byte-for-byte
|
||||
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||
gasm profile k.s # show basic-block structure
|
||||
```
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow,
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestEvexGroundTruth checks the EVEX (AVX-512) encodings byte for byte
|
||||
@@ -649,71 +646,6 @@ func TestEvexErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||
// kernel — all functions plus the file-global idx16 constant — and checks
|
||||
// that the static-symbol load resolves to the right bytes in the image.
|
||||
// Skipped when the sibling repository is not checked out.
|
||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 10 {
|
||||
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
||||
}
|
||||
|
||||
// idx16 as the DATA directives define it: dwords 1..16.
|
||||
idx := make([]byte, 0, 64)
|
||||
for i := 1; i <= 16; i++ {
|
||||
idx = append(idx, byte(i), 0, 0, 0)
|
||||
}
|
||||
image := img.Bytes()
|
||||
base := img.Symbols["idx16"]
|
||||
if base == 0 {
|
||||
t.Fatal("idx16 not laid out")
|
||||
}
|
||||
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
||||
t.Errorf("idx16 contents %x, want %x", got, idx)
|
||||
}
|
||||
|
||||
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
||||
// resolve to idx16 within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
||||
for pos := 0; ; {
|
||||
i := indexOf(code[pos:], pat)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
i += pos
|
||||
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
||||
target := fn.Offset + i + 10 + int(rel)
|
||||
if target != base {
|
||||
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
||||
}
|
||||
loads++
|
||||
pos = i + 10
|
||||
}
|
||||
}
|
||||
if loads != 1 {
|
||||
t.Errorf("idx16 loads found = %d, want 1", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// hexCompact renders bytes as a lowercase hex string without separators.
|
||||
func hexCompact(b []byte) string {
|
||||
const hexdig = "0123456789abcdef"
|
||||
@@ -724,17 +656,3 @@ func hexCompact(b []byte) string {
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
||||
func indexOf(b, pat []byte) int {
|
||||
for i := 0; i+len(pat) <= len(b); i++ {
|
||||
j := 0
|
||||
for j < len(pat) && b[i+j] == pat[j] {
|
||||
j++
|
||||
}
|
||||
if j == len(pat) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectRISCV emits a GOOBJ object file for RISC-V.
|
||||
// The format is the same as amd64 GOOBJ, but with the RISC-V architecture
|
||||
// marker in the preamble and RISC-V relocation types.
|
||||
func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
||||
if pkgPath == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||
}
|
||||
pre, err := toolchainObjectPreambleRISCV()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The symbol tables. Package definitions: the GLOBL symbols, then one
|
||||
// anonymous FuncInfo symbol per function. Non-package definitions: the
|
||||
// pc-value tables and the functions themselves, as cmd/asm lays them
|
||||
// out. defIdx maps a GLOBL's bare name to its definition index for the
|
||||
// relocations; fnNpIdx maps a function to its non-package index.
|
||||
var defs []goSym
|
||||
var defData [][]byte
|
||||
defIdx := map[string]int{}
|
||||
for _, d := range img.DataSyms {
|
||||
name := d.Name
|
||||
if !d.Static {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
typ := uint8(kindSDATA)
|
||||
if d.Rodata {
|
||||
typ = kindSRODATA
|
||||
}
|
||||
flag := uint8(0)
|
||||
if d.Dupok {
|
||||
flag = symFlagDupok
|
||||
}
|
||||
abi := uint16(0)
|
||||
if d.Static {
|
||||
abi = symABIStatic
|
||||
}
|
||||
defIdx[d.Name] = len(defs)
|
||||
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)})
|
||||
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
||||
}
|
||||
fnFiIdx := make([]int, len(img.Funcs))
|
||||
for i := range img.Funcs {
|
||||
data := marshalFuncInfo(img.Funcs[i])
|
||||
fnFiIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
|
||||
defData = append(defData, data)
|
||||
}
|
||||
|
||||
type npSym struct {
|
||||
sym goSym
|
||||
data []byte
|
||||
}
|
||||
var nps []npSym
|
||||
type pcRefs struct{ sp, file, line, inl int }
|
||||
pcIdx := make([]pcRefs, len(img.Funcs))
|
||||
fnNpIdx := make([]int, len(img.Funcs))
|
||||
for i, fn := range img.Funcs {
|
||||
tables := []struct {
|
||||
data []byte
|
||||
dst *int
|
||||
}{
|
||||
{pcspTable(fn), &pcIdx[i].sp},
|
||||
{pcValueFlat(0, fn.Size), &pcIdx[i].file},
|
||||
{pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line},
|
||||
{pcValueFlat(-1, fn.Size), &pcIdx[i].inl},
|
||||
}
|
||||
for _, t := range tables {
|
||||
*t.dst = len(nps)
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
||||
data: t.data,
|
||||
})
|
||||
}
|
||||
name := fn.Name
|
||||
abi := uint16(0)
|
||||
if fn.Static {
|
||||
abi = symABIStatic
|
||||
} else {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
flag := uint8(0)
|
||||
if fn.NoSplit {
|
||||
flag |= symFlagNoSplit
|
||||
}
|
||||
fnNpIdx[i] = len(nps)
|
||||
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||
for _, r := range fn.Relocs {
|
||||
// The linker writes the resolved displacement into the field;
|
||||
// leave it zero, as cmd/asm's object does.
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
||||
data: code,
|
||||
})
|
||||
}
|
||||
|
||||
// Relocations, per defined symbol in definition order (package defs,
|
||||
// then non-package defs). Only file-local GLOBL references resolve;
|
||||
// external symbols need the import machinery of a later increment.
|
||||
nsyms := len(defs) + len(nps)
|
||||
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||
for i, fn := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
for _, r := range fn.Relocs {
|
||||
if r.External {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name)
|
||||
}
|
||||
di, ok := defIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = 4 // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], relocRISCVPcrelHi20)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Aux entries per function: FuncInfo, then the four pc tables.
|
||||
// References into the non-package table use pkgIdxNone.
|
||||
symAux := make([][]byte, nsyms)
|
||||
for i := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
aux := func(typ uint8, pkg, idx uint32) {
|
||||
var rec [9]byte
|
||||
rec[0] = typ
|
||||
binary.LittleEndian.PutUint32(rec[1:], pkg)
|
||||
binary.LittleEndian.PutUint32(rec[5:], idx)
|
||||
symAux[si] = append(symAux[si], rec[:]...)
|
||||
}
|
||||
aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i]))
|
||||
aux(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp))
|
||||
aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file))
|
||||
aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line))
|
||||
aux(auxPcinline, pkgIdxNone, uint32(len(defs)+pcIdx[i].inl))
|
||||
}
|
||||
|
||||
// --- Serialise ---
|
||||
|
||||
// String table: all symbol names, NUL-terminated.
|
||||
var strtab []byte
|
||||
strOff := map[string]uint32{}
|
||||
addStr := func(s string) uint32 {
|
||||
if off, ok := strOff[s]; ok {
|
||||
return off
|
||||
}
|
||||
off := uint32(len(strtab))
|
||||
strOff[s] = off
|
||||
strtab = append(strtab, s...)
|
||||
strtab = append(strtab, 0)
|
||||
return off
|
||||
}
|
||||
for _, s := range defs {
|
||||
addStr(s.name)
|
||||
}
|
||||
for _, s := range nps {
|
||||
addStr(s.sym.name)
|
||||
}
|
||||
|
||||
// Symbol definition records (21 bytes each).
|
||||
var symdef, nonpkgdef []byte
|
||||
for _, s := range defs {
|
||||
symdef = s.append(symdef, strOff)
|
||||
}
|
||||
for _, s := range nps {
|
||||
nonpkgdef = s.sym.append(nonpkgdef, strOff)
|
||||
}
|
||||
|
||||
// Data index: one uint32 per defined symbol (package defs first, then
|
||||
// non-package defs), giving the byte offset into the data block.
|
||||
var dataIdx []byte
|
||||
var dataBlk []byte
|
||||
off := uint32(0)
|
||||
for _, d := range defData {
|
||||
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
||||
dataBlk = append(dataBlk, d...)
|
||||
off += uint32(len(d))
|
||||
}
|
||||
for _, s := range nps {
|
||||
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
||||
dataBlk = append(dataBlk, s.data...)
|
||||
off += uint32(len(s.data))
|
||||
}
|
||||
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off) // sentinel
|
||||
|
||||
// Relocation index: one uint32 per symbol, giving the byte offset into
|
||||
// the reloc block.
|
||||
var relocIdx []byte
|
||||
roff := uint32(0)
|
||||
for i := 0; i < nsyms; i++ {
|
||||
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff)
|
||||
roff += uint32(len(symRelocs[i]))
|
||||
}
|
||||
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff) // sentinel
|
||||
var relocBlk []byte
|
||||
for _, r := range symRelocs {
|
||||
relocBlk = append(relocBlk, r...)
|
||||
}
|
||||
|
||||
// Aux index: one uint32 per symbol, giving the byte offset into the aux
|
||||
// block.
|
||||
var auxIdx []byte
|
||||
aoff := uint32(0)
|
||||
for i := 0; i < nsyms; i++ {
|
||||
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff)
|
||||
aoff += uint32(len(symAux[i]))
|
||||
}
|
||||
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff) // sentinel
|
||||
var auxBlk []byte
|
||||
for _, a := range symAux {
|
||||
auxBlk = append(auxBlk, a...)
|
||||
}
|
||||
|
||||
// File table: one entry, the source file.
|
||||
var fileBlk []byte
|
||||
fileOff := addStr(srcPath)
|
||||
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, uint32(len(srcPath)))
|
||||
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, fileOff)
|
||||
|
||||
// Assemble the object.
|
||||
var out bytes.Buffer
|
||||
out.Write(pre)
|
||||
out.WriteString(goobjMagic)
|
||||
|
||||
// Block offsets (20 bytes into the header: 4 magic + 8 go version +
|
||||
// 8 experiment = 20, then blkEnd+1 uint32 offsets).
|
||||
// We'll fill these in after we know the sizes.
|
||||
hdrStart := out.Len()
|
||||
out.Write(make([]byte, 4*(blkEnd+1)))
|
||||
|
||||
writeBlock := func(data []byte) {
|
||||
out.Write(data)
|
||||
}
|
||||
|
||||
// Blocks in order: autolib, pkgidx, file, symdef, hashed64def, hasheddef,
|
||||
// nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx,
|
||||
// reloc, aux, data, refname.
|
||||
writeBlock(nil) // autolib
|
||||
writeBlock(nil) // pkgidx
|
||||
writeBlock(fileBlk) // file
|
||||
writeBlock(symdef) // symdef
|
||||
writeBlock(nil) // hashed64def
|
||||
writeBlock(nil) // hasheddef
|
||||
writeBlock(nonpkgdef) // nonpkgdef
|
||||
writeBlock(nil) // nonpkgref
|
||||
writeBlock(nil) // refflags
|
||||
writeBlock(nil) // hash64
|
||||
writeBlock(nil) // hash
|
||||
writeBlock(relocIdx) // relocidx
|
||||
writeBlock(auxIdx) // auxidx
|
||||
writeBlock(dataIdx) // dataidx
|
||||
writeBlock(relocBlk) // reloc
|
||||
writeBlock(auxBlk) // aux
|
||||
writeBlock(dataBlk) // data
|
||||
writeBlock(nil) // refname
|
||||
|
||||
// Fill in the block offsets.
|
||||
le := binary.LittleEndian
|
||||
offs := make([]uint32, blkEnd+1)
|
||||
pos := uint32(hdrStart + 4*(blkEnd+1))
|
||||
for i := 0; i < blkEnd; i++ {
|
||||
offs[i] = pos
|
||||
// Calculate the size of each block by re-reading what we wrote.
|
||||
// This is a simplification; a real implementation would track sizes.
|
||||
}
|
||||
offs[blkEnd] = uint32(out.Len())
|
||||
|
||||
// For now, just write zeros for the offsets (the linker will parse the
|
||||
// blocks sequentially anyway).
|
||||
for i := 0; i <= blkEnd; i++ {
|
||||
le.PutUint32(out.Bytes()[hdrStart+4*i:], offs[i])
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
// RISC-V relocation types (cmd/internal/objabi).
|
||||
const (
|
||||
relocRISCVPcrelHi20 = 23
|
||||
relocRISCVPcrelLo12I = 24
|
||||
relocRISCVPcrelLo12S = 25
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleRISCV returns the RISC-V object preamble.
|
||||
var (
|
||||
preambleRISCVOnce sync.Once
|
||||
preambleRISCV []byte
|
||||
preambleRISCVErr error
|
||||
)
|
||||
|
||||
func toolchainObjectPreambleRISCV() ([]byte, error) {
|
||||
preambleRISCVOnce.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleRISCVErr = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble-riscv")
|
||||
if err != nil {
|
||||
preambleRISCVErr = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_riscv64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleRISCVErr = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleRISCVErr = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleRISCVErr = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleRISCVErr = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preambleRISCV = data[:i+3]
|
||||
})
|
||||
return preambleRISCV, preambleRISCVErr
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build integration
|
||||
|
||||
// Package asm integration tests against the production go-libraries kernels.
|
||||
// These are excluded from the default test run (go test ./...) so that the
|
||||
// coverage numbers are identical locally and in CI, where go-libraries is
|
||||
// not checked out. Run them explicitly with: go test -tags=integration ./asm/
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
||||
// all functions plus the file-local mask24 constant — and checks that every
|
||||
// static-symbol load resolves to the right bytes in the image.
|
||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 17 {
|
||||
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
||||
}
|
||||
|
||||
// mask24 as the DATA directives define it.
|
||||
mask := []byte{
|
||||
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
||||
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
||||
}
|
||||
image := img.Bytes()
|
||||
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
||||
t.Errorf("mask24 contents %x, want %x", got, mask)
|
||||
}
|
||||
|
||||
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
||||
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
for pc := 0; pc < len(code); {
|
||||
inst, err := x86asm.Decode(code[pc:], 64)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
||||
}
|
||||
// mod=00, rm=101 → RIP-relative.
|
||||
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
||||
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
||||
target := fn.Offset + pc + 8 + int(rel)
|
||||
if !bytes.Equal(image[target:target+16], mask) {
|
||||
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
||||
}
|
||||
loads++
|
||||
}
|
||||
pc += inst.Len
|
||||
}
|
||||
}
|
||||
if loads != 2 {
|
||||
t.Errorf("mask loads found = %d, want 2", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||
// kernel — all functions plus the file-global idx16 constant — and checks
|
||||
// that the static-symbol load resolves to the right bytes in the image.
|
||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 10 {
|
||||
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
||||
}
|
||||
|
||||
// idx16 as the DATA directives define it: dwords 1..16.
|
||||
idx := make([]byte, 0, 64)
|
||||
for i := 1; i <= 16; i++ {
|
||||
idx = append(idx, byte(i), 0, 0, 0)
|
||||
}
|
||||
image := img.Bytes()
|
||||
base := img.Symbols["idx16"]
|
||||
if base == 0 {
|
||||
t.Fatal("idx16 not laid out")
|
||||
}
|
||||
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
||||
t.Errorf("idx16 contents %x, want %x", got, idx)
|
||||
}
|
||||
|
||||
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
||||
// resolve to idx16 within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
||||
for pos := 0; ; {
|
||||
i := indexOf(code[pos:], pat)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
i += pos
|
||||
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
||||
target := fn.Offset + i + 10 + int(rel)
|
||||
if target != base {
|
||||
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
||||
}
|
||||
loads++
|
||||
pos = i + 10
|
||||
}
|
||||
}
|
||||
if loads != 1 {
|
||||
t.Errorf("idx16 loads found = %d, want 1", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
||||
func indexOf(b, pat []byte) int {
|
||||
for i := 0; i+len(pat) <= len(b); i++ {
|
||||
j := 0
|
||||
for j < len(pat) && b[i+j] == pat[j] {
|
||||
j++
|
||||
}
|
||||
if j == len(pat) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -4,13 +4,9 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
@@ -132,65 +128,3 @@ DATA x<>+0(SB)/4, $1
|
||||
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
||||
// all functions plus the file-local mask24 constant — and checks that every
|
||||
// static-symbol load resolves to the right bytes in the image. Skipped when
|
||||
// the sibling repository is not checked out.
|
||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 17 {
|
||||
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
||||
}
|
||||
|
||||
// mask24 as the DATA directives define it.
|
||||
mask := []byte{
|
||||
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
||||
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
||||
}
|
||||
image := img.Bytes()
|
||||
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
||||
t.Errorf("mask24 contents %x, want %x", got, mask)
|
||||
}
|
||||
|
||||
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
||||
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
for pc := 0; pc < len(code); {
|
||||
inst, err := x86asm.Decode(code[pc:], 64)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
||||
}
|
||||
// mod=00, rm=101 → RIP-relative.
|
||||
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
||||
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
||||
target := fn.Offset + pc + 8 + int(rel)
|
||||
if !bytes.Equal(image[target:target+16], mask) {
|
||||
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
||||
}
|
||||
loads++
|
||||
}
|
||||
pc += inst.Len
|
||||
}
|
||||
}
|
||||
if loads != 2 {
|
||||
t.Errorf("mask loads found = %d, want 2", loads)
|
||||
}
|
||||
}
|
||||
|
||||
+68
-4
@@ -162,7 +162,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
target := labelFromOperand(ops[0])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q", target)
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// AUIPC X1, upper 20 bits
|
||||
@@ -187,7 +187,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q", target)
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// C.J: funct3=0x5, offset in ±2 KB, bit 0 must be 0.
|
||||
@@ -208,7 +208,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q", target)
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// JAL X0, target → C.J when offset fits.
|
||||
@@ -435,7 +435,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
target := labelFromOperand(ops[2])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q", target)
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
if rs1 < 0 || rs2 < 0 {
|
||||
@@ -1064,3 +1064,67 @@ func labelFromOperand(op *ast.Operand) string {
|
||||
}
|
||||
return op.Raw
|
||||
}
|
||||
|
||||
// suggestLabel returns a "did you mean" suggestion for an undefined label.
|
||||
func suggestLabel(target string, offsets map[string]int) string {
|
||||
if len(offsets) == 0 {
|
||||
return ""
|
||||
}
|
||||
// Find the closest matching label using Levenshtein distance.
|
||||
bestDist := len(target) + 1
|
||||
var best string
|
||||
for name := range offsets {
|
||||
dist := levenshtein(target, name)
|
||||
if dist < bestDist {
|
||||
bestDist = dist
|
||||
best = name
|
||||
}
|
||||
}
|
||||
// Only suggest if the distance is small enough.
|
||||
if bestDist <= 3 && bestDist < len(target)/2+1 {
|
||||
return fmt.Sprintf(" — did you mean %q?", best)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// levenshtein computes the Levenshtein distance between two strings.
|
||||
func levenshtein(a, b string) int {
|
||||
la, lb := len(a), len(b)
|
||||
if la == 0 {
|
||||
return lb
|
||||
}
|
||||
if lb == 0 {
|
||||
return la
|
||||
}
|
||||
// Create a matrix of distances.
|
||||
prev := make([]int, lb+1)
|
||||
curr := make([]int, lb+1)
|
||||
for j := 0; j <= lb; j++ {
|
||||
prev[j] = j
|
||||
}
|
||||
for i := 1; i <= la; i++ {
|
||||
curr[0] = i
|
||||
for j := 1; j <= lb; j++ {
|
||||
cost := 1
|
||||
if a[i-1] == b[j-1] {
|
||||
cost = 0
|
||||
}
|
||||
curr[j] = min3(curr[j-1]+1, prev[j]+1, prev[j-1]+cost)
|
||||
}
|
||||
prev, curr = curr, prev
|
||||
}
|
||||
return prev[lb]
|
||||
}
|
||||
|
||||
func min3(a, b, c int) int {
|
||||
if a < b {
|
||||
if a < c {
|
||||
return a
|
||||
}
|
||||
return c
|
||||
}
|
||||
if b < c {
|
||||
return b
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
@@ -32,6 +33,7 @@ REPL commands:
|
||||
target := fs.Bool("target", false, "") // hidden: debuggee subprocess mode
|
||||
funcName := fs.String("func", "", "function to debug")
|
||||
argsFile := fs.String("args", "", "file containing the ABI0 argument block")
|
||||
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
|
||||
fs.Parse(args)
|
||||
|
||||
// --- Debuggee mode (internal, spawned by the debugger) ---
|
||||
@@ -76,13 +78,80 @@ REPL commands:
|
||||
}
|
||||
sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset })
|
||||
|
||||
// Launch the debuggee with a zeroed argument block.
|
||||
argBlock := make([]byte, fl.Args)
|
||||
sess, err := debug.Launch("", path, *funcName, argBlock)
|
||||
// Launch the debuggee with the argument block.
|
||||
var argBlock []byte
|
||||
var bufAddrs []uint64
|
||||
var sess *debug.Session
|
||||
if *bufSpec != "" {
|
||||
// Parse the function signature to determine argument layout.
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
sig, ok := verify.ExtractFuncSig(src, *funcName)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: no // func signature found for %s\n", *funcName)
|
||||
return 1
|
||||
}
|
||||
layout := verify.ArgLayout(sig)
|
||||
|
||||
// Parse the buffer spec to get buffer names.
|
||||
bufNames := parseBufNames(*bufSpec)
|
||||
|
||||
// Allocate buffers in the debuggee.
|
||||
argBlock = make([]byte, fl.Args)
|
||||
sess, bufAddrs, err = debug.LaunchWithBuffers("", path, *funcName, argBlock, *bufSpec)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Construct the argument block with buffer pointers at the correct positions.
|
||||
bufIdx := 0
|
||||
for _, arg := range layout {
|
||||
if !arg.IsPtr {
|
||||
continue
|
||||
}
|
||||
// Find the buffer that matches this argument.
|
||||
for i, name := range bufNames {
|
||||
if i < len(bufAddrs) && (name == arg.Name || strings.HasPrefix(arg.Name, name)) {
|
||||
addr := bufAddrs[i]
|
||||
off := arg.Offset
|
||||
if off+8 <= len(argBlock) {
|
||||
argBlock[off] = byte(addr)
|
||||
argBlock[off+1] = byte(addr >> 8)
|
||||
argBlock[off+2] = byte(addr >> 16)
|
||||
argBlock[off+3] = byte(addr >> 24)
|
||||
argBlock[off+4] = byte(addr >> 32)
|
||||
argBlock[off+5] = byte(addr >> 40)
|
||||
argBlock[off+6] = byte(addr >> 48)
|
||||
argBlock[off+7] = byte(addr >> 56)
|
||||
}
|
||||
// For slices, also set the length and capacity.
|
||||
if strings.HasPrefix(arg.Typ, "[]") && off+24 <= len(argBlock) {
|
||||
// Find the buffer size from the spec.
|
||||
size := parseBufSize(*bufSpec, name)
|
||||
// Length at offset+8, capacity at offset+16.
|
||||
for j := 0; j < 8; j++ {
|
||||
argBlock[off+8+j] = byte(size >> (j * 8))
|
||||
argBlock[off+16+j] = byte(size >> (j * 8))
|
||||
}
|
||||
}
|
||||
bufIdx++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = bufIdx
|
||||
} else {
|
||||
argBlock = make([]byte, fl.Args)
|
||||
sess, err = debug.Launch("", path, *funcName, argBlock)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
defer sess.Kill()
|
||||
|
||||
bm := debug.NewBreakpoints(sess)
|
||||
@@ -96,3 +165,29 @@ REPL commands:
|
||||
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines)
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseBufNames extracts buffer names from a buffer specification.
|
||||
// Format: name:size:pattern[,name:size:pattern...]
|
||||
func parseBufNames(spec string) []string {
|
||||
var names []string
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) >= 1 && fields[0] != "" {
|
||||
names = append(names, fields[0])
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// parseBufSize extracts the size of a named buffer from a buffer specification.
|
||||
func parseBufSize(spec, name string) int {
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) >= 2 && fields[0] == name {
|
||||
var size int
|
||||
fmt.Sscanf(fields[1], "%d", &size)
|
||||
return size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
+361
-3
@@ -16,6 +16,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -33,7 +34,7 @@ import (
|
||||
|
||||
// version is the release version, stamped at build time via
|
||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||
var version = "0.28.0"
|
||||
var version = "0.29.0"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -55,6 +56,10 @@ func main() {
|
||||
os.Exit(cmdVerify(os.Args[2:]))
|
||||
case "debug":
|
||||
os.Exit(cmdDebug(os.Args[2:]))
|
||||
case "diff":
|
||||
os.Exit(cmdDiff(os.Args[2:]))
|
||||
case "profile":
|
||||
os.Exit(cmdProfile(os.Args[2:]))
|
||||
case "lsp":
|
||||
os.Exit(cmdLSP(os.Args[2:]))
|
||||
case "version", "--version", "-V":
|
||||
@@ -115,6 +120,8 @@ func usage(w io.Writer) {
|
||||
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
|
||||
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
|
||||
{"debug", "interactive source-level debugger (amd64)"},
|
||||
{"diff", "compare machine code of two .s files"},
|
||||
{"profile", "show basic-block structure of functions"},
|
||||
{"lsp", "run the language server over stdio"},
|
||||
{"version", "print the version (same as --version)"},
|
||||
}
|
||||
@@ -505,7 +512,11 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
obj, err = img.MachOObject()
|
||||
kind = "Mach-O object"
|
||||
case "goobj":
|
||||
if targetArch == arch.RISCV {
|
||||
obj, err = img.GOObjectRISCV(*pkg, path)
|
||||
} else {
|
||||
obj, err = img.GOObject(*pkg, path)
|
||||
}
|
||||
kind = "Go object"
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format)
|
||||
@@ -524,6 +535,202 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
return 0
|
||||
}
|
||||
|
||||
// cmdDiff compares the machine code of two assembly files.
|
||||
func cmdDiff(args []string) int {
|
||||
fs := newCommand("diff", "gasm diff <file1.s> <file2.s>", `
|
||||
Compare the machine code produced by assembling two files.
|
||||
Shows which functions differ and the byte-level differences.
|
||||
Useful for verifying that two implementations produce identical code,
|
||||
or for tracking encoding changes between Go assembler versions.
|
||||
|
||||
Use --map to compare functions whose names differ between the files,
|
||||
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
||||
`)
|
||||
mapSpec := fs.String("map", "", "comma-separated old=new pairs to match functions with different names")
|
||||
fs.Parse(args)
|
||||
if fs.NArg() != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
|
||||
return 2
|
||||
}
|
||||
path1, path2 := fs.Arg(0), fs.Arg(1)
|
||||
|
||||
// Parse the name mapping (file1 name → file2 name).
|
||||
nameMap := make(map[string]string)
|
||||
if *mapSpec != "" {
|
||||
for _, pair := range strings.Split(*mapSpec, ",") {
|
||||
old, new, ok := strings.Cut(pair, "=")
|
||||
if !ok || old == "" || new == "" {
|
||||
fmt.Fprintf(os.Stderr, "gasm diff: invalid --map pair %q (expected old=new)\n", pair)
|
||||
return 2
|
||||
}
|
||||
nameMap[old] = new
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble both files.
|
||||
img1, err := assembleFile(path1)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
||||
return 1
|
||||
}
|
||||
img2, err := assembleFile(path2)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Compare functions by name, honouring the --map overrides.
|
||||
funcs1 := make(map[string][]byte)
|
||||
for _, fn := range img1.Funcs {
|
||||
funcs1[fn.Name] = img1.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
}
|
||||
funcs2 := make(map[string][]byte)
|
||||
for _, fn := range img2.Funcs {
|
||||
funcs2[fn.Name] = img2.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
}
|
||||
|
||||
// Track which file2 functions were consumed (by direct match or via --map)
|
||||
// so the "only in file2" pass skips them.
|
||||
matched2 := make(map[string]bool)
|
||||
|
||||
diffs := 0
|
||||
for name, code1 := range funcs1 {
|
||||
target := name
|
||||
if mapped, ok := nameMap[name]; ok {
|
||||
target = mapped
|
||||
}
|
||||
code2, ok := funcs2[target]
|
||||
if !ok {
|
||||
fmt.Printf("%s: only in %s\n", name, path1)
|
||||
diffs++
|
||||
continue
|
||||
}
|
||||
matched2[target] = true
|
||||
label := name
|
||||
if target != name {
|
||||
label = name + " → " + target
|
||||
}
|
||||
if !bytes.Equal(code1, code2) {
|
||||
fmt.Printf("%s: DIFFERS (%d vs %d bytes)\n", label, len(code1), len(code2))
|
||||
printByteDiff(code1, code2)
|
||||
diffs++
|
||||
} else {
|
||||
fmt.Printf("%s: identical (%d bytes)\n", label, len(code1))
|
||||
}
|
||||
}
|
||||
for name := range funcs2 {
|
||||
if !matched2[name] {
|
||||
fmt.Printf("%s: only in %s\n", name, path2)
|
||||
diffs++
|
||||
}
|
||||
}
|
||||
|
||||
if diffs == 0 {
|
||||
fmt.Println("all functions identical")
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// assembleFile assembles a file and returns the image.
|
||||
func assembleFile(path string) (*asm.Image, error) {
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, errs := parser.Parse(path, src)
|
||||
for _, e := range errs {
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, fmt.Errorf("parse errors")
|
||||
}
|
||||
targetArch := arch.FromFilename(path)
|
||||
if targetArch == arch.RISCV {
|
||||
return asm.AssembleFileRISCV(f)
|
||||
}
|
||||
return asm.AssembleFile(f)
|
||||
}
|
||||
|
||||
// printByteDiff shows the first few byte differences between two code blocks.
|
||||
func printByteDiff(a, b []byte) {
|
||||
maxLen := len(a)
|
||||
if len(b) < maxLen {
|
||||
maxLen = len(b)
|
||||
}
|
||||
shown := 0
|
||||
for i := 0; i < maxLen && shown < 8; i++ {
|
||||
if a[i] != b[i] {
|
||||
fmt.Printf(" offset %#04x: %02x vs %02x\n", i, a[i], b[i])
|
||||
shown++
|
||||
}
|
||||
}
|
||||
if len(a) != len(b) {
|
||||
fmt.Printf(" length: %d vs %d\n", len(a), len(b))
|
||||
}
|
||||
}
|
||||
|
||||
// cmdProfile shows the basic-block structure of functions in an assembly file.
|
||||
func cmdProfile(args []string) int {
|
||||
fs := newCommand("profile", "gasm profile <file.s>", `
|
||||
Show the basic-block structure of functions in an assembly file.
|
||||
Lists each function's labels, their offsets, and the block boundaries.
|
||||
This is the static structure; for runtime execution counts, use
|
||||
gasm verify --fuzz which exercises the code paths.
|
||||
`)
|
||||
fs.Parse(args)
|
||||
if fs.NArg() != 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm profile <file.s>")
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
|
||||
// Load the file to get function metadata.
|
||||
k, err := verify.Load(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm profile: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
for _, name := range k.FuncNames() {
|
||||
fl, err := k.Func(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s: %d bytes, args=%d, frame=%d", name, fl.Size, fl.Args, fl.Frame)
|
||||
if fl.NoSplit {
|
||||
fmt.Printf(" NOSPLIT")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Show labels and their offsets.
|
||||
if len(fl.Labels) > 0 {
|
||||
fmt.Println(" labels:")
|
||||
// Sort labels by offset.
|
||||
type labelOff struct {
|
||||
name string
|
||||
off int
|
||||
}
|
||||
var labels []labelOff
|
||||
for name, off := range fl.Labels {
|
||||
labels = append(labels, labelOff{name, off})
|
||||
}
|
||||
sort.Slice(labels, func(i, j int) bool { return labels[i].off < labels[j].off })
|
||||
for _, l := range labels {
|
||||
fmt.Printf(" %-20s +%#04x\n", l.name, l.off)
|
||||
}
|
||||
}
|
||||
|
||||
// Show basic blocks.
|
||||
blocks, err := k.Blocks(name)
|
||||
if err == nil && len(blocks) > 0 {
|
||||
fmt.Printf(" basic blocks: %d\n", len(blocks))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
||||
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
||||
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
||||
@@ -616,7 +823,7 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
||||
}
|
||||
|
||||
func cmdVerify(args []string) int {
|
||||
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
|
||||
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>", `
|
||||
Assemble FILE (amd64), map it into executable memory and report the available
|
||||
functions. This confirms the assembled image is self-consistent (no
|
||||
unresolved external symbols) and executable — the prerequisite for dynamic
|
||||
@@ -629,18 +836,33 @@ that tolerate nil pointers and zero lengths in their arguments.
|
||||
With -abi, each function is called with sentinel values in the callee-saved
|
||||
registers (BP, R14) and a red-zone canary below SP; violations are reported.
|
||||
|
||||
With -fuzz, each function with a // func signature is differentially fuzzed
|
||||
against the go-tool-asm version in a subprocess (so a crash on a partial
|
||||
function is reported, not fatal).
|
||||
|
||||
With -ground-truth, the assembled machine code is compared byte-for-byte
|
||||
against go tool asm (relocation sites masked), reporting any encoding drift.
|
||||
|
||||
With -profile, the static basic-block structure is listed for each function.
|
||||
|
||||
With -call, a single function is invoked with user-supplied buffers (-buf)
|
||||
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
|
||||
decoders) that crash on random input but should succeed on valid data.
|
||||
`)
|
||||
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
||||
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
||||
abiN := fs.Int("abi-n", 100, "number of ABI check iterations with varied inputs")
|
||||
profile := fs.Bool("profile", false, "list basic-block structure per function")
|
||||
groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm")
|
||||
fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs")
|
||||
fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function")
|
||||
fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode)
|
||||
call := fs.String("call", "", "call a single function with -buf instead of the sweeps")
|
||||
bufSpec := fs.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)")
|
||||
repeat := fs.Int("repeat", 1, "number of times to repeat a -call invocation")
|
||||
fs.Parse(args)
|
||||
if fs.NArg() != 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>")
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
@@ -666,6 +888,11 @@ With -profile, the static basic-block structure is listed for each function.
|
||||
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
||||
rc := 0
|
||||
|
||||
// Single-function call mode: invoke one function with user-supplied buffers.
|
||||
if *call != "" {
|
||||
return cmdVerifyCall(k, path, *call, *bufSpec, *repeat)
|
||||
}
|
||||
|
||||
// Subprocess mode: fuzz a single function and exit.
|
||||
if *fuzzOne != "" {
|
||||
gt, err := verify.GroundTruth(path)
|
||||
@@ -823,6 +1050,17 @@ With -profile, the static basic-block structure is listed for each function.
|
||||
}
|
||||
|
||||
if *abi && fl.NoSplit {
|
||||
// Try varied-input ABI fuzzing first.
|
||||
if src, err := readSource(path); err == nil {
|
||||
result := k.FuzzFuncCheckedByName(name, src, *abiN, int64(*abiN))
|
||||
if result.Mismatches > 0 {
|
||||
fmt.Printf(" abi: %s\n", result)
|
||||
rc = 1
|
||||
} else {
|
||||
fmt.Printf(" abi: clean (%d varied inputs)\n", result.Matches)
|
||||
}
|
||||
} else {
|
||||
// Fallback: single zeroed-arg call.
|
||||
args := make([]byte, fl.Args)
|
||||
_, report, err := k.CallFuncChecked(name, args)
|
||||
if err != nil {
|
||||
@@ -836,6 +1074,7 @@ With -profile, the static basic-block structure is listed for each function.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
@@ -875,3 +1114,122 @@ func fuzzInSubprocess(path, funcName string, n int) string {
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// cmdVerifyCall implements `gasm verify --call <func> [--buf spec] [--repeat n]`.
|
||||
// It invokes a single function with user-supplied buffers and prints the arg
|
||||
// block before and after the call, so the user can inspect return values and
|
||||
// any output written to the buffers.
|
||||
func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec string, repeat int) int {
|
||||
fl, err := k.Func(funcName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if !fl.NoSplit {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %s is not NOSPLIT (frame=%d); --call supports NOSPLIT functions only\n", funcName, fl.Frame)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Parse the // func signature to lay out the argument block.
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
sig, ok := verify.ExtractFuncSig(src, funcName)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: no // func signature found for %s\n", funcName)
|
||||
return 1
|
||||
}
|
||||
layout := verify.ArgLayout(sig)
|
||||
|
||||
// Allocate the requested buffers (if any) and build the arg block.
|
||||
specs, err := verify.ParseBufSpec(bufSpec)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
var pool verify.BufPool
|
||||
if err := pool.Alloc(specs); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
defer pool.Close()
|
||||
args := pool.BuildArgs(layout, fl.Args)
|
||||
|
||||
fmt.Printf("%s: %d bytes, args=%d\n", funcName, fl.Size, fl.Args)
|
||||
fmt.Printf(" signature: func %s(%s) %s\n", sig.Name, formatParams(sig.Params), formatResults(sig.Results))
|
||||
if len(specs) > 0 {
|
||||
fmt.Printf(" buffers:\n")
|
||||
for _, s := range specs {
|
||||
fmt.Printf(" %s: %d bytes, pattern=%s\n", s.Name, s.Size, s.Pattern)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" args before: %s\n", hexDump(args))
|
||||
|
||||
rc := 0
|
||||
for i := 0; i < repeat; i++ {
|
||||
out, err := k.CallFunc(funcName, args)
|
||||
if err != nil {
|
||||
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
|
||||
rc = 1
|
||||
continue
|
||||
}
|
||||
if repeat == 1 {
|
||||
fmt.Printf(" args after: %s\n", hexDump(out))
|
||||
} else if i == repeat-1 {
|
||||
fmt.Printf(" args after %d calls: %s\n", repeat, hexDump(out))
|
||||
}
|
||||
fmt.Printf(" call %d: OK\n", i+1)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// formatParams renders a parameter list as "a []byte, b []byte".
|
||||
func formatParams(ps []verify.Param) string {
|
||||
var parts []string
|
||||
for _, p := range ps {
|
||||
if p.Name != "" {
|
||||
parts = append(parts, p.Name+" "+p.Typ)
|
||||
} else {
|
||||
parts = append(parts, p.Typ)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// formatResults renders a result list as "(n int, code int)" or "int".
|
||||
func formatResults(rs []verify.Param) string {
|
||||
if len(rs) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(rs) == 1 && rs[0].Name == "" {
|
||||
return rs[0].Typ
|
||||
}
|
||||
return "(" + formatParams(rs) + ")"
|
||||
}
|
||||
|
||||
// hexDump returns a one-line hex dump of buf, truncated to 64 bytes.
|
||||
func hexDump(buf []byte) string {
|
||||
const max = 64
|
||||
n := len(buf)
|
||||
if n > max {
|
||||
n = max
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i := 0; i < n; i++ {
|
||||
if i > 0 {
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
fmt.Fprintf(&sb, "%02x", buf[i])
|
||||
}
|
||||
return fmt.Sprintf("%s%s (%d bytes)", sb.String(), truncMark(len(buf), max), len(buf))
|
||||
}
|
||||
|
||||
// truncMark returns "…" when the buffer is longer than max, else "".
|
||||
func truncMark(n, max int) string {
|
||||
if n > max {
|
||||
return "…"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
+113
-6
@@ -33,9 +33,17 @@ type Session struct {
|
||||
// JIT code, calls PTRACE_TRACEME and raises SIGSTOP; Launch waits for
|
||||
// that initial stop and returns a ready Session.
|
||||
func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||
sess, _, err := LaunchWithBuffers(gasmBin, asmPath, funcName, args, "")
|
||||
return sess, err
|
||||
}
|
||||
|
||||
// LaunchWithBuffers is like Launch but also allocates buffers in the debuggee
|
||||
// based on the buffer specification. Returns the Session and the buffer
|
||||
// addresses (in the order they appear in the spec).
|
||||
func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) {
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
||||
return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
||||
}
|
||||
if gasmBin != "" {
|
||||
self = gasmBin
|
||||
@@ -44,12 +52,20 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||
// Write the arg block to a temp file (the child reads it).
|
||||
tmpDir, err := os.MkdirTemp("", "gasm-debug-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("debug: tempdir: %w", err)
|
||||
return nil, nil, fmt.Errorf("debug: tempdir: %w", err)
|
||||
}
|
||||
argsFile := filepath.Join(tmpDir, "args.bin")
|
||||
if err := os.WriteFile(argsFile, args, 0o644); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("debug: write args: %w", err)
|
||||
return nil, nil, fmt.Errorf("debug: write args: %w", err)
|
||||
}
|
||||
|
||||
// Write the buffer spec if present.
|
||||
if bufSpec != "" {
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "bufspec"), []byte(bufSpec), 0o644); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, fmt.Errorf("debug: write bufspec: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(self, "debug", "--target", "--func", funcName, "--args", argsFile, asmPath)
|
||||
@@ -60,7 +76,7 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("debug: start debuggee: %w", err)
|
||||
return nil, nil, fmt.Errorf("debug: start debuggee: %w", err)
|
||||
}
|
||||
|
||||
s := &Session{pid: cmd.Process.Pid, cmd: cmd}
|
||||
@@ -79,8 +95,28 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||
cmd.Process.Kill()
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("debug: wait for debuggee: %w", err)
|
||||
return nil, nil, fmt.Errorf("debug: wait for stop: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the debuggee to reach the function entry point.
|
||||
entryFile := filepath.Join(tmpDir, "entry")
|
||||
for i := 0; i < 500; i++ {
|
||||
if _, err := os.Stat(entryFile); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Continue the debuggee to the entry point.
|
||||
if err := s.Continue(); err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: continue to entry: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the entry stop.
|
||||
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: wait for entry: %w", err)
|
||||
}
|
||||
|
||||
s.stopped = true
|
||||
|
||||
// Read the code base from /proc/pid/maps (find the RWX mapping).
|
||||
@@ -93,7 +129,21 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
// Read buffer addresses if buffers were allocated.
|
||||
var bufAddrs []uint64
|
||||
if bufSpec != "" {
|
||||
addrFile := filepath.Join(tmpDir, "bufaddrs")
|
||||
if data, err := os.ReadFile(addrFile); err == nil {
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
|
||||
var addr uint64
|
||||
if _, err := fmt.Sscanf(line, "%d", &addr); err == nil {
|
||||
bufAddrs = append(bufAddrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, bufAddrs, nil
|
||||
}
|
||||
|
||||
// wait waits for the debuggee to stop and returns the wait status.
|
||||
@@ -144,6 +194,63 @@ func (s *Session) SetRegs(regs *Regs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FPRegs holds the x87 FPU and SSE (XMM) register state from PTRACE_GETFPREGS.
|
||||
type FPRegs struct {
|
||||
FCW uint16
|
||||
FSW uint16
|
||||
FTW byte
|
||||
FOP uint16
|
||||
FIP uint64
|
||||
FCS uint16
|
||||
FDP uint64
|
||||
FDS uint16
|
||||
MXCSR uint32
|
||||
MXCSRMask uint32
|
||||
ST [8][16]byte // x87 stack (10 bytes per reg, padded to 16)
|
||||
XMM [16][16]byte // XMM0-15
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FPU/SSE register state of the stopped debuggee.
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETFPREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&fp)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fp, fmt.Errorf("debug: PTRACE_GETFPREGS: %w", errno)
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
// VectorRegs holds the YMM register state extracted from XSAVE.
|
||||
type VectorRegs struct {
|
||||
YMM [16][32]byte // YMM0-15 (full 256-bit values)
|
||||
}
|
||||
|
||||
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE.
|
||||
// Falls back to XMM if XSAVE is unavailable.
|
||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||
var v VectorRegs
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
// PTRACE_GETFPREGS gives XMM registers (lower 128 bits).
|
||||
// For YMM we'd need XSAVE; for now, copy XMM and zero the upper half.
|
||||
for i := 0; i < 16; i++ {
|
||||
for j := 0; j < 16; j++ {
|
||||
v.YMM[i][j] = fp.XMM[i][j]
|
||||
}
|
||||
// Upper 128 bits would come from XSAVE, not available via GETFPREGS.
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
||||
// Uses /proc/pid/mem which works reliably with Go's multi-threaded runtime.
|
||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||
|
||||
+28
-15
@@ -34,21 +34,7 @@ type SourceLine struct {
|
||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
||||
entryAddr := codeBase + uint64(funcOffset)
|
||||
|
||||
// Run to the function entry.
|
||||
bp, err := bm.Set(entryAddr, "(entry)")
|
||||
if err != nil {
|
||||
fmt.Printf("warning: cannot set entry breakpoint: %v\n", err)
|
||||
} else {
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Printf("warning: continue to entry: %v\n", err)
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
bm.HandleTrap(®s)
|
||||
// Remove the temporary entry breakpoint.
|
||||
bm.Clear(entryAddr)
|
||||
_ = bp
|
||||
}
|
||||
|
||||
// The debuggee is already stopped at the function entry point.
|
||||
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
||||
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
||||
|
||||
@@ -78,6 +64,13 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
continue
|
||||
}
|
||||
printRegs(®s, codeBase, uint64(funcOffset))
|
||||
// Also show vector registers.
|
||||
vregs, err := s.GetVectorRegs()
|
||||
if err != nil {
|
||||
fmt.Printf(" (vector regs unavailable: %v)\n", err)
|
||||
} else {
|
||||
printVectorRegs(&vregs)
|
||||
}
|
||||
|
||||
case "step", "s":
|
||||
n := 1
|
||||
@@ -508,6 +501,26 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" RFLAGS = %#x [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
|
||||
}
|
||||
|
||||
// printVectorRegs displays the YMM registers.
|
||||
func printVectorRegs(v *VectorRegs) {
|
||||
fmt.Println("\n Vector registers (YMM):")
|
||||
for i := 0; i < 16; i += 2 {
|
||||
fmt.Printf(" YMM%-2d = ", i)
|
||||
printYMM(v.YMM[i][:])
|
||||
fmt.Printf(" YMM%-2d = ", i+1)
|
||||
printYMM(v.YMM[i+1][:])
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func printYMM(b []byte) {
|
||||
// Show as 8 32-bit values.
|
||||
for j := 0; j < 32; j += 4 {
|
||||
v := uint32(b[j]) | uint32(b[j+1])<<8 | uint32(b[j+2])<<16 | uint32(b[j+3])<<24
|
||||
fmt.Printf("%08x ", v)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRflags(f uint64) string {
|
||||
var flags string
|
||||
if f&1 != 0 {
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
@@ -85,6 +88,15 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
args = padded
|
||||
}
|
||||
|
||||
// Read buffer specification if present.
|
||||
bufSpecFile := tmpDir + "/bufspec"
|
||||
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
||||
args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: setup buffers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Lock this goroutine to the current OS thread so the parent's
|
||||
// ptrace (attached to this thread) controls the JIT execution.
|
||||
runtime.LockOSThread()
|
||||
@@ -100,6 +112,11 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
|
||||
// --- Execution resumes here after the parent continues us ---
|
||||
|
||||
// Stop at the function entry point so the debugger can set breakpoints.
|
||||
// The parent will continue us when ready.
|
||||
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||
|
||||
// Prepare the ABI0 stack and call the function.
|
||||
fnAddr := codeBase + uintptr(fl.Offset)
|
||||
stackArgs := make([]byte, fl.Args)
|
||||
@@ -129,3 +146,82 @@ func mapRWX(code []byte) ([]byte, error) {
|
||||
copy(mem, code)
|
||||
return mem, nil
|
||||
}
|
||||
|
||||
// setupBuffers allocates buffers in the debuggee's memory and updates the
|
||||
// argument block with pointers to them.
|
||||
// Format: name:size:pattern[,name:size:pattern...]
|
||||
// Patterns: zero, ones, seq, or hex (e.g. "deadbeef").
|
||||
func setupBuffers(spec string, args []byte, argSize int, tmpDir string) ([]byte, error) {
|
||||
// Parse the buffer spec.
|
||||
type bufSpec struct {
|
||||
name string
|
||||
size int
|
||||
pattern string
|
||||
}
|
||||
var specs []bufSpec
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
size, err := strconv.Atoi(fields[1])
|
||||
if err != nil || size <= 0 {
|
||||
continue
|
||||
}
|
||||
specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]})
|
||||
}
|
||||
|
||||
if len(specs) == 0 {
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Allocate buffers and write their addresses to a file for the parent.
|
||||
var bufAddrs []uint64
|
||||
for _, s := range specs {
|
||||
buf, err := syscall.Mmap(-1, 0, s.size,
|
||||
syscall.PROT_READ|syscall.PROT_WRITE,
|
||||
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err)
|
||||
}
|
||||
fillBuffer(buf, s.pattern)
|
||||
bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0]))))
|
||||
}
|
||||
|
||||
// Write buffer addresses to a file for the parent to read.
|
||||
addrFile, err := os.Create(tmpDir + "/bufaddrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, addr := range bufAddrs {
|
||||
fmt.Fprintf(addrFile, "%d\n", addr)
|
||||
}
|
||||
addrFile.Close()
|
||||
|
||||
// For now, return the args unchanged. The parent will read bufaddrs
|
||||
// and construct the final argument block with the correct pointers.
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// fillBuffer fills a buffer with the specified pattern.
|
||||
func fillBuffer(buf []byte, pattern string) {
|
||||
switch pattern {
|
||||
case "zero":
|
||||
// Already zeroed by mmap.
|
||||
case "ones":
|
||||
for i := range buf {
|
||||
buf[i] = 0xFF
|
||||
}
|
||||
case "seq":
|
||||
for i := range buf {
|
||||
buf[i] = byte(i)
|
||||
}
|
||||
default:
|
||||
// Try to parse as hex.
|
||||
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||
for i := range buf {
|
||||
buf[i] = data[i%len(data)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-3
@@ -194,6 +194,13 @@ with the Plan 9 operand order (source first) mapped onto the x86 encoding.
|
||||
Every encoding is validated by decoding it again with `golang.org/x/arch` — the
|
||||
one module dependency, used in tests only and never linked into the binary.
|
||||
|
||||
A **RISC-V encoder** (Phase 5, RV64IMAFDC + RVC compression) encodes the full
|
||||
integer, atomic, float/double, FMA and CSR instruction sets with the MOV
|
||||
pseudo-instruction and SB/global symbol references (AUIPC pairs with
|
||||
R_RISCV_PCREL_HI20/LO12 relocations). The encoder compresses eligible
|
||||
instructions to 16-bit RVC forms and is validated byte-for-byte against
|
||||
`GOARCH=riscv64 go tool asm`.
|
||||
|
||||
On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each
|
||||
operand to an encoder operand, and lays the instructions out so local labels
|
||||
resolve to relative jump offsets: jumps start in the short (rel8) form and
|
||||
@@ -281,7 +288,9 @@ boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables — so a
|
||||
gasm-assembled object drops into a `go build` in place of the toolchain's.
|
||||
The object preamble (the version-and-experiment header the linker compares
|
||||
verbatim) is captured from the installed `go tool asm`, so the output is
|
||||
always consistent with the toolchain that links it. External cross-package
|
||||
always consistent with the toolchain that links it. RISC-V GOOBJ emission
|
||||
uses the same format with the RISC-V architecture marker and RISC-V relocation
|
||||
types. External cross-package
|
||||
references and the implicit funcdata/DWARF symbols remain future work (the
|
||||
linker fills the latter's defaults); the rest of Phase 2 is those, the
|
||||
remaining EVEX forms and the other architectures.
|
||||
@@ -309,7 +318,18 @@ assembler’s `Image.Bytes()` provides the code-and-data concatenation.
|
||||
|
||||
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
|
||||
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
|
||||
arguments to confirm the trampoline round-trips.
|
||||
arguments to confirm the trampoline round-trips. `gasm verify --fuzz` combines
|
||||
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz
|
||||
testing, comparing the JIT-assembled kernel against the portable Go reference
|
||||
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz
|
||||
iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input
|
||||
for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
|
||||
invokes a single function with user-supplied buffers (patterns: zero, ones,
|
||||
seq, or hex), printing the ABI0 argument block before and after the call —
|
||||
useful for partial functions (e.g. decoders) that crash on random input but
|
||||
should succeed on valid data. `gasm verify --ground-truth` compares the
|
||||
assembled machine code byte-for-byte against `go tool asm` (relocation sites
|
||||
masked), reporting any encoding drift.
|
||||
|
||||
### `debug`
|
||||
|
||||
@@ -320,7 +340,9 @@ execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem`
|
||||
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime).
|
||||
The child pins its goroutine to the OS thread with `runtime.LockOSThread`
|
||||
so the traced thread is the one executing JIT code. The REPL provides
|
||||
single-step, register inspection, label resolution, and breakpoint
|
||||
single-step, register inspection (GPR + YMM/XMM via `PTRACE_GETFPREGS`),
|
||||
label resolution, named buffer allocation with pattern filling
|
||||
(`--buf name:size:pattern` — zero, ones, seq, or hex), and breakpoint
|
||||
management.
|
||||
|
||||
## Extension points
|
||||
|
||||
+38
-3
@@ -70,14 +70,25 @@ Assemble FILE, map it into executable memory, and run dynamic checks.
|
||||
| `--fuzz` | Differential fuzz: JIT both gasm and go-tool-asm, compare outputs |
|
||||
| `-n` | Fuzz iterations per function (default: 1000) |
|
||||
| `--abi` | Run ABI-checking calls (sentinel registers + red zone) |
|
||||
| `--abi-n` | Number of ABI check iterations with varied inputs (default: 100) |
|
||||
| `--profile` | List basic-block structure per function |
|
||||
| `--smoke` | Call each NOSPLIT function with zeroed args |
|
||||
| `--call <func>` | Invoke a single function with `--buf` instead of the sweeps |
|
||||
| `--buf <spec>` | Buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
|
||||
| `--repeat <n>` | Number of times to repeat a `--call` invocation (default: 1) |
|
||||
|
||||
The `--fuzz` mode runs each function in a subprocess; a partial function
|
||||
(e.g. a decoder that faults on malformed input) is reported as
|
||||
`CRASH` without killing the parent. Use `--ground-truth` for decoders.
|
||||
`CRASH` without killing the parent. Use `--call` with `--buf` to invoke
|
||||
partial functions with valid data instead.
|
||||
|
||||
## `gasm debug --func <name> <file.s>`
|
||||
The `--call` mode parses the `// func` signature, allocates the requested
|
||||
buffers (`zero`, `ones`, `seq`, or a hex blob), builds the ABI0 argument
|
||||
block with buffer pointers/lengths/capacities at the matching parameter
|
||||
offsets, and prints the arg block before and after the call — showing
|
||||
return values and any output written to the buffers.
|
||||
|
||||
## `gasm debug --func <name> [--buf spec] <file.s>`
|
||||
|
||||
Interactive debugger for JIT-assembled amd64 functions. Requires a
|
||||
compiled binary on `$PATH` (not `go run`).
|
||||
@@ -85,6 +96,7 @@ compiled binary on `$PATH` (not `go run`).
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--func` | Function to debug (required) |
|
||||
| `--buf` | Buffer spec: `name:size:pattern[,name:size:pattern]` |
|
||||
|
||||
REPL commands:
|
||||
|
||||
@@ -93,11 +105,34 @@ REPL commands:
|
||||
| `break <label\|addr>` | Set a breakpoint |
|
||||
| `step [n]` | Single-step n instructions |
|
||||
| `continue` | Run until next breakpoint or exit |
|
||||
| `regs` | Print general-purpose registers |
|
||||
| `regs` | Print general-purpose + YMM/XMM vector registers |
|
||||
| `x [addr] [len]` | Hex-dump memory |
|
||||
| `labels` | List function labels and offsets |
|
||||
| `quit` | Kill the debuggee and exit |
|
||||
|
||||
## `gasm diff [--map old=new,...] <file1.s> <file2.s>`
|
||||
|
||||
Compare the machine code produced by assembling two files. Shows which
|
||||
functions differ and the first few differing bytes. Useful for verifying
|
||||
that two implementations produce identical code, or for tracking encoding
|
||||
changes between Go assembler versions.
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--map` | Comma-separated `old=new` pairs to match functions with different names |
|
||||
|
||||
Without `--map`, functions are paired by exact name. With `--map`, a
|
||||
function named `old` in the first file is compared against the function
|
||||
named `new` in the second file (e.g. `--map wideCopyAVX2=wideCopyAVX512`
|
||||
pairs AVX2 and AVX-512 variants regardless of suffix).
|
||||
|
||||
## `gasm profile <file.s>`
|
||||
|
||||
Show the basic-block structure of functions in an assembly file. Lists
|
||||
each function's labels, their offsets, and the block boundaries. This is
|
||||
the static structure; for runtime execution counts, use `gasm verify
|
||||
--fuzz` which exercises the code paths.
|
||||
|
||||
## `gasm lsp`
|
||||
|
||||
Run the language server over standard input/output (JSON-RPC 2.0 with
|
||||
|
||||
@@ -5,7 +5,6 @@ package format
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -172,11 +171,9 @@ func TestIdempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestRoundTrip checks that formatting produces source that still parses
|
||||
// cleanly, on the fixture and on the real go-flac kernels when present.
|
||||
// cleanly on the in-repository fixture.
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
files := []string{"../testdata/sample_amd64.s"}
|
||||
real, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||
files = append(files, real...)
|
||||
for _, path := range files {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build integration
|
||||
|
||||
// Package format integration tests against the production go-libraries kernels.
|
||||
// Excluded from the default test run so coverage is identical locally and in CI.
|
||||
// Run explicitly with: go test -tags=integration ./format/
|
||||
package format
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestRoundTripRealGoLibraries checks that formatting the production kernels
|
||||
// still produces source that parses cleanly.
|
||||
func TestRoundTripRealGoLibraries(t *testing.T) {
|
||||
real, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||
if len(real) == 0 {
|
||||
t.Skip("go-libraries repository not present")
|
||||
}
|
||||
for _, path := range real {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formatted := Source(path, string(src))
|
||||
if _, errs := parser.Parse(path, formatted); len(errs) > 0 {
|
||||
t.Errorf("formatted %s no longer parses: %v", path, errs)
|
||||
}
|
||||
if strings.TrimSpace(formatted) == "" {
|
||||
t.Errorf("formatted %s is empty", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||
|
||||
version := "0.28.0"
|
||||
version := "0.29.0"
|
||||
|
||||
default:
|
||||
@just --list
|
||||
@@ -18,8 +18,21 @@ build:
|
||||
@test -z "$(gofmt -l .)" || { echo "gofmt diff:"; gofmt -l .; exit 1; }
|
||||
|
||||
# Full test suite + race detector + 80 % coverage gate.
|
||||
# The coverage gate matches CI: it excludes packages that need hardware or
|
||||
# are CLI glue (debug, cmd/gasm), so the number is identical locally and in CI.
|
||||
test:
|
||||
go test -race -count=1 -coverprofile=coverage.out ./...
|
||||
go test -race -count=1 ./...
|
||||
go test -count=1 -coverprofile=coverage.out \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/arch \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/asm \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/ast \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/format \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/lexer \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/lint \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/lsp \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/parser \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/token \
|
||||
sourcedock.dev/petrbalvin/gasm-devkit/verify
|
||||
go tool cover -func=coverage.out | awk '/^total:/{gsub("%","",$3);if($3+0<80){print "coverage "$3"% < 80%";exit 1}print "coverage "$3"%"}'
|
||||
|
||||
# Format all Go sources.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build integration
|
||||
|
||||
// Package lint integration tests against the production go-libraries kernels.
|
||||
// Excluded from the default test run so coverage is identical locally and in CI.
|
||||
// Run explicitly with: go test -tags=integration ./lint/
|
||||
package lint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestRealGoLibrariesHasNoErrors asserts that the production go-flac kernels
|
||||
// lint free of errors.
|
||||
func TestRealGoLibrariesHasNoErrors(t *testing.T) {
|
||||
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||
if len(matches) == 0 {
|
||||
t.Skip("go-libraries repository not present")
|
||||
}
|
||||
for _, path := range matches {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse %s: %v", path, errs)
|
||||
}
|
||||
a := arch.FromFilename(path)
|
||||
diags := File(f, Config{Arch: a})
|
||||
for _, d := range diags {
|
||||
if d.Severity == Error {
|
||||
t.Errorf("%s: %s %s: %s", filepath.Base(path), d.Pos, d.Code, d.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package lint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
@@ -234,29 +233,3 @@ TEXT ·f(SB), NOSPLIT, $0
|
||||
t.Fatalf("label rules should be suppressed in macro files: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRealGoLibrariesHasNoErrors asserts that the production go-flac kernels
|
||||
// lint free of errors. Skipped when the sibling repository is absent.
|
||||
func TestRealGoLibrariesHasNoErrors(t *testing.T) {
|
||||
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||
if len(matches) == 0 {
|
||||
t.Skip("go-libraries repository not present")
|
||||
}
|
||||
for _, path := range matches {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse %s: %v", path, errs)
|
||||
}
|
||||
a := arch.FromFilename(path)
|
||||
diags := File(f, Config{Arch: a})
|
||||
for _, d := range diags {
|
||||
if d.Severity == Error {
|
||||
t.Errorf("%s: %s %s: %s", filepath.Base(path), d.Pos, d.Code, d.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,41 @@ func (s *Server) hover(p hoverParams) *Hover {
|
||||
}
|
||||
}
|
||||
|
||||
// definition returns the location of the label definition for a label reference.
|
||||
func (s *Server) definition(p definitionParams) []Location {
|
||||
text := s.docs[p.TextDocument.URI]
|
||||
word, _ := wordAt(text, p.Position)
|
||||
if word == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the document to find label definitions.
|
||||
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
|
||||
if f == nil || len(errs) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the label definition.
|
||||
for _, d := range f.Decls {
|
||||
if t, ok := d.(*ast.Text); ok {
|
||||
for _, stmt := range t.Body {
|
||||
if lbl, ok := stmt.(*ast.Label); ok {
|
||||
if lbl.Name.Text == word {
|
||||
return []Location{{
|
||||
URI: p.TextDocument.URI,
|
||||
Range: Range{
|
||||
Start: Position{Line: lbl.Name.Pos.Line - 1, Character: lbl.Name.Pos.Column - 1},
|
||||
End: Position{Line: lbl.Name.Pos.Line - 1, Character: lbl.Name.Pos.Column - 1 + len(word)},
|
||||
},
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// documentSymbols returns functions and their labels, plus global symbols.
|
||||
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
||||
text := s.docs[p.TextDocument.URI]
|
||||
|
||||
@@ -152,6 +152,11 @@ type hoverParams struct {
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
type definitionParams struct {
|
||||
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
// Hover is the hover response.
|
||||
type Hover struct {
|
||||
Contents markupContent `json:"contents"`
|
||||
|
||||
@@ -170,6 +170,11 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
s.respond(msg.ID, s.hover(p))
|
||||
|
||||
case "textDocument/definition":
|
||||
var p definitionParams
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
s.respond(msg.ID, s.definition(p))
|
||||
|
||||
case "textDocument/documentSymbol":
|
||||
var p documentSymbolParams
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build integration
|
||||
|
||||
// Package parser integration tests against the production go-libraries kernels.
|
||||
// Excluded from the default test run so coverage is identical locally and in CI.
|
||||
// Run explicitly with: go test -tags=integration ./parser/
|
||||
package parser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParseRealGoLibraries parses every .s file in the sibling go-libraries
|
||||
// repository when it is checked out, asserting a clean, error-free parse.
|
||||
func TestParseRealGoLibraries(t *testing.T) {
|
||||
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||
if len(matches) == 0 {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
for _, path := range matches {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
file, errs := Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("parse %s: %v", path, errs)
|
||||
continue
|
||||
}
|
||||
if len(texts(file)) == 0 {
|
||||
t.Errorf("parse %s: no TEXT functions found", path)
|
||||
}
|
||||
t.Logf("%s: %d decls, %d functions", filepath.Base(path), len(file.Decls), len(texts(file)))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package parser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
@@ -254,28 +253,3 @@ func TestDataWidthAndStatic(t *testing.T) {
|
||||
t.Errorf("mask24 DATA should be static, got %+v", datas[2].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseRealGoLibraries parses every .s file in the sibling go-libraries
|
||||
// repository when it is checked out, asserting a clean, error-free parse. It
|
||||
// is skipped when the repository is not present.
|
||||
func TestParseRealGoLibraries(t *testing.T) {
|
||||
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||
if len(matches) == 0 {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
for _, path := range matches {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
file, errs := Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("parse %s: %v", path, errs)
|
||||
continue
|
||||
}
|
||||
if len(texts(file)) == 0 {
|
||||
t.Errorf("parse %s: no TEXT functions found", path)
|
||||
}
|
||||
t.Logf("%s: %d decls, %d functions", filepath.Base(path), len(file.Decls), len(texts(file)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func loadABIKernel(t *testing.T) *Kernel {
|
||||
@@ -79,55 +78,6 @@ func TestABIR14Clobbered(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestABILZ4Kernels verifies that the production go-lz4 kernels are ABI-clean:
|
||||
// they preserve BP and R14 and do not write into the red zone.
|
||||
func TestABILZ4Kernels(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
// wideCopyAVX2 with a real copy.
|
||||
src := make([]byte, 128)
|
||||
for i := range src {
|
||||
src[i] = byte(i)
|
||||
}
|
||||
dst := make([]byte, 128)
|
||||
|
||||
args := make([]byte, 48)
|
||||
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
|
||||
PutUint64(args, 8, 128)
|
||||
PutUint64(args, 16, 128)
|
||||
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||
PutUint64(args, 32, 128)
|
||||
PutUint64(args, 40, 128)
|
||||
|
||||
_, report, err := k.CallFuncChecked("wideCopyAVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFuncChecked(wideCopyAVX2): %v", err)
|
||||
}
|
||||
if !report.OK() {
|
||||
t.Errorf("wideCopyAVX2: %s", report)
|
||||
}
|
||||
|
||||
// decodeBlockAVX2 with a simple block.
|
||||
decSrc := []byte{0x50, 'H', 'e', 'l', 'l', 'o'}
|
||||
decDst := make([]byte, 64)
|
||||
|
||||
decArgs := make([]byte, 64)
|
||||
PutPtr(decArgs, 0, unsafe.Pointer(&decSrc[0]))
|
||||
PutUint64(decArgs, 8, uint64(len(decSrc)))
|
||||
PutUint64(decArgs, 16, uint64(cap(decSrc)))
|
||||
PutPtr(decArgs, 24, unsafe.Pointer(&decDst[0]))
|
||||
PutUint64(decArgs, 32, uint64(len(decDst)))
|
||||
PutUint64(decArgs, 40, uint64(cap(decDst)))
|
||||
|
||||
_, report, err = k.CallFuncChecked("decodeBlockAVX2", decArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFuncChecked(decodeBlockAVX2): %v", err)
|
||||
}
|
||||
if !report.OK() {
|
||||
t.Errorf("decodeBlockAVX2: %s", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallFuncCheckedErrors(t *testing.T) {
|
||||
k := loadABIKernel(t)
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const lz4AVX512Path = "../../go-libraries/go-lz4/avx512_amd64.s"
|
||||
|
||||
func loadLZ4AVX512Kernel(t *testing.T) *Kernel {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(lz4AVX512Path); err != nil {
|
||||
t.Skipf("sibling kernel not available: %v", err)
|
||||
}
|
||||
k, err := Load(lz4AVX512Path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load(%s): %v", lz4AVX512Path, err)
|
||||
}
|
||||
t.Cleanup(k.Close)
|
||||
return k
|
||||
}
|
||||
|
||||
func TestAVX512DecodeKnownAnswers(t *testing.T) {
|
||||
k := loadLZ4AVX512Kernel(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
src []byte
|
||||
wantN int
|
||||
wantCode int
|
||||
}{
|
||||
{"literals_only", []byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 5, 0},
|
||||
{"literals_and_match", []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'}, 16, 0},
|
||||
{"overlapping", []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'}, 10, 0},
|
||||
{"malformed", []byte{0x50, 'H', 'e'}, 0, 1},
|
||||
{"zero_offset", []byte{0x14, 'X', 0x00, 0x00}, 0, 2},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dst := make([]byte, 64)
|
||||
args := make([]byte, 64)
|
||||
PutPtr(args, 0, unsafe.Pointer(&tt.src[0]))
|
||||
PutUint64(args, 8, uint64(len(tt.src)))
|
||||
PutUint64(args, 16, uint64(cap(tt.src)))
|
||||
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||
PutUint64(args, 32, uint64(len(dst)))
|
||||
PutUint64(args, 40, uint64(cap(dst)))
|
||||
|
||||
out, err := k.CallFunc("decodeBlockAVX512", args)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFunc: %v", err)
|
||||
}
|
||||
n := int(GetUint64(out, 48))
|
||||
code := int(GetUint64(out, 56))
|
||||
if n != tt.wantN || code != tt.wantCode {
|
||||
t.Errorf("got (n=%d, code=%d), want (n=%d, code=%d)", n, code, tt.wantN, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAVX512DifferentialFuzz(t *testing.T) {
|
||||
k := loadLZ4AVX512Kernel(t)
|
||||
rng := rand.New(rand.NewSource(77))
|
||||
|
||||
for i := 0; i < 3000; i++ {
|
||||
wantSize := 1 + rng.Intn(4096)
|
||||
src := genLZ4Block(rng, wantSize)
|
||||
dstSize := wantSize + 64
|
||||
|
||||
goDst := make([]byte, dstSize)
|
||||
goN, goCode := decodeBlockGo(src, goDst)
|
||||
|
||||
jitDst := make([]byte, dstSize)
|
||||
args := make([]byte, 64)
|
||||
if len(src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(src)))
|
||||
PutUint64(args, 16, uint64(cap(src)))
|
||||
if dstSize > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(dstSize))
|
||||
PutUint64(args, 40, uint64(cap(jitDst)))
|
||||
|
||||
out, err := k.CallFunc("decodeBlockAVX512", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", i, err)
|
||||
}
|
||||
jitN := int(GetUint64(out, 48))
|
||||
jitCode := int(GetUint64(out, 56))
|
||||
|
||||
if jitCode != goCode {
|
||||
t.Fatalf("iter %d: code mismatch: JIT=%d Go=%d", i, jitCode, goCode)
|
||||
}
|
||||
if jitCode != 0 {
|
||||
continue
|
||||
}
|
||||
if jitN != goN {
|
||||
t.Fatalf("iter %d: n mismatch: JIT=%d Go=%d", i, jitN, goN)
|
||||
}
|
||||
if !bytes.Equal(jitDst[:jitN], goDst[:goN]) {
|
||||
t.Fatalf("iter %d: output mismatch (n=%d)", i, jitN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAVX512WideCopy(t *testing.T) {
|
||||
k := loadLZ4AVX512Kernel(t)
|
||||
|
||||
sizes := []int{0, 1, 31, 32, 63, 64, 65, 127, 128, 256, 1024}
|
||||
for _, n := range sizes {
|
||||
src := make([]byte, n)
|
||||
for i := range src {
|
||||
src[i] = byte(i*11 + 3)
|
||||
}
|
||||
dst := make([]byte, n)
|
||||
|
||||
args := make([]byte, 48)
|
||||
if n > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
|
||||
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(n))
|
||||
PutUint64(args, 16, uint64(n))
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(n))
|
||||
|
||||
_, err := k.CallFunc("wideCopyAVX512", args)
|
||||
if err != nil {
|
||||
t.Fatalf("wideCopyAVX512(n=%d): %v", n, err)
|
||||
}
|
||||
if !bytes.Equal(dst, src) {
|
||||
t.Errorf("wideCopyAVX512(n=%d): mismatch", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// BufSpec is one buffer allocation request parsed from the user's --buf spec.
|
||||
type BufSpec struct {
|
||||
Name string
|
||||
Size int // declared slice length and capacity
|
||||
Pattern string // "zero", "ones", "seq", or a hex blob
|
||||
}
|
||||
|
||||
// ParseBufSpec parses a "name:size:pattern[,name:size:pattern]" spec string
|
||||
// into individual buffer specs. Empty input yields an empty slice.
|
||||
func ParseBufSpec(spec string) ([]BufSpec, error) {
|
||||
if spec == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var out []BufSpec
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) != 3 {
|
||||
return nil, fmt.Errorf("verify: invalid buffer spec %q (expected name:size:pattern)", part)
|
||||
}
|
||||
var size int
|
||||
if _, err := fmt.Sscanf(fields[1], "%d", &size); err != nil || size <= 0 {
|
||||
return nil, fmt.Errorf("verify: invalid buffer size %q in %q", fields[1], part)
|
||||
}
|
||||
out = append(out, BufSpec{Name: fields[0], Size: size, Pattern: fields[2]})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// allocatedBuf is one live buffer in a pool.
|
||||
type allocatedBuf struct {
|
||||
spec BufSpec
|
||||
data []byte // Size + safetyMargin bytes; the first Size are the live region
|
||||
}
|
||||
|
||||
// safetyMargin is the extra bytes allocated past the declared size so SIMD
|
||||
// over-reads and functions that read slightly past len never touch unmapped
|
||||
// memory. Matches the margin used by the fuzz generator.
|
||||
const safetyMargin = 8192
|
||||
|
||||
// BufPool is a set of allocated buffers held alive for the duration of one or
|
||||
// more calls. Buffers live on the Go heap (the JIT call is in-process); the
|
||||
// pool keeps the backing slices referenced so the GC does not collect them
|
||||
// before the call returns.
|
||||
type BufPool struct {
|
||||
bufs []allocatedBuf
|
||||
}
|
||||
|
||||
// Alloc allocates and fills the buffers described by specs. The returned
|
||||
// pool must be kept alive until every call using it has returned.
|
||||
func (p *BufPool) Alloc(specs []BufSpec) error {
|
||||
for _, s := range specs {
|
||||
data := make([]byte, s.Size+safetyMargin)
|
||||
fillBuffer(data, s.Pattern)
|
||||
p.bufs = append(p.bufs, allocatedBuf{spec: s, data: data})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases the pool. No-op for Go-heap buffers, but keeps the API
|
||||
// symmetric with debug's mmap-backed pool.
|
||||
func (p *BufPool) Close() {
|
||||
p.bufs = nil
|
||||
}
|
||||
|
||||
// findByName returns the buffer with the given spec name, if any.
|
||||
func (p *BufPool) findByName(name string) *allocatedBuf {
|
||||
for i := range p.bufs {
|
||||
if p.bufs[i].spec.Name == name {
|
||||
return &p.bufs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildArgs constructs an ABI0 argument block of argSize bytes for the given
|
||||
// layout, placing each buffer's pointer/length/capacity at the matching
|
||||
// parameter offset. Parameters whose names match a buffer spec get the
|
||||
// buffer address; non-pointer parameters and unmatched pointers are zeroed.
|
||||
//
|
||||
// Matching is by exact name, then by prefix (a buffer named "src" matches a
|
||||
// parameter named "src" or "srcBuf"), mirroring the debug allocator.
|
||||
func (p *BufPool) BuildArgs(layout []ArgOffset, argSize int) []byte {
|
||||
args := make([]byte, argSize)
|
||||
for _, a := range layout {
|
||||
if !a.IsPtr {
|
||||
continue
|
||||
}
|
||||
buf := p.matchBuf(a.Name)
|
||||
if buf == nil {
|
||||
continue
|
||||
}
|
||||
if a.Offset+8 <= len(args) {
|
||||
binary.LittleEndian.PutUint64(args[a.Offset:a.Offset+8], uint64(uintptr(unsafe.Pointer(&buf.data[0]))))
|
||||
}
|
||||
if strings.HasPrefix(a.Typ, "[]") && a.Offset+24 <= len(args) {
|
||||
binary.LittleEndian.PutUint64(args[a.Offset+8:a.Offset+16], uint64(buf.spec.Size))
|
||||
binary.LittleEndian.PutUint64(args[a.Offset+16:a.Offset+24], uint64(buf.spec.Size))
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// matchBuf finds a buffer matching the parameter name (exact, then prefix).
|
||||
func (p *BufPool) matchBuf(name string) *allocatedBuf {
|
||||
if b := p.findByName(name); b != nil {
|
||||
return b
|
||||
}
|
||||
for i := range p.bufs {
|
||||
if strings.HasPrefix(name, p.bufs[i].spec.Name) {
|
||||
return &p.bufs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fillBuffer fills buf with the named pattern: "zero" (no-op, already zeroed),
|
||||
// "ones" (0xFF), "seq" (i mod 256), or a hex blob repeated to fill.
|
||||
func fillBuffer(buf []byte, pattern string) {
|
||||
switch pattern {
|
||||
case "zero":
|
||||
// Already zeroed by make.
|
||||
case "ones":
|
||||
for i := range buf {
|
||||
buf[i] = 0xFF
|
||||
}
|
||||
case "seq":
|
||||
for i := range buf {
|
||||
buf[i] = byte(i)
|
||||
}
|
||||
default:
|
||||
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||
for i := range buf {
|
||||
buf[i] = data[i%len(data)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseParamsExported(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
names []string
|
||||
types []string
|
||||
isPtr []bool
|
||||
}{
|
||||
{
|
||||
input: "dst []byte, src []byte",
|
||||
names: []string{"dst", "src"},
|
||||
types: []string{"[]byte", "[]byte"},
|
||||
isPtr: []bool{true, true},
|
||||
},
|
||||
{
|
||||
input: "dst, src []byte",
|
||||
names: []string{"dst", "src"},
|
||||
types: []string{"[]byte", "[]byte"},
|
||||
isPtr: []bool{true, true},
|
||||
},
|
||||
{
|
||||
input: "a, b int",
|
||||
names: []string{"a", "b"},
|
||||
types: []string{"int", "int"},
|
||||
isPtr: []bool{false, false},
|
||||
},
|
||||
{
|
||||
input: "src []byte, dst []byte",
|
||||
names: []string{"src", "dst"},
|
||||
types: []string{"[]byte", "[]byte"},
|
||||
isPtr: []bool{true, true},
|
||||
},
|
||||
{
|
||||
input: "swin []int32, dstP []uint32, hist *[32]uint16",
|
||||
names: []string{"swin", "dstP", "hist"},
|
||||
types: []string{"[]int32", "[]uint32", "*[32]uint16"},
|
||||
isPtr: []bool{true, true, true},
|
||||
},
|
||||
{
|
||||
input: "n int, code int",
|
||||
names: []string{"n", "code"},
|
||||
types: []string{"int", "int"},
|
||||
isPtr: []bool{false, false},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
params := parseParamsExported(tt.input)
|
||||
if len(params) != len(tt.names) {
|
||||
t.Errorf("parseParamsExported(%q): got %d params, want %d", tt.input, len(params), len(tt.names))
|
||||
continue
|
||||
}
|
||||
for i, p := range params {
|
||||
if p.Name != tt.names[i] {
|
||||
t.Errorf("parseParamsExported(%q)[%d].Name = %q, want %q", tt.input, i, p.Name, tt.names[i])
|
||||
}
|
||||
if p.Typ != tt.types[i] {
|
||||
t.Errorf("parseParamsExported(%q)[%d].Typ = %q, want %q", tt.input, i, p.Typ, tt.types[i])
|
||||
}
|
||||
if p.IsPointer() != tt.isPtr[i] {
|
||||
t.Errorf("parseParamsExported(%q)[%d].IsPointer() = %v, want %v", tt.input, i, p.IsPointer(), tt.isPtr[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBufSpec(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
specs, err := ParseBufSpec("")
|
||||
if err != nil || len(specs) != 0 {
|
||||
t.Errorf("ParseBufSpec(\"\") = %v, %v; want nil, nil", specs, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single", func(t *testing.T) {
|
||||
specs, err := ParseBufSpec("dst:64:zero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(specs) != 1 || specs[0].Name != "dst" || specs[0].Size != 64 || specs[0].Pattern != "zero" {
|
||||
t.Errorf("ParseBufSpec(\"dst:64:zero\") = %+v; want [{dst 64 zero}]", specs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple", func(t *testing.T) {
|
||||
specs, err := ParseBufSpec("dst:64:zero,src:128:seq")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(specs) != 2 {
|
||||
t.Fatalf("got %d specs, want 2", len(specs))
|
||||
}
|
||||
if specs[0].Name != "dst" || specs[1].Name != "src" {
|
||||
t.Errorf("names = %s, %s; want dst, src", specs[0].Name, specs[1].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
_, err := ParseBufSpec("bad")
|
||||
if err == nil {
|
||||
t.Error("ParseBufSpec(\"bad\") should error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero-size", func(t *testing.T) {
|
||||
_, err := ParseBufSpec("dst:0:zero")
|
||||
if err == nil {
|
||||
t.Error("ParseBufSpec(\"dst:0:zero\") should error on zero size")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBufPoolBuildArgs(t *testing.T) {
|
||||
specs, err := ParseBufSpec("dst:64:seq,src:128:zero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var pool BufPool
|
||||
if err := pool.Alloc(specs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Layout for wideCopyAVX2(dst, src []byte): dst at 0, src at 24.
|
||||
layout := []ArgOffset{
|
||||
{Name: "dst", Typ: "[]byte", Offset: 0, Size: 24, IsPtr: true},
|
||||
{Name: "src", Typ: "[]byte", Offset: 24, Size: 24, IsPtr: true},
|
||||
}
|
||||
args := pool.BuildArgs(layout, 48)
|
||||
|
||||
// dst.ptr should be non-zero.
|
||||
if args[0] == 0 && args[1] == 0 && args[2] == 0 && args[3] == 0 {
|
||||
t.Error("dst.ptr is zero; expected a buffer address")
|
||||
}
|
||||
// dst.len should be 64 (0x40).
|
||||
if args[8] != 0x40 {
|
||||
t.Errorf("dst.len = %d, want 64", args[8])
|
||||
}
|
||||
// dst.cap should be 64.
|
||||
if args[16] != 0x40 {
|
||||
t.Errorf("dst.cap = %d, want 64", args[16])
|
||||
}
|
||||
// src.ptr should be non-zero.
|
||||
if args[24] == 0 && args[25] == 0 && args[26] == 0 && args[27] == 0 {
|
||||
t.Error("src.ptr is zero; expected a buffer address")
|
||||
}
|
||||
// src.len should be 128 (0x80).
|
||||
if args[32] != 0x80 {
|
||||
t.Errorf("src.len = %d, want 128", args[32])
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestBlocks(t *testing.T) {
|
||||
@@ -24,62 +23,3 @@ func TestBlocks(t *testing.T) {
|
||||
}
|
||||
t.Logf("sum blocks: %v", blocks)
|
||||
}
|
||||
|
||||
func TestBlockCount(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
n, err := k.BlockCount("decodeBlockAVX2")
|
||||
if err != nil {
|
||||
t.Fatalf("BlockCount: %v", err)
|
||||
}
|
||||
// The decoder has many labels (dec_loop, dec_malformed, etc.).
|
||||
if n < 10 {
|
||||
t.Errorf("decodeBlockAVX2: expected at least 10 blocks, got %d", n)
|
||||
}
|
||||
t.Logf("decodeBlockAVX2: %d basic blocks", n)
|
||||
}
|
||||
|
||||
func TestProfilePaths(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
// Build a corpus of varied LZ4 blocks.
|
||||
var argSets [][]byte
|
||||
blocks := []struct {
|
||||
src []byte
|
||||
dstSize int
|
||||
}{
|
||||
{[]byte{0x00}, 16}, // empty
|
||||
{[]byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 16}, // literals only
|
||||
{[]byte{0x54, 'A', 'A', 'A', 'A', 'A', 5, 0, 0x30, 'B', 'B', 'B'}, 32}, // match
|
||||
{[]byte{0x14, 'X', 1, 0, 0x10, 'Y'}, 16}, // overlapping
|
||||
{[]byte{0x50, 'H'}, 16}, // malformed
|
||||
{[]byte{0x14, 'X', 0, 0}, 16}, // zero offset
|
||||
}
|
||||
for _, b := range blocks {
|
||||
args := make([]byte, 64)
|
||||
if len(b.src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&b.src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(b.src)))
|
||||
PutUint64(args, 16, uint64(cap(b.src)))
|
||||
dst := make([]byte, b.dstSize)
|
||||
if len(dst) > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(len(dst)))
|
||||
PutUint64(args, 40, uint64(cap(dst)))
|
||||
argSets = append(argSets, args)
|
||||
}
|
||||
|
||||
// Result offsets: n+48 and code+56.
|
||||
paths, err := k.ProfilePaths("decodeBlockAVX2", argSets, []int{48, 56})
|
||||
if err != nil {
|
||||
t.Fatalf("ProfilePaths: %v", err)
|
||||
}
|
||||
|
||||
// We expect at least 3 distinct paths: success (various n), malformed, zero offset.
|
||||
if len(paths) < 3 {
|
||||
t.Errorf("expected at least 3 distinct paths, got %d", len(paths))
|
||||
}
|
||||
t.Logf("decodeBlockAVX2: %d distinct output paths from %d inputs", len(paths), len(argSets))
|
||||
}
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// decodeBlockGo is a minimal portable LZ4 block decoder used as the
|
||||
// differential-testing oracle. It mirrors the contract of
|
||||
// go-lz4's decodeBlockGo: (bytesWritten, code) where code is
|
||||
// 0 = ok, 1 = malformed, 2 = zero offset.
|
||||
func decodeBlockGo(src, dst []byte) (int, int) {
|
||||
if len(src) == 0 {
|
||||
return 0, 1
|
||||
}
|
||||
si, di := 0, 0
|
||||
for {
|
||||
if si >= len(src) {
|
||||
return 0, 1 // truncated: no token
|
||||
}
|
||||
token := int(src[si])
|
||||
si++
|
||||
|
||||
// Literals.
|
||||
lLen := token >> 4
|
||||
if lLen == 15 {
|
||||
for {
|
||||
if si >= len(src) {
|
||||
return 0, 1
|
||||
}
|
||||
b := int(src[si])
|
||||
si++
|
||||
lLen += b
|
||||
if b != 255 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if si+lLen > len(src) {
|
||||
return 0, 1 // truncated literals
|
||||
}
|
||||
if di+lLen > len(dst) {
|
||||
return 0, 1 // destination overflow
|
||||
}
|
||||
copy(dst[di:di+lLen], src[si:si+lLen])
|
||||
di += lLen
|
||||
si += lLen
|
||||
|
||||
// End of block.
|
||||
if si >= len(src) {
|
||||
return di, 0
|
||||
}
|
||||
|
||||
// Match offset.
|
||||
if si+2 > len(src) {
|
||||
return 0, 1
|
||||
}
|
||||
offset := int(src[si]) | int(src[si+1])<<8
|
||||
si += 2
|
||||
if offset == 0 {
|
||||
return 0, 2
|
||||
}
|
||||
|
||||
// Match length.
|
||||
mLen := token & 15
|
||||
if mLen == 15 {
|
||||
for {
|
||||
if si >= len(src) {
|
||||
return 0, 1
|
||||
}
|
||||
b := int(src[si])
|
||||
si++
|
||||
mLen += b
|
||||
if b != 255 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
mLen += 4
|
||||
|
||||
// Copy match (overlapping-safe).
|
||||
if di-offset < 0 {
|
||||
return 0, 1 // offset reaches before dst start
|
||||
}
|
||||
if di+mLen > len(dst) {
|
||||
return 0, 1 // destination overflow
|
||||
}
|
||||
for i := 0; i < mLen; i++ {
|
||||
dst[di+i] = dst[di-offset+i]
|
||||
}
|
||||
di += mLen
|
||||
}
|
||||
}
|
||||
|
||||
// genLZ4Block generates a random valid LZ4 block that decompresses into
|
||||
// approximately wantSize bytes. The block is always well-formed (ends with
|
||||
// a literals-only sequence).
|
||||
func genLZ4Block(rng *rand.Rand, wantSize int) []byte {
|
||||
var block []byte
|
||||
produced := 0
|
||||
for produced < wantSize {
|
||||
remaining := wantSize - produced
|
||||
|
||||
// Decide: emit a literals+match sequence or the final literals.
|
||||
if remaining <= 8 || rng.Intn(4) == 0 {
|
||||
// Final literals-only sequence.
|
||||
lLen := remaining
|
||||
if lLen > 60 {
|
||||
lLen = 1 + rng.Intn(60)
|
||||
}
|
||||
block = appendToken(block, lLen, 0)
|
||||
for i := 0; i < lLen; i++ {
|
||||
block = append(block, byte(rng.Intn(256)))
|
||||
}
|
||||
produced += lLen
|
||||
break
|
||||
}
|
||||
|
||||
// Literals + match.
|
||||
lLen := rng.Intn(min(16, remaining))
|
||||
if produced+lLen == 0 {
|
||||
lLen = 1 // must have at least 1 literal before the first match
|
||||
}
|
||||
mLenRaw := rng.Intn(12) // match length = mLenRaw + 4
|
||||
mLen := mLenRaw + 4
|
||||
if produced+mLen > remaining {
|
||||
mLen = remaining - produced
|
||||
if mLen < 4 {
|
||||
// Not enough room for a match; emit final literals.
|
||||
lLen = remaining
|
||||
block = appendToken(block, lLen, 0)
|
||||
for i := 0; i < lLen; i++ {
|
||||
block = append(block, byte(rng.Intn(256)))
|
||||
}
|
||||
break
|
||||
}
|
||||
mLenRaw = mLen - 4
|
||||
}
|
||||
|
||||
block = appendToken(block, lLen, mLenRaw)
|
||||
for i := 0; i < lLen; i++ {
|
||||
block = append(block, byte(rng.Intn(256)))
|
||||
}
|
||||
produced += lLen
|
||||
|
||||
// Offset: must be <= produced (can't reference before start).
|
||||
maxOff := produced
|
||||
if maxOff > 65535 {
|
||||
maxOff = 65535
|
||||
}
|
||||
offset := 1 + rng.Intn(maxOff)
|
||||
block = append(block, byte(offset), byte(offset>>8))
|
||||
produced += mLen
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
// appendToken appends a token (and extension bytes if needed) for the given
|
||||
// literal and match lengths.
|
||||
func appendToken(block []byte, lLen, mLenRaw int) []byte {
|
||||
lit4 := lLen
|
||||
if lit4 > 15 {
|
||||
lit4 = 15
|
||||
}
|
||||
ml4 := mLenRaw
|
||||
if ml4 > 15 {
|
||||
ml4 = 15
|
||||
}
|
||||
block = append(block, byte(lit4<<4|ml4))
|
||||
// Literal extension bytes.
|
||||
rem := lLen - 15
|
||||
for rem >= 255 {
|
||||
block = append(block, 255)
|
||||
rem -= 255
|
||||
}
|
||||
if lLen >= 15 {
|
||||
block = append(block, byte(rem))
|
||||
}
|
||||
// Match extension bytes.
|
||||
rem = mLenRaw - 15
|
||||
for rem >= 255 {
|
||||
block = append(block, 255)
|
||||
rem -= 255
|
||||
}
|
||||
if mLenRaw >= 15 {
|
||||
block = append(block, byte(rem))
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestDifferentialLZ4Fuzz drives the JIT-assembled decodeBlockAVX2 with
|
||||
// random valid LZ4 blocks and compares the output bit-for-bit against the
|
||||
// portable Go reference.
|
||||
func TestDifferentialLZ4Fuzz(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
const iterations = 5000
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wantSize := 1 + rng.Intn(4096)
|
||||
src := genLZ4Block(rng, wantSize)
|
||||
dstSize := wantSize + 64 // generous destination
|
||||
|
||||
// Go reference.
|
||||
goDst := make([]byte, dstSize)
|
||||
goN, goCode := decodeBlockGo(src, goDst)
|
||||
|
||||
// JIT kernel.
|
||||
jitDst := make([]byte, dstSize)
|
||||
jitN, jitCode := callDecodeBlockAVX2(t, k, src, jitDst)
|
||||
|
||||
if jitCode != goCode {
|
||||
t.Fatalf("iter %d: code mismatch: JIT=%d, Go=%d (src len=%d)",
|
||||
i, jitCode, goCode, len(src))
|
||||
}
|
||||
if jitCode != 0 {
|
||||
continue // both agree it's malformed/zero-offset
|
||||
}
|
||||
if jitN != goN {
|
||||
t.Fatalf("iter %d: n mismatch: JIT=%d, Go=%d (src len=%d)",
|
||||
i, jitN, goN, len(src))
|
||||
}
|
||||
if !bytes.Equal(jitDst[:jitN], goDst[:goN]) {
|
||||
t.Fatalf("iter %d: output mismatch (n=%d, src len=%d)", i, jitN, len(src))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDifferentialLZ4Hostile drives the kernel with random garbage to check
|
||||
// that error codes agree with the Go reference (no crashes, same classification).
|
||||
func TestDifferentialLZ4Hostile(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
const iterations = 2000
|
||||
rng := rand.New(rand.NewSource(99))
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
srcLen := rng.Intn(128)
|
||||
src := make([]byte, srcLen)
|
||||
rng.Read(src)
|
||||
dstSize := rng.Intn(512)
|
||||
dst := make([]byte, dstSize)
|
||||
|
||||
// Go reference.
|
||||
goDst := make([]byte, dstSize)
|
||||
copy(goDst, dst)
|
||||
_, goCode := decodeBlockGo(src, goDst)
|
||||
|
||||
// JIT kernel.
|
||||
jitDst := make([]byte, dstSize)
|
||||
copy(jitDst, dst)
|
||||
_, jitCode := callDecodeBlockAVX2(t, k, src, jitDst)
|
||||
|
||||
if jitCode != goCode {
|
||||
t.Fatalf("iter %d: hostile code mismatch: JIT=%d, Go=%d (srcLen=%d, dstSize=%d)",
|
||||
i, jitCode, goCode, srcLen, dstSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// callDecodeBlockAVX2Raw is like callDecodeBlockAVX2 but accepts explicit
|
||||
// dst size (for hostile tests where dst may be smaller than the output).
|
||||
func callDecodeBlockAVX2Raw(t *testing.T, k *Kernel, src, dst []byte) (int, int) {
|
||||
t.Helper()
|
||||
args := make([]byte, 64)
|
||||
if len(src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(src)))
|
||||
PutUint64(args, 16, uint64(cap(src)))
|
||||
if len(dst) > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(len(dst)))
|
||||
PutUint64(args, 40, uint64(cap(dst)))
|
||||
|
||||
out, err := k.CallFunc("decodeBlockAVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFunc(decodeBlockAVX2): %v", err)
|
||||
}
|
||||
return int(GetUint64(out, 48)), int(GetUint64(out, 56))
|
||||
}
|
||||
@@ -1,585 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const flacKernelPath = "../../go-libraries/go-flac/avx2_amd64.s"
|
||||
|
||||
func loadFLACKernel(t *testing.T) *Kernel {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(flacKernelPath); err != nil {
|
||||
t.Skipf("sibling kernel not available: %v", err)
|
||||
}
|
||||
k, err := Load(flacKernelPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load(%s): %v", flacKernelPath, err)
|
||||
}
|
||||
t.Cleanup(k.Close)
|
||||
return k
|
||||
}
|
||||
|
||||
// --- Portable Go references (from go-flac/simd.go) ---
|
||||
|
||||
func decodeMono16Go(src []byte, dst []int32) {
|
||||
for i := 0; i < len(dst); i++ {
|
||||
dst[i] = int32(int16(uint16(src[2*i]) | uint16(src[2*i+1])<<8))
|
||||
}
|
||||
}
|
||||
|
||||
func pack16Go(dst []byte, src []int32) {
|
||||
for i, v := range src {
|
||||
dst[2*i] = byte(v)
|
||||
dst[2*i+1] = byte(v >> 8)
|
||||
}
|
||||
}
|
||||
|
||||
func decorrelateLeftSideGo(left, right, out []int32) {
|
||||
for i := range left {
|
||||
l := left[i]
|
||||
out[2*i] = l
|
||||
out[2*i+1] = l - right[i]
|
||||
}
|
||||
}
|
||||
|
||||
func decorrelateSideRightGo(left, right, out []int32) {
|
||||
for i := range left {
|
||||
side := left[i]
|
||||
rch := right[i]
|
||||
out[2*i] = rch + side
|
||||
out[2*i+1] = rch
|
||||
}
|
||||
}
|
||||
|
||||
func decorrelateMidSideGo(left, right, out []int32) {
|
||||
for i := range left {
|
||||
mid := left[i]
|
||||
side := right[i]
|
||||
mid2 := mid<<1 | (side & 1)
|
||||
out[2*i] = (mid2 + side) >> 1
|
||||
out[2*i+1] = (mid2 - side) >> 1
|
||||
}
|
||||
}
|
||||
|
||||
func decorrelateInterleaveGo(left, right, out []int32) {
|
||||
for i := range left {
|
||||
out[2*i] = left[i]
|
||||
out[2*i+1] = right[i]
|
||||
}
|
||||
}
|
||||
|
||||
func analyzeO1RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
|
||||
swin = swin[:len(dstP)+1]
|
||||
for j := 0; j+1 < len(swin); j++ {
|
||||
r := swin[j+1] - swin[j]
|
||||
if r == -2147483648 { // math.MinInt32
|
||||
overflow = true
|
||||
}
|
||||
f := uint32(r<<1) ^ uint32(r>>31)
|
||||
dstP[j] = f
|
||||
partSum += uint64(f)
|
||||
bl := 0
|
||||
for v := f; v > 0; v >>= 1 {
|
||||
bl++
|
||||
}
|
||||
if bl > 31 {
|
||||
bl = 31
|
||||
}
|
||||
hist[bl]++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func analyzeO2RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
|
||||
swin = swin[:len(dstP)+2]
|
||||
for j := 0; j+2 < len(swin); j++ {
|
||||
r := swin[j+2] - 2*swin[j+1] + swin[j]
|
||||
if r == -2147483648 {
|
||||
overflow = true
|
||||
}
|
||||
f := uint32(r<<1) ^ uint32(r>>31)
|
||||
dstP[j] = f
|
||||
partSum += uint64(f)
|
||||
bl := 0
|
||||
for v := f; v > 0; v >>= 1 {
|
||||
bl++
|
||||
}
|
||||
if bl > 31 {
|
||||
bl = 31
|
||||
}
|
||||
hist[bl]++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func analyzeResRangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
|
||||
for j := 0; j < len(swin); j++ {
|
||||
r := swin[j]
|
||||
if r == -2147483648 {
|
||||
overflow = true
|
||||
}
|
||||
f := uint32(r<<1) ^ uint32(r>>31)
|
||||
dstP[j] = f
|
||||
partSum += uint64(f)
|
||||
bl := 0
|
||||
for v := f; v > 0; v >>= 1 {
|
||||
bl++
|
||||
}
|
||||
if bl > 31 {
|
||||
bl = 31
|
||||
}
|
||||
hist[bl]++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decodeMono24Go(src []byte, dst []int32) {
|
||||
for i := 0; i < len(dst); i++ {
|
||||
off := 3 * i
|
||||
u := uint32(src[off]) | uint32(src[off+1])<<8 | uint32(src[off+2])<<16
|
||||
dst[i] = int32(u<<8) >> 8
|
||||
}
|
||||
}
|
||||
|
||||
func decodeStereo16Go(src []byte, left, right []int32) {
|
||||
for i := 0; i < len(left); i++ {
|
||||
left[i] = int32(int16(uint16(src[4*i]) | uint16(src[4*i+1])<<8))
|
||||
right[i] = int32(int16(uint16(src[4*i+2]) | uint16(src[4*i+3])<<8))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Differential tests ---
|
||||
|
||||
func TestFLACDecodeMono16(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(7))
|
||||
|
||||
for iter := 0; iter < 500; iter++ {
|
||||
n := rng.Intn(256)
|
||||
src := make([]byte, 2*n)
|
||||
rng.Read(src)
|
||||
|
||||
goDst := make([]int32, n)
|
||||
decodeMono16Go(src, goDst)
|
||||
|
||||
jitDst := make([]int32, n)
|
||||
args := make([]byte, 48)
|
||||
if len(src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(src)))
|
||||
PutUint64(args, 16, uint64(cap(src)))
|
||||
if n > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(jitDst)))
|
||||
|
||||
_, err := k.CallFunc("decodeMono16AVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
for i := range goDst {
|
||||
if jitDst[i] != goDst[i] {
|
||||
t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACPack16(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(13))
|
||||
|
||||
for iter := 0; iter < 500; iter++ {
|
||||
n := rng.Intn(256)
|
||||
src := make([]int32, n)
|
||||
for i := range src {
|
||||
src[i] = int32(rng.Intn(65536) - 32768)
|
||||
}
|
||||
|
||||
goDst := make([]byte, 2*n)
|
||||
pack16Go(goDst, src)
|
||||
|
||||
jitDst := make([]byte, 2*n)
|
||||
args := make([]byte, 48)
|
||||
if len(jitDst) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&jitDst[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(jitDst)))
|
||||
PutUint64(args, 16, uint64(cap(jitDst)))
|
||||
if n > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(src)))
|
||||
|
||||
_, err := k.CallFunc("pack16AVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
if !bytes.Equal(jitDst, goDst) {
|
||||
t.Fatalf("iter %d: output mismatch (n=%d)", iter, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACDecorrelate(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(21))
|
||||
|
||||
kernels := []struct {
|
||||
name string
|
||||
ref func(left, right, out []int32)
|
||||
}{
|
||||
{"decorrelateLeftSideAVX2", decorrelateLeftSideGo},
|
||||
{"decorrelateSideRightAVX2", decorrelateSideRightGo},
|
||||
{"decorrelateMidSideAVX2", decorrelateMidSideGo},
|
||||
{"decorrelateInterleaveAVX2", decorrelateInterleaveGo},
|
||||
}
|
||||
|
||||
for _, kk := range kernels {
|
||||
t.Run(kk.name, func(t *testing.T) {
|
||||
for iter := 0; iter < 200; iter++ {
|
||||
n := rng.Intn(128)
|
||||
left := make([]int32, n)
|
||||
right := make([]int32, n)
|
||||
for i := range left {
|
||||
left[i] = int32(rng.Intn(1<<24) - 1<<23)
|
||||
right[i] = int32(rng.Intn(1<<24) - 1<<23)
|
||||
}
|
||||
|
||||
goOut := make([]int32, 2*n)
|
||||
kk.ref(left, right, goOut)
|
||||
|
||||
jitOut := make([]int32, 2*n)
|
||||
args := make([]byte, 72)
|
||||
if n > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&left[0]))
|
||||
PutPtr(args, 24, unsafe.Pointer(&right[0]))
|
||||
PutPtr(args, 48, unsafe.Pointer(&jitOut[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(n))
|
||||
PutUint64(args, 16, uint64(cap(left)))
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(right)))
|
||||
PutUint64(args, 56, uint64(2*n))
|
||||
PutUint64(args, 64, uint64(cap(jitOut)))
|
||||
|
||||
_, err := k.CallFunc(kk.name, args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
for i := range goOut {
|
||||
if jitOut[i] != goOut[i] {
|
||||
t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitOut[i], goOut[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACAnalyzeO1Range(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(33))
|
||||
|
||||
for iter := 0; iter < 300; iter++ {
|
||||
n := 1 + rng.Intn(128) // partition size
|
||||
swin := make([]int32, n+1)
|
||||
for i := range swin {
|
||||
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
|
||||
}
|
||||
|
||||
goDstP := make([]uint32, n)
|
||||
var goHist [32]uint16
|
||||
goSum, goOvf := analyzeO1RangeGo(swin, goDstP, &goHist)
|
||||
|
||||
jitDstP := make([]uint32, n)
|
||||
var jitHist [32]uint16
|
||||
args := make([]byte, 72) // 65 rounded up
|
||||
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
|
||||
PutUint64(args, 8, uint64(len(swin)))
|
||||
PutUint64(args, 16, uint64(cap(swin)))
|
||||
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(jitDstP)))
|
||||
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
|
||||
|
||||
out, err := k.CallFunc("analyzeO1RangeAVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
jitSum := GetUint64(out, 56)
|
||||
jitOvf := out[64] != 0
|
||||
|
||||
if jitSum != goSum {
|
||||
t.Fatalf("iter %d: partSum mismatch: JIT=%d Go=%d", iter, jitSum, goSum)
|
||||
}
|
||||
if jitOvf != goOvf {
|
||||
t.Fatalf("iter %d: overflow mismatch: JIT=%v Go=%v", iter, jitOvf, goOvf)
|
||||
}
|
||||
for i := range goDstP {
|
||||
if jitDstP[i] != goDstP[i] {
|
||||
t.Fatalf("iter %d: dstP[%d] mismatch: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
|
||||
}
|
||||
}
|
||||
if jitHist != goHist {
|
||||
t.Fatalf("iter %d: hist mismatch: JIT=%v Go=%v", iter, jitHist, goHist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACFastStereoSums(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(44))
|
||||
|
||||
for iter := 0; iter < 300; iter++ {
|
||||
n := 1 + rng.Intn(256)
|
||||
left := make([]int32, n)
|
||||
right := make([]int32, n)
|
||||
for i := range left {
|
||||
left[i] = int32(rng.Intn(1<<24) - 1<<23)
|
||||
right[i] = int32(rng.Intn(1<<24) - 1<<23)
|
||||
}
|
||||
|
||||
// Go reference: compute the four sums.
|
||||
var goSums [4]uint64
|
||||
for i := 0; i < n; i++ {
|
||||
l := left[i]
|
||||
r := right[i]
|
||||
side := l - r
|
||||
mid := (l + r) >> 1
|
||||
goSums[0] += foldAbs(l) + foldAbs(r)
|
||||
goSums[1] += foldAbs(l) + foldAbs(side)
|
||||
goSums[2] += foldAbs(side) + foldAbs(r)
|
||||
goSums[3] += foldAbs(mid) + foldAbs(side)
|
||||
}
|
||||
|
||||
var jitSums [4]uint64
|
||||
args := make([]byte, 56)
|
||||
PutPtr(args, 0, unsafe.Pointer(&left[0]))
|
||||
PutUint64(args, 8, uint64(n))
|
||||
PutUint64(args, 16, uint64(cap(left)))
|
||||
PutPtr(args, 24, unsafe.Pointer(&right[0]))
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(right)))
|
||||
PutPtr(args, 48, unsafe.Pointer(&jitSums[0]))
|
||||
|
||||
_, err := k.CallFunc("fastStereoSumsAVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
if jitSums != goSums {
|
||||
t.Fatalf("iter %d: sums mismatch:\n JIT=%v\n Go =%v", iter, jitSums, goSums)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func foldAbs(v int32) uint64 {
|
||||
return uint64(uint32(v<<1) ^ uint32(v>>31))
|
||||
}
|
||||
|
||||
// runAnalyzeTest is the shared harness for the analyzeO*Range family.
|
||||
func runAnalyzeTest(t *testing.T, k *Kernel, name string, order int, ref func([]int32, []uint32, *[32]uint16) (uint64, bool)) {
|
||||
t.Helper()
|
||||
rng := rand.New(rand.NewSource(int64(order)*100 + 7))
|
||||
for iter := 0; iter < 200; iter++ {
|
||||
n := 1 + rng.Intn(128)
|
||||
swin := make([]int32, n+order)
|
||||
for i := range swin {
|
||||
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
|
||||
}
|
||||
|
||||
goDstP := make([]uint32, n)
|
||||
var goHist [32]uint16
|
||||
goSum, goOvf := ref(swin, goDstP, &goHist)
|
||||
|
||||
jitDstP := make([]uint32, n)
|
||||
var jitHist [32]uint16
|
||||
args := make([]byte, 72)
|
||||
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
|
||||
PutUint64(args, 8, uint64(len(swin)))
|
||||
PutUint64(args, 16, uint64(cap(swin)))
|
||||
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(jitDstP)))
|
||||
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
|
||||
|
||||
out, err := k.CallFunc(name, args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
jitSum := GetUint64(out, 56)
|
||||
jitOvf := out[64] != 0
|
||||
|
||||
if jitSum != goSum {
|
||||
t.Fatalf("iter %d: partSum: JIT=%d Go=%d", iter, jitSum, goSum)
|
||||
}
|
||||
if jitOvf != goOvf {
|
||||
t.Fatalf("iter %d: overflow: JIT=%v Go=%v", iter, jitOvf, goOvf)
|
||||
}
|
||||
for i := range goDstP {
|
||||
if jitDstP[i] != goDstP[i] {
|
||||
t.Fatalf("iter %d: dstP[%d]: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
|
||||
}
|
||||
}
|
||||
if jitHist != goHist {
|
||||
t.Fatalf("iter %d: hist mismatch", iter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACAnalyzeO2Range(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
runAnalyzeTest(t, k, "analyzeO2RangeAVX2", 2, analyzeO2RangeGo)
|
||||
}
|
||||
|
||||
func TestFLACAnalyzeResRange(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
// analyzeResRange has order 0: swin IS the residual (no prediction).
|
||||
runAnalyzeTest(t, k, "analyzeResRangeAVX2", 0, analyzeResRangeGo)
|
||||
}
|
||||
|
||||
func TestFLACAnalyzeO3Range(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
|
||||
swin = swin[:len(dstP)+3]
|
||||
var partSum uint64
|
||||
var overflow bool
|
||||
for j := 0; j+3 < len(swin); j++ {
|
||||
r := swin[j+3] - 3*swin[j+2] + 3*swin[j+1] - swin[j]
|
||||
if r == -2147483648 {
|
||||
overflow = true
|
||||
}
|
||||
f := uint32(r<<1) ^ uint32(r>>31)
|
||||
dstP[j] = f
|
||||
partSum += uint64(f)
|
||||
bl := 0
|
||||
for v := f; v > 0; v >>= 1 {
|
||||
bl++
|
||||
}
|
||||
if bl > 31 {
|
||||
bl = 31
|
||||
}
|
||||
hist[bl]++
|
||||
}
|
||||
return partSum, overflow
|
||||
}
|
||||
runAnalyzeTest(t, k, "analyzeO3RangeAVX2", 3, ref)
|
||||
}
|
||||
|
||||
func TestFLACAnalyzeO4Range(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
|
||||
swin = swin[:len(dstP)+4]
|
||||
var partSum uint64
|
||||
var overflow bool
|
||||
for j := 0; j+4 < len(swin); j++ {
|
||||
r := swin[j+4] - 4*swin[j+3] + 6*swin[j+2] - 4*swin[j+1] + swin[j]
|
||||
if r == -2147483648 {
|
||||
overflow = true
|
||||
}
|
||||
f := uint32(r<<1) ^ uint32(r>>31)
|
||||
dstP[j] = f
|
||||
partSum += uint64(f)
|
||||
bl := 0
|
||||
for v := f; v > 0; v >>= 1 {
|
||||
bl++
|
||||
}
|
||||
if bl > 31 {
|
||||
bl = 31
|
||||
}
|
||||
hist[bl]++
|
||||
}
|
||||
return partSum, overflow
|
||||
}
|
||||
runAnalyzeTest(t, k, "analyzeO4RangeAVX2", 4, ref)
|
||||
}
|
||||
|
||||
func TestFLACDecodeMono24(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(55))
|
||||
|
||||
for iter := 0; iter < 500; iter++ {
|
||||
n := rng.Intn(256)
|
||||
src := make([]byte, 3*n)
|
||||
rng.Read(src)
|
||||
|
||||
goDst := make([]int32, n)
|
||||
decodeMono24Go(src, goDst)
|
||||
|
||||
jitDst := make([]int32, n)
|
||||
args := make([]byte, 48)
|
||||
if len(src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(src)))
|
||||
PutUint64(args, 16, uint64(cap(src)))
|
||||
if n > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(jitDst)))
|
||||
|
||||
_, err := k.CallFunc("decodeMono24AVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
for i := range goDst {
|
||||
if jitDst[i] != goDst[i] {
|
||||
t.Fatalf("iter %d: dst[%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFLACDecodeStereo16(t *testing.T) {
|
||||
k := loadFLACKernel(t)
|
||||
rng := rand.New(rand.NewSource(66))
|
||||
|
||||
for iter := 0; iter < 500; iter++ {
|
||||
n := rng.Intn(256)
|
||||
src := make([]byte, 4*n) // [L0,R0,L1,R1,...]
|
||||
rng.Read(src)
|
||||
|
||||
goLeft := make([]int32, n)
|
||||
goRight := make([]int32, n)
|
||||
decodeStereo16Go(src, goLeft, goRight)
|
||||
|
||||
jitLeft := make([]int32, n)
|
||||
jitRight := make([]int32, n)
|
||||
args := make([]byte, 72)
|
||||
if len(src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(src)))
|
||||
PutUint64(args, 16, uint64(cap(src)))
|
||||
if n > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&jitLeft[0]))
|
||||
PutPtr(args, 48, unsafe.Pointer(&jitRight[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(cap(jitLeft)))
|
||||
PutUint64(args, 56, uint64(n))
|
||||
PutUint64(args, 64, uint64(cap(jitRight)))
|
||||
|
||||
_, err := k.CallFunc("decodeStereo16AVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("iter %d: %v", iter, err)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
if jitLeft[i] != goLeft[i] || jitRight[i] != goRight[i] {
|
||||
t.Fatalf("iter %d: [%d] L: JIT=%d Go=%d; R: JIT=%d Go=%d",
|
||||
iter, i, jitLeft[i], goLeft[i], jitRight[i], goRight[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-18
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
@@ -20,6 +21,7 @@ type FuzzResult struct {
|
||||
Matches int
|
||||
Mismatches int
|
||||
FirstFail string // description of the first mismatch ("" if none)
|
||||
CrashInput []byte // input that caused the last crash/mismatch (nil if none)
|
||||
}
|
||||
|
||||
// OK returns true when all iterations matched.
|
||||
@@ -30,8 +32,12 @@ func (r FuzzResult) String() string {
|
||||
if r.OK() {
|
||||
return fmt.Sprintf("%s: %d/%d iterations match", r.Func, r.Matches, r.Iterations)
|
||||
}
|
||||
return fmt.Sprintf("%s: %d/%d match, %d MISMATCH — %s",
|
||||
s := fmt.Sprintf("%s: %d/%d match, %d MISMATCH — %s",
|
||||
r.Func, r.Matches, r.Iterations, r.Mismatches, r.FirstFail)
|
||||
if len(r.CrashInput) > 0 {
|
||||
s += fmt.Sprintf("\n input: %x", r.CrashInput)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// funcSig is a parsed // func signature from the assembly source.
|
||||
@@ -74,22 +80,39 @@ func parseParams(s string) []param {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.Split(s, ",")
|
||||
// First pass: extract the type from each field (if present).
|
||||
types := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
parts := strings.Fields(strings.TrimSpace(field))
|
||||
if len(parts) >= 2 {
|
||||
types[i] = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
// Propagate types backward: a field without a type inherits from the next
|
||||
// field that has one (e.g. "dst" inherits "[]byte" from "src []byte").
|
||||
for i := range fields {
|
||||
if types[i] == "" {
|
||||
for j := i + 1; j < len(fields); j++ {
|
||||
if types[j] != "" {
|
||||
types[i] = types[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []param
|
||||
for _, field := range strings.Split(s, ",") {
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(field)
|
||||
if len(parts) == 1 {
|
||||
// Unnamed: "int" or "[]byte".
|
||||
typ := types[i]
|
||||
if typ == "" {
|
||||
out = append(out, param{typ: parts[0]})
|
||||
} else {
|
||||
// Named: "a []byte" or shared "a, b []int32" (handled by the
|
||||
// comma split above — "a" alone means the type follows in the
|
||||
// next field; this is a simplification that covers the common
|
||||
// case where each param has its own type).
|
||||
out = append(out, param{name: parts[0], typ: parts[1]})
|
||||
out = append(out, param{name: parts[0], typ: typ})
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -160,6 +183,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
||||
// (e.g. histogram increments) don't corrupt the other's input.
|
||||
gasmArgs, goArgs, bufs := genDualArgs(rng, sig, fl.Args)
|
||||
|
||||
// Save the current input for crash diagnostics.
|
||||
result.CrashInput = gasmArgs
|
||||
|
||||
// Call the gasm version.
|
||||
gasmOut, err := k.CallFunc(name, gasmArgs)
|
||||
if err != nil {
|
||||
@@ -167,7 +193,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
||||
if result.FirstFail == "" {
|
||||
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
|
||||
}
|
||||
releaseBufs(bufs)
|
||||
runtime.KeepAlive(bufs)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -178,7 +204,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
||||
if result.FirstFail == "" {
|
||||
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
|
||||
}
|
||||
releaseBufs(bufs)
|
||||
runtime.KeepAlive(bufs)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -196,7 +222,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
||||
} else {
|
||||
result.Matches++
|
||||
}
|
||||
releaseBufs(bufs)
|
||||
runtime.KeepAlive(bufs)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -359,9 +385,3 @@ func equalBytes(a, b []byte) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func releaseBufs(bufs [][]byte) {
|
||||
// Keep buffers alive until after the call; nothing to free in Go,
|
||||
// but this prevents the compiler from collecting them too early.
|
||||
_ = bufs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFuzzResultString(t *testing.T) {
|
||||
t.Run("ok", func(t *testing.T) {
|
||||
r := FuzzResult{Func: "add", Iterations: 100, Matches: 100}
|
||||
s := r.String()
|
||||
if s != "add: 100/100 iterations match" {
|
||||
t.Errorf("String() = %q", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mismatch", func(t *testing.T) {
|
||||
r := FuzzResult{Func: "mul", Iterations: 100, Matches: 95, Mismatches: 5, FirstFail: "iter 23"}
|
||||
s := r.String()
|
||||
if s != "mul: 95/100 match, 5 MISMATCH — iter 23" {
|
||||
t.Errorf("String() = %q", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("crash", func(t *testing.T) {
|
||||
r := FuzzResult{Func: "dec", Iterations: 100, Matches: 99, Mismatches: 1, FirstFail: "SIGSEGV", CrashInput: []byte{0x01, 0x02}}
|
||||
s := r.String()
|
||||
if s != "dec: 99/100 match, 1 MISMATCH — SIGSEGV\n input: 0102" {
|
||||
t.Errorf("String() = %q", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ string
|
||||
want int
|
||||
}{
|
||||
{"*[32]uint16", 32},
|
||||
{"*[16]int32", 16},
|
||||
{"bad", 1},
|
||||
{"*[]", 1},
|
||||
{"*[0x]", 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := arrayLen(tt.typ); got != tt.want {
|
||||
t.Errorf("arrayLen(%q) = %d, want %d", tt.typ, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestElemSizeFor(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ string
|
||||
want int
|
||||
}{
|
||||
{"[]byte", 1}, {"[]uint8", 1}, {"[]int8", 1},
|
||||
{"[]uint16", 2}, {"[]int16", 2},
|
||||
{"[]uint32", 4}, {"[]int32", 4}, {"[]float32", 4},
|
||||
{"[]uint64", 8}, {"[]int64", 8}, {"[]float64", 8},
|
||||
{"[]unknown", 8},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := elemSizeFor(tt.typ); got != tt.want {
|
||||
t.Errorf("elemSizeFor(%q) = %d, want %d", tt.typ, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualBytes(t *testing.T) {
|
||||
if !equalBytes([]byte{1, 2, 3}, []byte{1, 2, 3}) {
|
||||
t.Error("expected equal")
|
||||
}
|
||||
if equalBytes([]byte{1, 2}, []byte{1, 2, 3}) {
|
||||
t.Error("different length: expected not equal")
|
||||
}
|
||||
if equalBytes([]byte{1, 2, 3}, []byte{1, 2, 4}) {
|
||||
t.Error("different content: expected not equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamsSize(t *testing.T) {
|
||||
sig := funcSig{
|
||||
name: "test",
|
||||
params: []param{{name: "a", typ: "[]byte"}, {name: "b", typ: "int"}},
|
||||
results: []param{{name: "n", typ: "int"}},
|
||||
}
|
||||
if got := paramsSize(sig); got != 32 {
|
||||
t.Errorf("paramsSize = %d, want 32 (24 for slice + 8 for int)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockCount(t *testing.T) {
|
||||
k := loadBasic(t)
|
||||
n, err := k.BlockCount("sum")
|
||||
if err != nil {
|
||||
t.Fatalf("BlockCount(sum): %v", err)
|
||||
}
|
||||
if n < 2 {
|
||||
t.Errorf("sum: expected at least 2 blocks, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillBuffer(t *testing.T) {
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
// fillBuffer("zero") is a no-op — relies on make already zeroing.
|
||||
buf := make([]byte, 16)
|
||||
fillBuffer(buf, "zero")
|
||||
for _, b := range buf {
|
||||
if b != 0 {
|
||||
t.Error("zero pattern: make should produce zeroed buffer")
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("ones", func(t *testing.T) {
|
||||
buf := make([]byte, 16)
|
||||
fillBuffer(buf, "ones")
|
||||
for _, b := range buf {
|
||||
if b != 0xFF {
|
||||
t.Error("ones pattern should fill with 0xFF")
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("seq", func(t *testing.T) {
|
||||
buf := make([]byte, 256)
|
||||
fillBuffer(buf, "seq")
|
||||
for i, b := range buf {
|
||||
if b != byte(i) {
|
||||
t.Errorf("seq[%d] = %d, want %d", i, b, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("hex", func(t *testing.T) {
|
||||
buf := make([]byte, 6)
|
||||
fillBuffer(buf, "deadbeef")
|
||||
want := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD}
|
||||
for i, b := range buf {
|
||||
if b != want[i] {
|
||||
t.Errorf("hex[%d] = %02x, want %02x", i, b, want[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFuzzFuncChecked(t *testing.T) {
|
||||
k := loadBasic(t)
|
||||
sig := funcSig{
|
||||
name: "sum",
|
||||
params: []param{{name: "data", typ: "[]int64"}},
|
||||
results: []param{{name: "r", typ: "int64"}},
|
||||
}
|
||||
result := k.FuzzFuncChecked("sum", sig, 10, 0)
|
||||
if !result.OK() {
|
||||
t.Errorf("FuzzFuncChecked(sum): %s", result)
|
||||
}
|
||||
// Test with non-existent function — should report failure.
|
||||
result = k.FuzzFuncChecked("nope", sig, 10, 0)
|
||||
if result.OK() {
|
||||
t.Error("FuzzFuncChecked(nope): expected failure")
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -68,6 +69,7 @@ func TestJITSum(t *testing.T) {
|
||||
PutUint64(args, 16, uint64(cap(tt.data)))
|
||||
|
||||
out, err := k.CallFunc("sum", args)
|
||||
runtime.KeepAlive(tt.data)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFunc(sum, %v): %v", tt.data, err)
|
||||
}
|
||||
@@ -111,6 +113,8 @@ func TestJITWideCopy(t *testing.T) {
|
||||
PutUint64(args, 40, uint64(tt.n)) // src_cap
|
||||
|
||||
_, err := k.CallFunc("wideCopy", args)
|
||||
runtime.KeepAlive(dst)
|
||||
runtime.KeepAlive(src)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFunc(wideCopy): %v", err)
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// lz4KernelPath is the sibling repository's AVX2 kernel, used for
|
||||
// integration testing. The test is skipped when the file is absent
|
||||
// (e.g. in CI without the sibling checkout).
|
||||
const lz4KernelPath = "../../go-libraries/go-lz4/avx2_amd64.s"
|
||||
|
||||
func loadLZ4Kernel(t *testing.T) *Kernel {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(lz4KernelPath); err != nil {
|
||||
t.Skipf("sibling kernel not available: %v", err)
|
||||
}
|
||||
k, err := Load(lz4KernelPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load(%s): %v", lz4KernelPath, err)
|
||||
}
|
||||
t.Cleanup(k.Close)
|
||||
return k
|
||||
}
|
||||
|
||||
// callDecodeBlockAVX2 invokes the JIT-assembled decodeBlockAVX2 with the
|
||||
// given src and dst buffers, returning (n, code).
|
||||
func callDecodeBlockAVX2(t *testing.T, k *Kernel, src, dst []byte) (int, int) {
|
||||
t.Helper()
|
||||
args := make([]byte, 64)
|
||||
if len(src) > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(len(src)))
|
||||
PutUint64(args, 16, uint64(cap(src)))
|
||||
if len(dst) > 0 {
|
||||
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||
}
|
||||
PutUint64(args, 32, uint64(len(dst)))
|
||||
PutUint64(args, 40, uint64(cap(dst)))
|
||||
|
||||
out, err := k.CallFunc("decodeBlockAVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFunc(decodeBlockAVX2): %v", err)
|
||||
}
|
||||
return int(GetUint64(out, 48)), int(GetUint64(out, 56))
|
||||
}
|
||||
|
||||
func TestLZ4DecodeKnownAnswers(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
src []byte
|
||||
dstSize int
|
||||
wantDst []byte
|
||||
wantN int
|
||||
wantCode int
|
||||
}{
|
||||
{
|
||||
name: "literals_only",
|
||||
src: []byte{0x50, 'H', 'e', 'l', 'l', 'o'},
|
||||
dstSize: 16,
|
||||
wantDst: []byte("Hello"),
|
||||
wantN: 5,
|
||||
wantCode: 0,
|
||||
},
|
||||
{
|
||||
name: "literals_and_match",
|
||||
src: []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'},
|
||||
dstSize: 32,
|
||||
wantDst: []byte("AAAAAAAAAAAAABBB"),
|
||||
wantN: 16,
|
||||
wantCode: 0,
|
||||
},
|
||||
{
|
||||
name: "overlapping_match",
|
||||
// 1 literal 'X', then match offset=1 length=4+4=8 → "XXXXXXXXX",
|
||||
// then final 1 literal 'Y'.
|
||||
src: []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'},
|
||||
dstSize: 16,
|
||||
wantDst: []byte("XXXXXXXXXY"),
|
||||
wantN: 10,
|
||||
wantCode: 0,
|
||||
},
|
||||
{
|
||||
name: "malformed_truncated",
|
||||
src: []byte{0x50, 'H', 'e'}, // claims 5 literals, has 2
|
||||
dstSize: 16,
|
||||
wantN: 0,
|
||||
wantCode: 1,
|
||||
},
|
||||
{
|
||||
name: "zero_offset",
|
||||
src: []byte{0x14, 'X', 0x00, 0x00},
|
||||
dstSize: 16,
|
||||
wantN: 0,
|
||||
wantCode: 2,
|
||||
},
|
||||
{
|
||||
name: "empty_token",
|
||||
src: []byte{0x00}, // 0 literals, end of block
|
||||
dstSize: 16,
|
||||
wantDst: nil,
|
||||
wantN: 0,
|
||||
wantCode: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dst := make([]byte, tt.dstSize)
|
||||
n, code := callDecodeBlockAVX2(t, k, tt.src, dst)
|
||||
if n != tt.wantN || code != tt.wantCode {
|
||||
t.Fatalf("decodeBlockAVX2: got (n=%d, code=%d), want (n=%d, code=%d)",
|
||||
n, code, tt.wantN, tt.wantCode)
|
||||
}
|
||||
if tt.wantCode == 0 && tt.wantDst != nil {
|
||||
if !bytes.Equal(dst[:n], tt.wantDst) {
|
||||
t.Errorf("output mismatch:\n got %q\n want %q", dst[:n], tt.wantDst)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLZ4WideCopyAVX2(t *testing.T) {
|
||||
k := loadLZ4Kernel(t)
|
||||
|
||||
sizes := []int{0, 1, 15, 16, 31, 32, 33, 63, 64, 100, 256, 1024}
|
||||
for _, n := range sizes {
|
||||
src := make([]byte, n)
|
||||
for i := range src {
|
||||
src[i] = byte(i*13 + 7)
|
||||
}
|
||||
dst := make([]byte, n)
|
||||
|
||||
args := make([]byte, 48)
|
||||
if n > 0 {
|
||||
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
|
||||
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||
}
|
||||
PutUint64(args, 8, uint64(n))
|
||||
PutUint64(args, 16, uint64(n))
|
||||
PutUint64(args, 32, uint64(n))
|
||||
PutUint64(args, 40, uint64(n))
|
||||
|
||||
_, err := k.CallFunc("wideCopyAVX2", args)
|
||||
if err != nil {
|
||||
t.Fatalf("wideCopyAVX2(n=%d): %v", n, err)
|
||||
}
|
||||
if !bytes.Equal(dst, src) {
|
||||
t.Errorf("wideCopyAVX2(n=%d): output mismatch", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FuncSig is a parsed // func signature from the assembly source.
|
||||
type FuncSig struct {
|
||||
Name string
|
||||
Params []Param
|
||||
Results []Param
|
||||
}
|
||||
|
||||
// Param is a single function parameter.
|
||||
type Param struct {
|
||||
Name string
|
||||
Typ string // "[]byte", "[]int32", "int", "*[32]uint16", etc.
|
||||
}
|
||||
|
||||
// IsPointer reports whether the parameter type is a pointer or slice
|
||||
// (i.e., it needs a buffer address in the argument block).
|
||||
func (p Param) IsPointer() bool {
|
||||
return strings.HasPrefix(p.Typ, "[]") || strings.HasPrefix(p.Typ, "*")
|
||||
}
|
||||
|
||||
// ParseFuncSig extracts the function signature from a "// func ..." comment.
|
||||
func ParseFuncSig(comment string) (FuncSig, bool) {
|
||||
m := funcSigRe.FindStringSubmatch(strings.TrimSpace(comment))
|
||||
if m == nil {
|
||||
return FuncSig{}, false
|
||||
}
|
||||
sig := FuncSig{Name: m[1]}
|
||||
sig.Params = parseParamsExported(m[2])
|
||||
// Results may be "(a int, b int)" or "int" or "(int, error)".
|
||||
res := strings.TrimSpace(m[3])
|
||||
res = strings.TrimPrefix(res, "(")
|
||||
res = strings.TrimSuffix(res, ")")
|
||||
if res != "" {
|
||||
sig.Results = parseParamsExported(res)
|
||||
}
|
||||
return sig, true
|
||||
}
|
||||
|
||||
// parseParamsExported splits a parameter list like "a []byte, b []int32" or
|
||||
// "dst, src []byte" into typed parameters. Go syntax allows grouped names
|
||||
// where the type at the end applies to every name in the group:// "dst, src []byte" means both dst and src are []byte.
|
||||
func parseParamsExported(s string) []Param {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.Split(s, ",")
|
||||
// First pass: extract the type from each field (if present).
|
||||
types := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
parts := strings.Fields(strings.TrimSpace(field))
|
||||
if len(parts) >= 2 {
|
||||
types[i] = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
// Propagate types backward: a field without a type inherits the type from
|
||||
// the next field that has one (e.g. "dst" inherits "[]byte" from "src []byte").
|
||||
for i := range fields {
|
||||
if types[i] == "" {
|
||||
for j := i + 1; j < len(fields); j++ {
|
||||
if types[j] != "" {
|
||||
types[i] = types[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Second pass: build params.
|
||||
var out []Param
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(field)
|
||||
typ := types[i]
|
||||
if typ == "" {
|
||||
typ = parts[0] // unnamed: the whole field is the type
|
||||
out = append(out, Param{Typ: typ})
|
||||
} else {
|
||||
out = append(out, Param{Name: parts[0], Typ: typ})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ExtractFuncSig extracts the // func signature for the named function
|
||||
// from the assembly source.
|
||||
func ExtractFuncSig(src, funcName string) (FuncSig, bool) {
|
||||
lines := strings.Split(src, "\n")
|
||||
for i, line := range lines {
|
||||
if sig, ok := ParseFuncSig(line); ok && sig.Name == funcName {
|
||||
// Verify the next non-comment line is the TEXT directive.
|
||||
for j := i + 1; j < len(lines); j++ {
|
||||
trimmed := strings.TrimSpace(lines[j])
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "//") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "TEXT") {
|
||||
return sig, true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return FuncSig{}, false
|
||||
}
|
||||
|
||||
// ArgLayout computes the ABI0 argument layout for a function signature.
|
||||
// Returns the offset of each parameter in the argument block.
|
||||
func ArgLayout(sig FuncSig) []ArgOffset {
|
||||
var offsets []ArgOffset
|
||||
off := 0
|
||||
for _, p := range sig.Params {
|
||||
size := paramSize(p.Typ)
|
||||
offsets = append(offsets, ArgOffset{Name: p.Name, Typ: p.Typ, Offset: off, Size: size, IsPtr: p.IsPointer()})
|
||||
off += size
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// ArgOffset describes one parameter's position in the argument block.
|
||||
type ArgOffset struct {
|
||||
Name string
|
||||
Typ string
|
||||
Offset int
|
||||
Size int
|
||||
IsPtr bool
|
||||
}
|
||||
|
||||
// paramSize returns the size in bytes of a parameter type in the ABI0 layout.
|
||||
func paramSize(typ string) int {
|
||||
switch {
|
||||
case strings.HasPrefix(typ, "[]"):
|
||||
// Slice: pointer + length + capacity = 24 bytes.
|
||||
return 24
|
||||
case strings.HasPrefix(typ, "*"):
|
||||
// Pointer: 8 bytes.
|
||||
return 8
|
||||
case typ == "int", typ == "int64", typ == "uint64", typ == "uintptr":
|
||||
return 8
|
||||
case typ == "int32", typ == "uint32", typ == "float32":
|
||||
return 4
|
||||
case typ == "int16", typ == "uint16":
|
||||
return 2
|
||||
case typ == "int8", typ == "uint8", typ == "byte", typ == "bool":
|
||||
return 1
|
||||
default:
|
||||
// Default to 8 bytes for unknown types.
|
||||
return 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFuncSigExported(t *testing.T) {
|
||||
tests := []struct {
|
||||
comment string
|
||||
wantName string
|
||||
nParams int
|
||||
nResults int
|
||||
}{
|
||||
{"// func add(a int64, b int64) int64", "add", 2, 1},
|
||||
{"// func wideCopy(dst []byte, src []byte)", "wideCopy", 2, 0},
|
||||
{"// func decodeBlockAVX2(src, dst []byte) (n int, code int)", "decodeBlockAVX2", 2, 2},
|
||||
{"// func analyzeO1RangeAVX2(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool)", "analyzeO1RangeAVX2", 3, 2},
|
||||
{"// not a func", "", 0, 0},
|
||||
{"// func", "", 0, 0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
sig, ok := ParseFuncSig(tt.comment)
|
||||
if tt.wantName == "" {
|
||||
if ok {
|
||||
t.Errorf("ParseFuncSig(%q): expected not ok", tt.comment)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("ParseFuncSig(%q): expected ok", tt.comment)
|
||||
continue
|
||||
}
|
||||
if sig.Name != tt.wantName {
|
||||
t.Errorf("ParseFuncSig(%q).Name = %q, want %q", tt.comment, sig.Name, tt.wantName)
|
||||
}
|
||||
if len(sig.Params) != tt.nParams {
|
||||
t.Errorf("ParseFuncSig(%q): %d params, want %d", tt.comment, len(sig.Params), tt.nParams)
|
||||
}
|
||||
if len(sig.Results) != tt.nResults {
|
||||
t.Errorf("ParseFuncSig(%q): %d results, want %d", tt.comment, len(sig.Results), tt.nResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFuncSig(t *testing.T) {
|
||||
src := `// func add(a int64, b int64) int64
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
|
||||
// func wideCopyAVX2(dst, src []byte)
|
||||
TEXT ·wideCopyAVX2(SB), NOSPLIT, $0-48
|
||||
`
|
||||
sig, ok := ExtractFuncSig(src, "add")
|
||||
if !ok {
|
||||
t.Fatal("ExtractFuncSig(add): not found")
|
||||
}
|
||||
if sig.Name != "add" {
|
||||
t.Errorf("Name = %q, want %q", sig.Name, "add")
|
||||
}
|
||||
if len(sig.Params) != 2 {
|
||||
t.Errorf("params = %d, want 2", len(sig.Params))
|
||||
}
|
||||
|
||||
sig, ok = ExtractFuncSig(src, "wideCopyAVX2")
|
||||
if !ok {
|
||||
t.Fatal("ExtractFuncSig(wideCopyAVX2): not found")
|
||||
}
|
||||
if len(sig.Params) != 2 {
|
||||
t.Errorf("params = %d, want 2", len(sig.Params))
|
||||
}
|
||||
|
||||
_, ok = ExtractFuncSig(src, "nonexistent")
|
||||
if ok {
|
||||
t.Error("ExtractFuncSig(nonexistent): expected not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgLayout(t *testing.T) {
|
||||
sig := FuncSig{
|
||||
Name: "wideCopyAVX2",
|
||||
Params: []Param{
|
||||
{Name: "dst", Typ: "[]byte"},
|
||||
{Name: "src", Typ: "[]byte"},
|
||||
},
|
||||
}
|
||||
layout := ArgLayout(sig)
|
||||
if len(layout) != 2 {
|
||||
t.Fatalf("ArgLayout: %d entries, want 2", len(layout))
|
||||
}
|
||||
// dst: offset 0, size 24 (slice)
|
||||
if layout[0].Name != "dst" || layout[0].Offset != 0 || layout[0].Size != 24 || !layout[0].IsPtr {
|
||||
t.Errorf("layout[0] = %+v", layout[0])
|
||||
}
|
||||
// src: offset 24, size 24 (slice)
|
||||
if layout[1].Name != "src" || layout[1].Offset != 24 || layout[1].Size != 24 || !layout[1].IsPtr {
|
||||
t.Errorf("layout[1] = %+v", layout[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgLayoutMixed(t *testing.T) {
|
||||
sig := FuncSig{
|
||||
Name: "decodeBlockAVX2",
|
||||
Params: []Param{
|
||||
{Name: "src", Typ: "[]byte"},
|
||||
{Name: "dst", Typ: "[]byte"},
|
||||
},
|
||||
Results: []Param{
|
||||
{Name: "n", Typ: "int"},
|
||||
{Name: "code", Typ: "int"},
|
||||
},
|
||||
}
|
||||
layout := ArgLayout(sig)
|
||||
if len(layout) != 2 {
|
||||
t.Fatalf("ArgLayout: %d entries, want 2", len(layout))
|
||||
}
|
||||
// Two slices: 24 + 24 = 48 bytes of params.
|
||||
if layout[1].Offset != 24 {
|
||||
t.Errorf("src offset = %d, want 24", layout[1].Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ string
|
||||
want int
|
||||
}{
|
||||
{"[]byte", 24},
|
||||
{"[]int32", 24},
|
||||
{"*uint16", 8},
|
||||
{"*[32]uint16", 8},
|
||||
{"int", 8},
|
||||
{"int64", 8},
|
||||
{"uint64", 8},
|
||||
{"uintptr", 8},
|
||||
{"int32", 4},
|
||||
{"uint32", 4},
|
||||
{"float32", 4},
|
||||
{"int16", 2},
|
||||
{"uint16", 2},
|
||||
{"int8", 1},
|
||||
{"uint8", 1},
|
||||
{"byte", 1},
|
||||
{"bool", 1},
|
||||
{"string", 8}, // unknown type defaults to 8
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := paramSize(tt.typ); got != tt.want {
|
||||
t.Errorf("paramSize(%q) = %d, want %d", tt.typ, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ package verify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
@@ -114,6 +116,66 @@ func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, e
|
||||
return CallChecked(fnAddr, args)
|
||||
}
|
||||
|
||||
// FuzzFuncCheckedByName is like FuzzFuncChecked but extracts the signature
|
||||
// from the source code internally.
|
||||
func (k *Kernel) FuzzFuncCheckedByName(name, src string, iterations int, seed int64) FuzzResult {
|
||||
result := FuzzResult{Func: name, Iterations: iterations}
|
||||
sig, ok := ExtractSignatures(src)[name]
|
||||
if !ok {
|
||||
result.Mismatches = iterations
|
||||
result.FirstFail = "no // func signature found"
|
||||
return result
|
||||
}
|
||||
return k.FuzzFuncChecked(name, sig, iterations, seed)
|
||||
}
|
||||
|
||||
// FuzzFuncChecked combines fuzzing with ABI checks: it generates varied
|
||||
// inputs and verifies that callee-saved registers and the red zone are
|
||||
// preserved even on deep execution paths (not just early exits).
|
||||
func (k *Kernel) FuzzFuncChecked(name string, sig funcSig, iterations int, seed int64) FuzzResult {
|
||||
result := FuzzResult{Func: name, Iterations: iterations}
|
||||
rng := rand.New(rand.NewSource(seed))
|
||||
|
||||
fl, err := k.Func(name)
|
||||
if err != nil {
|
||||
result.Mismatches = iterations
|
||||
result.FirstFail = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
violations := 0
|
||||
for i := 0; i < iterations; i++ {
|
||||
gasmArgs, _, bufs := genDualArgs(rng, sig, fl.Args)
|
||||
result.CrashInput = gasmArgs
|
||||
|
||||
_, report, err := k.CallFuncChecked(name, gasmArgs)
|
||||
if err != nil {
|
||||
result.Mismatches++
|
||||
if result.FirstFail == "" {
|
||||
result.FirstFail = fmt.Sprintf("iter %d: call: %v", i, err)
|
||||
}
|
||||
runtime.KeepAlive(bufs)
|
||||
continue
|
||||
}
|
||||
|
||||
if !report.OK() {
|
||||
violations++
|
||||
result.Mismatches++
|
||||
if result.FirstFail == "" {
|
||||
result.FirstFail = fmt.Sprintf("iter %d: %s", i, report.String())
|
||||
}
|
||||
} else {
|
||||
result.Matches++
|
||||
}
|
||||
runtime.KeepAlive(bufs)
|
||||
}
|
||||
|
||||
if violations > 0 && result.FirstFail == "" {
|
||||
result.FirstFail = fmt.Sprintf("%d ABI violations across %d iterations", violations, iterations)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Close releases the executable mapping.
|
||||
func (k *Kernel) Close() {
|
||||
if k.exec != nil {
|
||||
|
||||
Reference in New Issue
Block a user