Files
gasm-sdk/asm/flaglist_integration_test.go
T
petrbalvin e5df0a9d84
Test / test (push) Successful in 31s
fix(asm): adapt the flag list integration test to the paired oracle
Assisted-by: GLM 5.3 Flash
2026-10-07 21:52:27 +02:00

131 lines
4.7 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
)
// flagListShapes are the TEXT shapes the flags operand decides: joined
// names, the legacy numeric spellings, the arithmetic and immediate forms,
// each with a frame size beside it. A flag the operand carries suppresses
// the stack-split guard; the toolchain reads one integer from the operand,
// and after the front end folds it the bytes must not tell the difference.
// The bodies are leaf-shaped on purpose: the frame engine's open divergences
// (the NOFRAME prologue for a non-leaf, the big-frame guard's shape) sit
// outside the flags operand and are somebody else's gap list.
var flagListShapes = []struct {
name string // the subtest's name
flags string // the flags operand as written
frame string // the frame operand as written
body string // the function body
}{
{"name", "NOSPLIT", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
{"numeric", "4", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
{"numericOR", "2|4", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
{"joinedFrame", "DUPOK|NOSPLIT", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
{"joinedFrameless", "NOSPLIT|TOPFRAME", "$0-0", "\tRET\n"},
{"parenthesised", "(NOSPLIT|NOFRAME)", "$0-0", "\tMOVD R0, R1\n\tRET\n"},
}
// TestFlagListAssemblyPathRejectsUnknown holds the front end's rejections
// that mirror the toolchain's: an identifier outside the flag table
// ("unexpected TYPO evaluating expression") and the immediate spelling of
// the operand ("TEXT: expected integer constant"), each refused on the
// assembly path before the encoder ever sees a tree.
func TestFlagListAssemblyPathRejectsUnknown(t *testing.T) {
for _, c := range []struct{ src, want string }{
{"#include \"textflag.h\"\n\nTEXT f(SB), NOSPLIT|TYPO, $0-0\n\tRET\n",
"unexpected TYPO evaluating expression"},
{"#include \"textflag.h\"\n\nTEXT f(SB), $NOSPLIT, $0-0\n\tRET\n",
"TEXT: expected integer constant; found $NOSPLIT"},
{"#include \"textflag.h\"\n\nGLOBL g<>(SB), $8, $8\n",
"GLOBL: expected integer constant; found $8"},
} {
_, errs := parser.ParseWithOptions("f.s", c.src, parser.Options{Expand: true})
if len(errs) == 0 || !strings.Contains(errs[0].Error(), c.want) {
t.Errorf("errors = %v, want %q", errs, c.want)
}
}
}
// TestFlagListByteParity assembles every flag-list shape through the fixed
// front end and holds the function's bytes equal to the installed
// toolchain's, no guard words where the flag suppresses them and none
// missing where it demands one.
func TestFlagListByteParity(t *testing.T) {
if testing.Short() {
t.Skip("live go tool asm oracle: skipped in -short mode")
}
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("no Go toolchain available")
}
out, err := exec.Command(goBin, "env", "GOROOT").Output()
if err != nil {
t.Fatalf("go env GOROOT: %v", err)
}
include := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
for _, c := range flagListShapes {
t.Run(c.name, func(t *testing.T) {
src := "#include \"textflag.h\"\n\nTEXT f(SB), " + c.flags + ", " + c.frame + "\n" + c.body
dir := t.TempDir()
file := filepath.Join(dir, "f.s")
if err := os.WriteFile(file, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
af, errs := parser.ParseWithOptions(file, src, parser.Options{Expand: true})
if len(errs) > 0 {
t.Fatalf("parse: %v", errs[0])
}
img, err := AssembleFileARM64(af)
if err != nil {
t.Fatalf("AssembleFileARM64: %v", err)
}
if len(img.Funcs) != 1 {
t.Fatalf("%d functions, want 1", len(img.Funcs))
}
objPath := filepath.Join(t.TempDir(), "oracle.o")
cmd := exec.Command(goBin, "tool", "asm", "-std", "-I", include,
"-p", "flaglisttest", "-o", objPath, file)
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64")
if oout, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("go tool asm: %v\n%s", err, oout)
}
blocks, ok := oracleFuncText(t, mustRead(t, objPath))["f"]
if !ok {
t.Fatal("the oracle output carries no f")
}
if len(blocks) != 1 {
t.Fatalf("%d text blocks for f, want 1", len(blocks))
}
goCode := blocks[0]
fn := img.Funcs[0]
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
cmpLen := min(len(goCode), len(gasmCode))
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
t.Errorf("bytes differ:\ngasm: % x\ngo: % x", gasmCode[:cmpLen], goCode[:cmpLen])
}
if len(goCode) > len(gasmCode) {
for _, b := range goCode[len(gasmCode):] {
if b != 0 {
t.Errorf("non-zero trailing bytes in the oracle output")
break
}
}
}
})
}
}