From d9f6167a4dce8d56b8ab4ec5af260072854f2dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sat, 25 Jul 2026 08:17:53 +0200 Subject: [PATCH] feat(verify): add basic-block enumeration and path-diversity profiling Assisted-by: Qwen 3.8 Max Preview --- cmd/gasm/main.go | 2 +- justfile | 2 +- verify/coverage.go | 103 ++++++++++++++++++++++++++++++++++++++++ verify/coverage_test.go | 85 +++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 verify/coverage.go create mode 100644 verify/coverage_test.go diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index d7f9fb9..d5d0277 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -29,7 +29,7 @@ import ( // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). -var version = "0.19.0" +var version = "0.20.0" func main() { if len(os.Args) < 2 { diff --git a/justfile b/justfile index f7b0e48..173d632 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.19.0" +version := "0.20.0" default: @just --list diff --git a/verify/coverage.go b/verify/coverage.go new file mode 100644 index 0000000..539f526 --- /dev/null +++ b/verify/coverage.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package verify + +import ( + "fmt" + "sort" +) + +// Block describes one basic block within a function: a maximal sequence of +// instructions with a single entry point (a label or the function start) and +// a single exit (a jump, conditional jump or RET). +type Block struct { + Offset int // byte offset within the function + Label string // label name ("" for the entry block) +} + +// Blocks identifies the basic blocks of a function from its local labels. +// Each label is a potential jump target and therefore a block boundary; the +// function entry (offset 0) is always a block. The blocks are returned in +// ascending offset order. +func (k *Kernel) Blocks(name string) ([]Block, error) { + idx, ok := k.funcs[name] + if !ok { + return nil, fmt.Errorf("verify: function %q not found", name) + } + fl := k.img.Funcs[idx] + + blocks := []Block{{Offset: 0, Label: "(entry)"}} + // Build a reverse map: offset → label name. + offToLabel := make(map[int]string, len(fl.Labels)) + for label, off := range fl.Labels { + if off > 0 && off < fl.Size { + offToLabel[off] = label + } + } + // Collect and sort offsets. + offsets := make([]int, 0, len(offToLabel)) + for off := range offToLabel { + offsets = append(offsets, off) + } + sort.Ints(offsets) + for _, off := range offsets { + blocks = append(blocks, Block{Offset: off, Label: offToLabel[off]}) + } + return blocks, nil +} + +// BlockCount returns the number of identified basic blocks for the function. +func (k *Kernel) BlockCount(name string) (int, error) { + blocks, err := k.Blocks(name) + if err != nil { + return 0, err + } + return len(blocks), nil +} + +// PathFingerprint is the observable output of one function execution: the +// values written back into the result slots of the argument block. Two +// executions that produce the same fingerprint took observationally +// equivalent paths (though they may differ internally). +type PathFingerprint struct { + Results []uint64 // the result words from the arg block +} + +// ProfilePaths runs the function with each of the given argument blocks and +// collects the distinct output fingerprints. This measures path diversity: +// how many observationally different execution paths the input corpus +// exercises. Combined with Blocks (the static block count), it gives a +// lower bound on code coverage. +func (k *Kernel) ProfilePaths(name string, argSets [][]byte, resultOffsets []int) ([]PathFingerprint, error) { + idx, ok := k.funcs[name] + if !ok { + return nil, fmt.Errorf("verify: function %q not found", name) + } + fl := k.img.Funcs[idx] + + seen := map[string]bool{} + var paths []PathFingerprint + + for _, args := range argSets { + if len(args) < fl.Args { + return nil, fmt.Errorf("verify: %s: arg block too small", name) + } + out, err := k.CallFunc(name, args) + if err != nil { + return nil, err + } + fp := PathFingerprint{} + key := "" + for _, off := range resultOffsets { + v := GetUint64(out, off) + fp.Results = append(fp.Results, v) + key += fmt.Sprintf("%016x", v) + } + if !seen[key] { + seen[key] = true + paths = append(paths, fp) + } + } + return paths, nil +} diff --git a/verify/coverage_test.go b/verify/coverage_test.go new file mode 100644 index 0000000..c276652 --- /dev/null +++ b/verify/coverage_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package verify + +import ( + "testing" + "unsafe" +) + +func TestBlocks(t *testing.T) { + k := loadBasic(t) + + // The "sum" function has labels: sum_done, sum_loop. + blocks, err := k.Blocks("sum") + if err != nil { + t.Fatalf("Blocks(sum): %v", err) + } + if len(blocks) < 3 { + t.Errorf("sum: expected at least 3 blocks (entry + 2 labels), got %d", len(blocks)) + } + if blocks[0].Offset != 0 { + t.Errorf("first block offset = %d, want 0", blocks[0].Offset) + } + 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)) +}