// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package lint import ( "testing" "sourcedock.dev/petrbalvin/gasm-sdk/arch" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) // TestUnnamedFPReference pins the toolchain contract "cannot reference FP // without a symbol": a numeric offset against FP parses as a memory operand // with FP in the base, and both assemblers reject it at assembly time. func TestUnnamedFPReference(t *testing.T) { diags := lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-8 MOVQ 0(FP), AX RET `) if codes(diags)[CodeUnnamedFPRef] != 1 { t.Fatalf("want one unnamed-fp-reference, got %+v", diags) } for _, d := range diags { if d.Code == CodeUnnamedFPRef && d.Severity != Error { t.Fatalf("unnamed FP reference must be error severity: %+v", d) } } // The named spelling is the language's requirement. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0-8 MOVQ x+0(FP), AX RET `) if codes(diags)[CodeUnnamedFPRef] != 0 { t.Fatalf("named FP reference must not be flagged: %+v", diags) } // -disable silences the rule. f, _ := parser.Parse("t_amd64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0-8\n\tMOVQ 0(FP), AX\n\tRET\n") diags = File(f, Config{Arch: arch.AMD64, Disable: map[string]bool{CodeUnnamedFPRef: true}}) if codes(diags)[CodeUnnamedFPRef] != 0 { t.Fatalf("disabled rule must stay silent: %+v", diags) } } // TestHardwareSPAddressing pins the one-character edge between the virtual // frame and the real stack pointer: a negative unnamed offset against (SP) // is the local-frame spelling with the name missing, while the runtime's // deliberate hardware-SP references all carry positive offsets. func TestHardwareSPAddressing(t *testing.T) { diags := lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $16 MOVQ $1, -8(SP) RET `) if codes(diags)[CodeHardwareSP] != 1 { t.Fatalf("want one hardware-sp-addressing, got %+v", diags) } // The named local in the virtual frame, the intended spelling. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $16 MOVQ $1, x-8(SP) RET `) if codes(diags)[CodeHardwareSP] != 0 { t.Fatalf("named SP local must not be flagged: %+v", diags) } // Positive hardware-SP offsets are the runtime's caller-frame idiom. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 MOVQ 8(SP), AX RET `) if codes(diags)[CodeHardwareSP] != 0 { t.Fatalf("positive hardware-SP offset must not be flagged: %+v", diags) } // The bare register is not a memory reference. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 MOVQ SP, AX RET `) if codes(diags)[CodeHardwareSP] != 0 { t.Fatalf("bare SP register must not be flagged: %+v", diags) } // The rule is architecture-neutral: riscv64 numeric (SP) parses the // same way (the parser documents 0(SP) as a RISC-V shape). diags = lintSrcArch(t, "f_riscv64.s", ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $16 MOV $1, -8(SP) RET `) if codes(diags)[CodeHardwareSP] != 1 { t.Fatalf("want one hardware-sp-addressing on riscv64, got %+v", diags) } // -disable silences the rule. f, _ := parser.Parse("t_amd64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $16\n\tMOVQ $1, -8(SP)\n\tRET\n") diags = File(f, Config{Arch: arch.AMD64, Disable: map[string]bool{CodeHardwareSP: true}}) if codes(diags)[CodeHardwareSP] != 0 { t.Fatalf("disabled rule must stay silent: %+v", diags) } }