// Copyright (c) 2026 Petr BalvĂ­n (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 } } } }) } }