feat(verify): add --call and --buf flags for single-function invocation
Assisted-by: GLM 5.2
This commit is contained in:
@@ -14,6 +14,20 @@ Unreleased changes on the `development` branch.
|
|||||||
- **`gasm diff --map`** — compare functions whose names differ between files
|
- **`gasm diff --map`** — compare functions whose names differ between files
|
||||||
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs AVX2 and AVX-512 variants
|
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs AVX2 and AVX-512 variants
|
||||||
regardless of suffix). Unmapped functions fall back to the original name match.
|
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.
|
||||||
|
|
||||||
## [0.29.0] — 2026-08-05
|
## [0.29.0] — 2026-08-05
|
||||||
|
|
||||||
|
|||||||
+140
-2
@@ -823,7 +823,7 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdVerify(args []string) 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
|
Assemble FILE (amd64), map it into executable memory and report the available
|
||||||
functions. This confirms the assembled image is self-consistent (no
|
functions. This confirms the assembled image is self-consistent (no
|
||||||
unresolved external symbols) and executable — the prerequisite for dynamic
|
unresolved external symbols) and executable — the prerequisite for dynamic
|
||||||
@@ -836,7 +836,18 @@ that tolerate nil pointers and zero lengths in their arguments.
|
|||||||
With -abi, each function is called with sentinel values in the callee-saved
|
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.
|
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 -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")
|
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)")
|
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
||||||
@@ -846,9 +857,12 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs")
|
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")
|
fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function")
|
||||||
fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode)
|
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)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
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
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
@@ -874,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))
|
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
||||||
rc := 0
|
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.
|
// Subprocess mode: fuzz a single function and exit.
|
||||||
if *fuzzOne != "" {
|
if *fuzzOne != "" {
|
||||||
gt, err := verify.GroundTruth(path)
|
gt, err := verify.GroundTruth(path)
|
||||||
@@ -1095,3 +1114,122 @@ func fuzzInSubprocess(path, funcName string, n int) string {
|
|||||||
}
|
}
|
||||||
return strings.TrimSpace(string(out))
|
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 ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-8
@@ -79,22 +79,39 @@ func parseParams(s string) []param {
|
|||||||
if s == "" {
|
if s == "" {
|
||||||
return nil
|
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
|
var out []param
|
||||||
for _, field := range strings.Split(s, ",") {
|
for i, field := range fields {
|
||||||
field = strings.TrimSpace(field)
|
field = strings.TrimSpace(field)
|
||||||
if field == "" {
|
if field == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts := strings.Fields(field)
|
parts := strings.Fields(field)
|
||||||
if len(parts) == 1 {
|
typ := types[i]
|
||||||
// Unnamed: "int" or "[]byte".
|
if typ == "" {
|
||||||
out = append(out, param{typ: parts[0]})
|
out = append(out, param{typ: parts[0]})
|
||||||
} else {
|
} else {
|
||||||
// Named: "a []byte" or shared "a, b []int32" (handled by the
|
out = append(out, param{name: parts[0], typ: typ})
|
||||||
// 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]})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
+31
-7
@@ -44,25 +44,49 @@ func ParseFuncSig(comment string) (FuncSig, bool) {
|
|||||||
return sig, true
|
return sig, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseParamsExported splits "a []byte, b []int32" into typed parameters.
|
// 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 {
|
func parseParamsExported(s string) []Param {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return nil
|
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
|
var out []Param
|
||||||
for _, field := range strings.Split(s, ",") {
|
for i, field := range fields {
|
||||||
field = strings.TrimSpace(field)
|
field = strings.TrimSpace(field)
|
||||||
if field == "" {
|
if field == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts := strings.Fields(field)
|
parts := strings.Fields(field)
|
||||||
if len(parts) == 1 {
|
typ := types[i]
|
||||||
// Unnamed: "int" or "[]byte".
|
if typ == "" {
|
||||||
out = append(out, Param{Typ: parts[0]})
|
typ = parts[0] // unnamed: the whole field is the type
|
||||||
|
out = append(out, Param{Typ: typ})
|
||||||
} else {
|
} else {
|
||||||
// Named: "a []byte".
|
out = append(out, Param{Name: parts[0], Typ: typ})
|
||||||
out = append(out, Param{Name: parts[0], Typ: parts[1]})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
Reference in New Issue
Block a user