test(asm): pin the flag-list TEXT shapes against the toolchain
Assisted-by: GLM 5.3 Flash
This commit is contained in:
1 parent
9179d5cc7a
commit
6f3e054bab
4 files changed
+145
-10
No files matched your search
@@ -0,0 +1,127 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
goCode, ok := oracleFuncText(t, mustRead(t, objPath))["f"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("the oracle output carries no f")
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||||
|
goCode = maskCode(append([]byte(nil), goCode...), 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-7
@@ -77,7 +77,8 @@ func flagsRun(g []token.Token) []token.Token {
|
|||||||
// evalFlags evaluates one flags operand. Every identifier must name a
|
// evalFlags evaluates one flags operand. Every identifier must name a
|
||||||
// flag in textFlags; the toolchain rejects any other with "unexpected NAME
|
// flag in textFlags; the toolchain rejects any other with "unexpected NAME
|
||||||
// evaluating expression", and so does this. The whole operand folds to
|
// evaluating expression", and so does this. The whole operand folds to
|
||||||
// one integer, which is returned as the value.
|
// one integer, which is returned as the value. pseudo names the directive
|
||||||
|
// in the toolchain's own diagnostics ("TEXT", "GLOBL").
|
||||||
//
|
//
|
||||||
// The names returned follow the operand's own spelling: an operand written
|
// The names returned follow the operand's own spelling: an operand written
|
||||||
// purely as names keeps them in written order, duplicates included, the
|
// purely as names keeps them in written order, duplicates included, the
|
||||||
@@ -89,17 +90,21 @@ func flagsRun(g []token.Token) []token.Token {
|
|||||||
// After a failure the names collected so far are returned with value 0:
|
// After a failure the names collected so far are returned with value 0:
|
||||||
// the tree stays usable beside the diagnostic, and value 0 is exactly the
|
// the tree stays usable beside the diagnostic, and value 0 is exactly the
|
||||||
// flags the assembler can trust the operand for.
|
// flags the assembler can trust the operand for.
|
||||||
func (p *state) evalFlags(g []token.Token, keepWritten bool) ([]string, int64) {
|
func (p *state) evalFlags(g []token.Token, keepWritten bool, pseudo string) ([]string, int64) {
|
||||||
if len(g) == 0 {
|
if len(g) == 0 {
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
// An explicit '$' makes the operand an immediate ('$NOSPLIT' over the
|
// The flags operand is a bare constant expression; the immediate
|
||||||
// preprocessor's '$4'); the toolchain reads it as the same constant.
|
// spelling is the toolchain's "TEXT: expected integer constant".
|
||||||
if g[0].Kind == token.Dollar {
|
if g[0].Kind == token.Dollar {
|
||||||
g = g[1:]
|
if p.expand {
|
||||||
if len(g) == 0 {
|
found := "$"
|
||||||
return nil, 0
|
if len(g) > 1 {
|
||||||
|
found += g[1].Text
|
||||||
}
|
}
|
||||||
|
p.errorf(g[0].Pos, "%s: expected integer constant; found %s", pseudo, found)
|
||||||
|
}
|
||||||
|
return nil, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
sub := make([]token.Token, 0, len(g))
|
sub := make([]token.Token, 0, len(g))
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ func TestTextFlagsExpression(t *testing.T) {
|
|||||||
{"(NOSPLIT|NOFRAME)", []string{"NOSPLIT", "NOFRAME"}, 516},
|
{"(NOSPLIT|NOFRAME)", []string{"NOSPLIT", "NOFRAME"}, 516},
|
||||||
{"NOSPLIT|4", []string{"NOSPLIT"}, 4},
|
{"NOSPLIT|4", []string{"NOSPLIT"}, 4},
|
||||||
{"NOSPLIT&DUPOK", nil, 0},
|
{"NOSPLIT&DUPOK", nil, 0},
|
||||||
{"$NOSPLIT", []string{"NOSPLIT"}, 4},
|
// The immediate spelling is the toolchain's "expected integer
|
||||||
|
// constant" on the assembly path; the raw view reads nothing from
|
||||||
|
// it and stays quiet.
|
||||||
|
{"$NOSPLIT", nil, 0},
|
||||||
} {
|
} {
|
||||||
txt := parseTextHeader(t, "TEXT \u00b7f(SB), "+c.operand+", $0")
|
txt := parseTextHeader(t, "TEXT \u00b7f(SB), "+c.operand+", $0")
|
||||||
if !slices.Equal(txt.Flags, c.wantFlag) {
|
if !slices.Equal(txt.Flags, c.wantFlag) {
|
||||||
|
|||||||
+2
-2
@@ -261,7 +261,7 @@ func (p *state) parseText(line []token.Token) {
|
|||||||
var frame []token.Token
|
var frame []token.Token
|
||||||
switch {
|
switch {
|
||||||
case len(ops) >= 2:
|
case len(ops) >= 2:
|
||||||
text.Flags, text.FlagVal = p.evalFlags(flagsRun(ops[0]), false)
|
text.Flags, text.FlagVal = p.evalFlags(flagsRun(ops[0]), false, "TEXT")
|
||||||
frame = ops[1]
|
frame = ops[1]
|
||||||
if len(ops) > 2 {
|
if len(ops) > 2 {
|
||||||
p.errorf(line[0].Pos, "expect two or three operands for TEXT")
|
p.errorf(line[0].Pos, "expect two or three operands for TEXT")
|
||||||
@@ -355,7 +355,7 @@ func (p *state) parseGlobl(line []token.Token) *ast.Globl {
|
|||||||
ops := splitOperands(rest)
|
ops := splitOperands(rest)
|
||||||
switch {
|
switch {
|
||||||
case len(ops) >= 2:
|
case len(ops) >= 2:
|
||||||
g.Flags, g.FlagVal = p.evalFlags(flagsRun(ops[0]), true)
|
g.Flags, g.FlagVal = p.evalFlags(flagsRun(ops[0]), true, "GLOBL")
|
||||||
g.Size = parseOperand(ops[1], false)
|
g.Size = parseOperand(ops[1], false)
|
||||||
case len(ops) == 1:
|
case len(ops) == 1:
|
||||||
g.Size = parseOperand(ops[0], false)
|
g.Size = parseOperand(ops[0], false)
|
||||||
|
|||||||
Reference in new issue
Block a user