2026-09-20 14:25:47 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package parser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-09-26 11:08:43 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
2026-09-20 14:25:47 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// expand parses src with preprocessing enabled and returns the first TEXT's
|
|
|
|
|
// body instructions as "MNEMONIC operand|operand" strings, the shape the
|
|
|
|
|
// expansion assertions below compare against. Runs of spaces are
|
|
|
|
|
// collapsed: Raw renders a token group as its tokens joined with single
|
|
|
|
|
// spaces, so "$(32-7)" arrives as "$ ( 32 - 7 )" and the comparison must
|
|
|
|
|
// not depend on that spelling.
|
|
|
|
|
func expand(t *testing.T, src string) (*ast.File, []string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", src, Options{Expand: true})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
ts := texts(f)
|
|
|
|
|
if len(ts) == 0 {
|
|
|
|
|
t.Fatalf("no TEXT in:\n%s", src)
|
|
|
|
|
}
|
|
|
|
|
var got []string
|
|
|
|
|
for _, s := range ts[0].Body {
|
|
|
|
|
in, ok := s.(*ast.Instr)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var ops []string
|
|
|
|
|
for _, op := range in.Operands {
|
|
|
|
|
ops = append(ops, op.Raw)
|
|
|
|
|
}
|
|
|
|
|
line := in.Mnemonic.Text + " " + strings.Join(ops, ", ")
|
|
|
|
|
got = append(got, strings.ReplaceAll(line, " ", ""))
|
|
|
|
|
}
|
|
|
|
|
return f, got
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func wantLines(t *testing.T, got []string, want ...string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
strip := func(lines []string) string {
|
|
|
|
|
var out []string
|
|
|
|
|
for _, l := range lines {
|
|
|
|
|
out = append(out, strings.ReplaceAll(l, " ", ""))
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(out, "\n")
|
|
|
|
|
}
|
|
|
|
|
if strip(got) != strip(want) {
|
|
|
|
|
t.Errorf("expanded body:\n %s\nwant:\n %s", strings.Join(got, "\n "), strings.Join(want, "\n "))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestObjectMacroExpandsAtUse(t *testing.T) {
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define REGTMP CX
|
|
|
|
|
#define TWICE ADDQ CX, AX; ADDQ CX, AX
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ 8(SP), REGTMP
|
|
|
|
|
TWICE
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"MOVQ 8(SP), CX",
|
|
|
|
|
"ADDQ CX, AX",
|
|
|
|
|
"ADDQ CX, AX",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParameterisedMacroSubstitutesArguments(t *testing.T) {
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", `
|
|
|
|
|
#define ROUND1(a, index, const, shift) \
|
|
|
|
|
ADDQ $const, a; \
|
|
|
|
|
MOVW (index*4)(SP), a; \
|
|
|
|
|
RORQ $(32-shift), a
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
ROUND1(AX, 3, 0xd76aa478, 7)
|
|
|
|
|
RET
|
|
|
|
|
`, Options{Expand: true})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
body := texts(f)[0].Body
|
|
|
|
|
add := body[0].(*ast.Instr)
|
|
|
|
|
if add.Mnemonic.Text != "ADDQ" || !add.Operands[0].Imm.HasVal ||
|
|
|
|
|
add.Operands[0].Imm.Val != 0xd76aa478 || add.Operands[1].Addr.Sym == nil ||
|
|
|
|
|
add.Operands[1].Addr.Sym.Name != "AX" {
|
|
|
|
|
t.Errorf("ADDQ operands substituted wrong: %+v %+v", add.Operands[0].Imm, add.Operands[1].Addr)
|
|
|
|
|
}
|
|
|
|
|
mov := body[1].(*ast.Instr)
|
|
|
|
|
if addr := mov.Operands[0].Addr; !addr.HasOff || addr.Offset != 12 {
|
|
|
|
|
t.Errorf("MOVW offset = %+v, want 12 from 3*4", addr)
|
|
|
|
|
}
|
|
|
|
|
ror := body[2].(*ast.Instr)
|
|
|
|
|
if !ror.Operands[0].Imm.HasVal || ror.Operands[0].Imm.Val != 25 {
|
|
|
|
|
t.Errorf("RORQ immediate = %+v, want 25 from (32-7)", ror.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMacroArgumentsKeepCommasInParens(t *testing.T) {
|
|
|
|
|
// An argument may itself be an unparenthesised expression: the tokens
|
|
|
|
|
// substitute verbatim and the parser folds the result, as the
|
|
|
|
|
// toolchain's parser does.
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", `
|
|
|
|
|
#define LOAD(dst, off) MOVQ off(SP), dst
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
LOAD(AX, 1*8)
|
|
|
|
|
RET
|
|
|
|
|
`, Options{Expand: true})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
in := texts(f)[0].Body[0].(*ast.Instr)
|
|
|
|
|
addr := in.Operands[0].Addr
|
|
|
|
|
if !addr.HasOff || addr.Offset != 8 {
|
|
|
|
|
t.Errorf("offset = %+v, want 8", addr)
|
|
|
|
|
}
|
|
|
|
|
if sym := in.Operands[1].Addr.Sym; sym == nil || sym.Name != "AX" {
|
|
|
|
|
t.Errorf("destination = %+v, want AX", in.Operands[1].Addr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNestedMacroInvocations(t *testing.T) {
|
|
|
|
|
// An object macro naming a parameterised one, and a parameterised body
|
|
|
|
|
// invoking another parameterised macro: the toolchain's input stack
|
|
|
|
|
// rescans substituted tokens, and so does expansion here.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define DOUBLE(x) ADDQ x, x
|
|
|
|
|
#define TWICE2 DOUBLE
|
|
|
|
|
#define FOUR(a, b) DOUBLE(a); DOUBLE(b)
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
TWICE2(AX)
|
|
|
|
|
FOUR(AX, CX)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"ADDQ AX, AX",
|
|
|
|
|
"ADDQ AX, AX",
|
|
|
|
|
"ADDQ CX, CX",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMultiLineBodySplitsWithoutSemicolons(t *testing.T) {
|
|
|
|
|
// The arm64 style: backslash-continued lines with no semicolons. The
|
|
|
|
|
// continuation newline is a statement boundary, as in the toolchain.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define PAIR \
|
|
|
|
|
ADDQ AX, AX \
|
|
|
|
|
MOVQ AX, CX
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
PAIR
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"ADDQ AX, AX",
|
|
|
|
|
"MOVQ AX, CX",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestZeroArgumentMacro(t *testing.T) {
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define BARRIER()
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
BARRIER()
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParameterisedWithoutParensStandsAsName(t *testing.T) {
|
|
|
|
|
// A parameterised macro invoked without its parentheses names itself,
|
|
|
|
|
// which the parser then reports as an unknown instruction rather than
|
|
|
|
|
// silently expanding nothing.
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", `
|
|
|
|
|
#define M(x) ADDQ x, x
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
M
|
|
|
|
|
RET
|
|
|
|
|
`, Options{Expand: true})
|
|
|
|
|
if len(errs) != 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
fn := texts(f)[0]
|
|
|
|
|
if len(fn.Body) == 0 {
|
|
|
|
|
t.Fatal("body empty")
|
|
|
|
|
}
|
|
|
|
|
in, ok := fn.Body[0].(*ast.Instr)
|
|
|
|
|
if !ok || in.Mnemonic.Text != "M" {
|
|
|
|
|
t.Fatalf("bare parameterised macro did not stand as its name: %+v", fn.Body[0])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDefinitionScoping(t *testing.T) {
|
|
|
|
|
// A definition applies from its point onward: the use before the
|
|
|
|
|
// #define stays untouched.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
SPECIAL
|
|
|
|
|
#define SPECIAL ADDQ AX, AX
|
|
|
|
|
SPECIAL
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"SPECIAL",
|
|
|
|
|
"ADDQ AX, AX",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUndefRemovesMacro(t *testing.T) {
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define TEMP AX
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
TEMP
|
|
|
|
|
#undef TEMP
|
|
|
|
|
TEMP
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"AX",
|
|
|
|
|
"TEMP",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUndefUndefinedMacroIsAnError(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#undef NOSUCH\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "undefined macro NOSUCH") {
|
|
|
|
|
t.Fatalf("#undef of an undefined macro: got %v, want an error naming it", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRedefinitionIsAnError(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#define A X\n#define A Y\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "redefinition of macro A") {
|
|
|
|
|
t.Fatalf("redefinition: got %v, want an error", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRecursiveMacroIsAnError(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#define A B\n#define B A\nTEXT ·f(SB), NOSPLIT, $0\n\tA\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "recursive macro invocation") {
|
|
|
|
|
t.Fatalf("recursion: got %v, want a recursive-macro error, not a hang", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWrongArgumentCountIsAnError(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#define M(a, b) ADDQ a, b\nTEXT ·f(SB), NOSPLIT, $0\n\tM(AX)\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "wrong arg count for macro M") {
|
|
|
|
|
t.Fatalf("arg count: got %v, want an error", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConditionalsSelectOneBranch(t *testing.T) {
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define MODE2
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
#ifdef MODE2
|
|
|
|
|
ADDQ AX, AX
|
|
|
|
|
#else
|
|
|
|
|
SUBQ AX, AX
|
|
|
|
|
#endif
|
|
|
|
|
#ifndef MODE2
|
|
|
|
|
SUBQ CX, CX
|
|
|
|
|
#else
|
|
|
|
|
ADDQ CX, CX
|
|
|
|
|
#endif
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"ADDQ AX, AX",
|
|
|
|
|
"ADDQ CX, CX",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConditionalsHideDefinitionsAndIncludes(t *testing.T) {
|
|
|
|
|
// A definition inside a disabled branch must not exist, and an
|
|
|
|
|
// unresolvable include there must not be followed.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
#ifdef NOTDEFINED
|
|
|
|
|
#define HIDEN ADDQ AX, AX
|
|
|
|
|
#include "nowhere.h"
|
|
|
|
|
#endif
|
|
|
|
|
HIDEN
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "HIDEN", "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnclosedConditionalIsAnError(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#ifdef X\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "unclosed #ifdef") {
|
|
|
|
|
t.Fatalf("unclosed conditional: got %v, want an error", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnmatchedConditionalDelimitersAreErrors(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#endif\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "unmatched #endif") {
|
|
|
|
|
t.Fatalf("unmatched #endif: got %v, want an error", errs)
|
|
|
|
|
}
|
|
|
|
|
_, errs = ParseWithOptions("t_amd64.s", "#else\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "unmatched #else") {
|
|
|
|
|
t.Fatalf("unmatched #else: got %v, want an error", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// includeTree writes a directory of include files and returns its path.
|
|
|
|
|
func includeTree(t *testing.T, files map[string]string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
for name, content := range files {
|
|
|
|
|
path := filepath.Join(dir, name)
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return dir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIncludeSplicesAndDefinesAreShared(t *testing.T) {
|
|
|
|
|
dir := includeTree(t, map[string]string{
|
|
|
|
|
"consts.h": "#define KONST $42\n",
|
|
|
|
|
})
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", `
|
|
|
|
|
#include "consts.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ KONST, AX
|
|
|
|
|
RET
|
|
|
|
|
`, Options{Expand: true, IncludeDirs: []string{dir}})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
in := texts(f)[0].Body[0].(*ast.Instr)
|
|
|
|
|
if in.Mnemonic.Text != "MOVQ" || strings.ReplaceAll(in.Operands[0].Raw, " ", "") != "$42" {
|
|
|
|
|
t.Fatalf("include splicing failed: %+v", in)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIncludeResolutionOrder(t *testing.T) {
|
|
|
|
|
// The including file's directory wins over the -I list, and the -I list
|
|
|
|
|
// is searched in order.
|
|
|
|
|
src := includeTree(t, map[string]string{
|
|
|
|
|
"inc/main.s": "#include \"which.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
|
|
|
|
"inc/which.h": "#define WHO ONE\n",
|
|
|
|
|
"first/which.h": "#define WHO TWO\n",
|
|
|
|
|
"second/which.h": "#define WHO THREE\n",
|
|
|
|
|
})
|
|
|
|
|
main := filepath.Join(src, "inc", "main.s")
|
|
|
|
|
body, err := os.ReadFile(main)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
// The header exists in the including file's directory and in two -I
|
|
|
|
|
// directories; the source-directory copy must win.
|
|
|
|
|
f, errs := ParseWithOptions(main, string(body), Options{Expand: true, IncludeDirs: []string{
|
|
|
|
|
filepath.Join(src, "first"), filepath.Join(src, "second"),
|
|
|
|
|
}})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
found := false
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
if pp, ok := d.(*ast.Preproc); ok && strings.Contains(pp.Raw, "define WHO ONE") {
|
|
|
|
|
found = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
t.Error("the including file's directory did not win include resolution")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIncludeSearchesIncludeDirsInOrder(t *testing.T) {
|
|
|
|
|
src := includeTree(t, map[string]string{
|
|
|
|
|
"inc/main.s": "#include \"which.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
|
|
|
|
"first/which.h": "#define WHO TWO\n",
|
|
|
|
|
"second/which.h": "#define WHO THREE\n",
|
|
|
|
|
})
|
|
|
|
|
main := filepath.Join(src, "inc", "main.s")
|
|
|
|
|
body, err := os.ReadFile(main)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f, errs := ParseWithOptions(main, string(body), Options{Expand: true, IncludeDirs: []string{
|
|
|
|
|
filepath.Join(src, "first"), filepath.Join(src, "second"),
|
|
|
|
|
}})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
if pp, ok := d.(*ast.Preproc); ok && strings.Contains(pp.Raw, "define WHO THREE") {
|
|
|
|
|
t.Error("the second -I directory was searched before the first")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIncludeCycleIsDetected(t *testing.T) {
|
|
|
|
|
src := includeTree(t, map[string]string{
|
|
|
|
|
"a.s": "#include \"b.s\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
|
|
|
|
"b.s": "#include \"a.s\"\n",
|
|
|
|
|
})
|
|
|
|
|
_, errs := ParseWithOptions(filepath.Join(src, "a.s"), "#include \"b.s\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
|
|
|
|
Options{Expand: true})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "include cycle") {
|
|
|
|
|
t.Fatalf("include cycle: got %v, want a cycle diagnostic, not a hang", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnresolvableIncludeIsAnError(t *testing.T) {
|
|
|
|
|
_, errs := ParseWithOptions("t_amd64.s", "#include \"nothere.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
|
|
|
|
Options{Expand: true, IncludeDirs: []string{t.TempDir()}})
|
|
|
|
|
if len(errs) == 0 || !strings.Contains(errs[0].Error(), `#include "nothere.h"`) {
|
|
|
|
|
t.Fatalf("missing include: got %v, want a clear diagnostic", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTextflagHeaderIsNeverSpliced(t *testing.T) {
|
|
|
|
|
// textflag.h resolves nowhere here, yet the file must parse: the flag
|
|
|
|
|
// names are consumed natively and the include stays in the tree.
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
RET
|
|
|
|
|
`, Options{Expand: true})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
hasInclude := false
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
if _, ok := d.(*ast.Include); ok {
|
|
|
|
|
hasInclude = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !hasInclude {
|
|
|
|
|
t.Error("textflag.h include was dropped from the tree")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSemicolonSplitsRawLinesToo(t *testing.T) {
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
BYTE $0x0f; BYTE $0x1f
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "BYTE $0x0f", "BYTE $0x1f", "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParseUnchangedWithoutExpand(t *testing.T) {
|
|
|
|
|
// Without Expand the preprocessor must not exist: a macro invocation
|
2026-09-20 19:15:35 +02:00
|
|
|
// stays an unexpanded instruction line. The ';' statement separator is
|
|
|
|
|
// not part of the preprocessor: the plain parse path splits on it the
|
|
|
|
|
// same way the expansion path does, so both spellings agree.
|
2026-09-20 14:25:47 +02:00
|
|
|
f, errs := Parse("t_amd64.s", `
|
|
|
|
|
#define TWICE ADDQ AX, AX
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
TWICE
|
|
|
|
|
BYTE $0x0f; BYTE $0x1f
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
fn := texts(f)[0]
|
|
|
|
|
var mnemonics []string
|
|
|
|
|
for _, s := range fn.Body {
|
|
|
|
|
if in, ok := s.(*ast.Instr); ok {
|
|
|
|
|
mnemonics = append(mnemonics, in.Mnemonic.Text)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-20 19:15:35 +02:00
|
|
|
if strings.Join(mnemonics, " ") != "TWICE BYTE BYTE RET" {
|
2026-09-20 14:25:47 +02:00
|
|
|
t.Errorf("non-expanding parse changed: %v", mnemonics)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConstantExpressionFolding(t *testing.T) {
|
|
|
|
|
// The shapes substituted macro bodies leave behind: parenthesised
|
|
|
|
|
// arithmetic in immediates and displacements, tilde complements. The
|
|
|
|
|
// assertions read the semantic fields; Raw keeps the operand's tokens
|
|
|
|
|
// in the canonicalised rendering, not the folded values.
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", `
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
RORQ $(32-7), AX
|
|
|
|
|
ANDQ $~63, AX
|
|
|
|
|
MOVQ ((2*4)+0)(SP), AX
|
|
|
|
|
MOVQ $((1<<3)|(1<<1)), AX
|
|
|
|
|
RET
|
|
|
|
|
`, Options{Expand: true})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
body := texts(f)[0].Body
|
|
|
|
|
ror := body[0].(*ast.Instr)
|
|
|
|
|
if !ror.Operands[0].Imm.HasVal || ror.Operands[0].Imm.Val != 25 {
|
|
|
|
|
t.Errorf("RORQ immediate = %+v, want 25", ror.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
and := body[1].(*ast.Instr)
|
|
|
|
|
if !and.Operands[0].Imm.HasVal || and.Operands[0].Imm.Val != -64 {
|
|
|
|
|
t.Errorf("ANDQ immediate = %+v, want -64", and.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
mov := body[2].(*ast.Instr)
|
|
|
|
|
addr := mov.Operands[0].Addr
|
|
|
|
|
if !addr.HasOff || addr.Offset != 8 || addr.Base != "SP" {
|
|
|
|
|
t.Errorf("MOVQ address = %+v, want 8(SP)", addr)
|
|
|
|
|
}
|
|
|
|
|
mov2 := body[3].(*ast.Instr)
|
|
|
|
|
if !mov2.Operands[0].Imm.HasVal || mov2.Operands[0].Imm.Val != 10 {
|
|
|
|
|
t.Errorf("MOVQ immediate = %+v, want 10", mov2.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConstantExpressionFoldsWithoutExpand(t *testing.T) {
|
|
|
|
|
// Folding is a parser capability, not a preprocessing one: a
|
|
|
|
|
// hand-written $(32-7) folds the same way with expansion off.
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n\tRORQ $(32-7), AX\n\tRET\n", Options{})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
in := texts(f)[0].Body[0].(*ast.Instr)
|
|
|
|
|
if !in.Operands[0].Imm.HasVal || in.Operands[0].Imm.Val != 25 {
|
|
|
|
|
t.Errorf("Imm = %+v, want 25", in.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 22:02:19 +02:00
|
|
|
func TestParameterWithSelectorSubstitutes(t *testing.T) {
|
|
|
|
|
// The lexer folds A.S4 into one identifier token, so a parameter used
|
|
|
|
|
// with an element or lane selector never matched the whole-token
|
|
|
|
|
// substitution; the toolchain's lexer splits on the period and its
|
|
|
|
|
// substitution sees the name alone. Several parameters carry selectors
|
|
|
|
|
// in one body here, which is the chacha8_arm64.s QR shape in miniature.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define QR(A, B, C, D) VADD A.S4, B.S4, C.S4; VEOR D.B16, A.B16, D.B16
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
QR(V0, V1, V2, V3)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"VADD V0.S4, V1.S4, V2.S4",
|
|
|
|
|
"VEOR V3.B16, V0.B16, V3.B16",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSelectorWithCompoundArgumentPastesTextually(t *testing.T) {
|
|
|
|
|
// An argument that is itself one compound identifier pastes verbatim:
|
|
|
|
|
// A.S4 over V0.B16 spells V0.B16.S4, the operand the toolchain's
|
|
|
|
|
// split-then-substitute leaves behind.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define M(A) VADD A.S4, A.S4, A.S4
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
M(V0.B16)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "VADD V0.B16.S4, V0.B16.S4, V0.B16.S4", "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSelectorAlongsideBareParameter(t *testing.T) {
|
|
|
|
|
// A body may use the parameter bare and suffixed, and the argument may
|
|
|
|
|
// itself end in a selector; neither disturbs the other.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define M(A) VADD A, A.S4, A
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
M(V0)
|
|
|
|
|
M(V1.B16)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"VADD V0, V0.S4, V0",
|
|
|
|
|
"VADD V1.B16, V1.B16.S4, V1.B16",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSelectorKeepsNonParameterPrefixes(t *testing.T) {
|
|
|
|
|
// The prefix before the period must be the whole parameter name:
|
|
|
|
|
// AB.S4 never reaches a parameter A.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define M(A) VADD AB.S4, A.S4, AB.S4
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
M(V0)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "VADD AB.S4, V0.S4, AB.S4", "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSelectorExpandsMacroValuedArgument(t *testing.T) {
|
|
|
|
|
// gcm_arm64.s invokes mulRound(B1) where B1 is itself an object macro:
|
|
|
|
|
// the paste stays rescannable, so B1.D1 still expands to V1.D1 the way
|
|
|
|
|
// the toolchain's rescan of substituted tokens does.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define B1 V1
|
|
|
|
|
#define mulRound(X) VPMULL X.D1, T1.D1, T3.Q1
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
mulRound(B1)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "VPMULL V1.D1, T1.D1, T3.Q1", "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestObjectMacroBehindSelectorExpands(t *testing.T) {
|
|
|
|
|
// Ordinary code writes ACC0.B16 where ACC0 is an object macro; the
|
|
|
|
|
// toolchain expands the alias because its lexer reads the selector as
|
|
|
|
|
// its own token, and the lookup here must reach the macro through the
|
|
|
|
|
// compound spelling the same way.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define ACC0 V8
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
VEOR ACC0.B16, ACC0.B16, ACC0.B16
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got, "VEOR V8.B16, V8.B16, V8.B16", "RET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestChacha8QRMacroExpands(t *testing.T) {
|
|
|
|
|
// The real QR round of chacha8_arm64.s end to end: every parameter
|
|
|
|
|
// carries a selector somewhere, and the round is sixteen instructions.
|
|
|
|
|
_, got := expand(t, `
|
|
|
|
|
#define QR(A, B, C, D) \
|
|
|
|
|
VADD A.S4, B.S4, A.S4; VEOR D.B16, A.B16, D.B16; VREV32 D.H8, D.H8; \
|
|
|
|
|
VADD C.S4, D.S4, C.S4; VEOR B.B16, C.B16, V30.B16; VSHL $12, V30.S4, B.S4; VSRI $20, V30.S4, B.S4; \
|
|
|
|
|
VADD A.S4, B.S4, A.S4; VEOR D.B16, A.B16, D.B16; VTBL V31.B16, [D.B16], D.B16; \
|
|
|
|
|
VADD C.S4, D.S4, C.S4; VEOR B.B16, C.B16, V30.B16; VSHL $7, V30.S4, B.S4; VSRI $25, V30.S4, B.S4
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
QR(V0, V1, V2, V3)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
wantLines(t, got,
|
|
|
|
|
"VADD V0.S4, V1.S4, V0.S4",
|
|
|
|
|
"VEOR V3.B16, V0.B16, V3.B16",
|
|
|
|
|
"VREV32 V3.H8, V3.H8",
|
|
|
|
|
"VADD V2.S4, V3.S4, V2.S4",
|
|
|
|
|
"VEOR V1.B16, V2.B16, V30.B16",
|
|
|
|
|
"VSHL $12, V30.S4, V1.S4",
|
|
|
|
|
"VSRI $20, V30.S4, V1.S4",
|
|
|
|
|
"VADD V0.S4, V1.S4, V0.S4",
|
|
|
|
|
"VEOR V3.B16, V0.B16, V3.B16",
|
|
|
|
|
"VTBL V31.B16, [V3.B16], V3.B16",
|
|
|
|
|
"VADD V2.S4, V3.S4, V2.S4",
|
|
|
|
|
"VEOR V1.B16, V2.B16, V30.B16",
|
|
|
|
|
"VSHL $7, V30.S4, V1.S4",
|
|
|
|
|
"VSRI $25, V30.S4, V1.S4",
|
|
|
|
|
"RET",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 14:25:47 +02:00
|
|
|
func TestNotAnExpressionFallsBack(t *testing.T) {
|
|
|
|
|
// Symbol immediates and floats must keep their ordinary parse.
|
|
|
|
|
f, errs := ParseWithOptions("t_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ $1.5, AX\n\tMOVQ $·sym(SB), AX\n\tRET\n", Options{})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
fn := texts(f)[0]
|
|
|
|
|
mov1 := fn.Body[0].(*ast.Instr)
|
|
|
|
|
if mov1.Operands[0].Imm.HasVal || mov1.Operands[0].Imm.Float != "1.5" {
|
|
|
|
|
t.Errorf("float immediate parsed as %+v", mov1.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
mov2 := fn.Body[1].(*ast.Instr)
|
|
|
|
|
if mov2.Operands[0].Imm.Sym == nil {
|
|
|
|
|
t.Errorf("symbol immediate parsed as %+v", mov2.Operands[0].Imm)
|
|
|
|
|
}
|
|
|
|
|
}
|