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
|
||||
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs AVX2 and AVX-512 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.
|
||||
|
||||
## [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 {
|
||||
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
|
||||
@@ -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
|
||||
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)")
|
||||
@@ -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")
|
||||
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)
|
||||
@@ -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))
|
||||
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)
|
||||
@@ -1095,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 ""
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
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
|
||||
|
||||
+31
-7
@@ -44,25 +44,49 @@ func ParseFuncSig(comment string) (FuncSig, bool) {
|
||||
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 {
|
||||
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 _, 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".
|
||||
out = append(out, Param{Typ: parts[0]})
|
||||
typ := types[i]
|
||||
if typ == "" {
|
||||
typ = parts[0] // unnamed: the whole field is the type
|
||||
out = append(out, Param{Typ: typ})
|
||||
} else {
|
||||
// Named: "a []byte".
|
||||
out = append(out, Param{Name: parts[0], Typ: parts[1]})
|
||||
out = append(out, Param{Name: parts[0], Typ: typ})
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user