// 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" ) func TestNoFrameFrameSize(t *testing.T) { // NOFRAME against a positive frame: the flag states an arrangement the // directive does not describe. diags := lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT|NOFRAME, $8 MOVQ $1, AX RET `) if codes(diags)[CodeNoFrameFrameSize] != 1 { t.Fatalf("want one noframe-frame-size, got %+v", diags) } // The valid spelling: NOFRAME with a zero frame. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT|NOFRAME, $0 MOVQ $1, AX RET `) if codes(diags)[CodeNoFrameFrameSize] != 0 { t.Fatalf("NOFRAME with $0 must not be flagged: %+v", diags) } // A negative frame with NOFRAME is the arm64 BSD syscall-stub pattern // (GOROOT's sys_netbsd_arm64.s runtime·open), not a defect. diags = lintSrcArch(t, "f_arm64.s", ` #include "textflag.h" TEXT ·open(SB),NOSPLIT|NOFRAME,$-8 MOVD name+0(FP), R0 RET `) if codes(diags)[CodeNoFrameFrameSize] != 0 { t.Fatalf("NOFRAME with a negative frame must not be flagged: %+v", diags) } // A frame without the flag is the ordinary spelling. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $8 MOVQ $1, AX RET `) if codes(diags)[CodeNoFrameFrameSize] != 0 { t.Fatalf("a frame without NOFRAME must not be flagged: %+v", diags) } // -disable silences the rule. f, _ := parser.Parse("t_amd64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT|NOFRAME, $8\n\tRET\n") diags = File(f, Config{Arch: arch.AMD64, Disable: map[string]bool{CodeNoFrameFrameSize: true}}) if codes(diags)[CodeNoFrameFrameSize] != 0 { t.Fatalf("disabled rule must stay silent: %+v", diags) } } func TestMissingArgSize(t *testing.T) { // Without NOSPLIT and without an argument area, the object records // ArgsSizeUnknown even though the signature states the size. diags := lintSrc(t, ` // func f(a int) int TEXT ·f(SB), $0 MOVQ a+0(FP), AX MOVQ AX, ret+8(FP) RET `) if codes(diags)[CodeMissingArgSize] != 1 { t.Fatalf("want one missing-argsize, got %+v", diags) } // The declared area matching the signature is clean. diags = lintSrc(t, ` // func f(a int) int TEXT ·f(SB), $0-16 MOVQ a+0(FP), AX MOVQ AX, ret+8(FP) RET `) if codes(diags)[CodeMissingArgSize] != 0 { t.Fatalf("a declared argument area must not be flagged: %+v", diags) } // NOSPLIT functions omit the area throughout GOROOT; not flagged. diags = lintSrc(t, ` // func f(a int) int TEXT ·f(SB), NOSPLIT, $0 MOVQ a+0(FP), AX MOVQ AX, ret+8(FP) RET `) if codes(diags)[CodeMissingArgSize] != 0 { t.Fatalf("NOSPLIT without an area must not be flagged: %+v", diags) } // No signature, no opinion. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), $0 MOVQ $1, AX RET `) if codes(diags)[CodeMissingArgSize] != 0 { t.Fatalf("signature-less function must not be flagged: %+v", diags) } // A signature whose argument area is empty implies nothing to declare. diags = lintSrc(t, ` // func f() TEXT ·f(SB), $0 RET `) if codes(diags)[CodeMissingArgSize] != 0 { t.Fatalf("zero-size signature must not be flagged: %+v", diags) } } // TestMissingRetFallthroughEnd pins the second shape of missing-ret: the // body knows how to return, but its tail can run off the end, and the // toolchain appends nothing, so execution continues into the next TEXT. func TestMissingRetFallthroughEnd(t *testing.T) { diags := lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 CMPQ AX, $0 JNE done RET done: MOVQ $2, BX `) if codes(diags)[CodeMissingRet] != 1 { t.Fatalf("want one missing-ret for the fall-through tail, got %+v", diags) } // Ending in RET is clean, trailing labels included. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 CMPQ AX, $0 JNE done MOVQ $1, AX done: RET `) if codes(diags)[CodeMissingRet] != 0 { t.Fatalf("RET last must not be flagged: %+v", diags) } // Ending in an unconditional branch is a tail call, not a defect. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 MOVQ $1, AX JMP ·g(SB) `) if codes(diags)[CodeMissingRet] != 0 { t.Fatalf("a tail call must not be flagged: %+v", diags) } // A conditional branch last, with a RET earlier, still falls through. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 JE done RET done: CMPQ AX, $0 JNE done `) if codes(diags)[CodeMissingRet] != 1 { t.Fatalf("conditional-branch tail must be flagged: %+v", diags) } // GC annotations after the terminator carry no control flow. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT, $0 RET FUNCDATA $1, ·sm(SB) `) if codes(diags)[CodeMissingRet] != 0 { t.Fatalf("FUNCDATA after RET must not be flagged: %+v", diags) } // Trapping tails: the runtime's abort paths end in INT (amd64) or BRK // (arm64); they never return, so no RET is needed. diags = lintSrc(t, ` #include "textflag.h" TEXT ·abort(SB), NOSPLIT, $0 INT $3 `) if codes(diags)[CodeMissingRet] != 0 { t.Fatalf("INT tail must not be flagged: %+v", diags) } diags = lintSrcArch(t, "f_arm64.s", ` #include "textflag.h" TEXT ·abort(SB), NOSPLIT, $0 BRK `) if codes(diags)[CodeMissingRet] != 0 { t.Fatalf("BRK tail must not be flagged: %+v", diags) } // A file that includes non-textflag headers may hide the terminator in // a macro, so the heuristic stays silent there. diags = lintSrcArch(t, "f_amd64.s", ` #include "go_tls.h" TEXT ·f(SB), NOSPLIT, $0 MOVQ $1, AX `) if codes(diags)[CodeMissingRet] != 0 { t.Fatalf("macro-using files must not be flagged: %+v", diags) } } func TestInvalidFlagPlacement(t *testing.T) { // NOSPLIT is a TEXT flag; on GLOBL it does nothing. diags := lintSrc(t, ` #include "textflag.h" GLOBL ·tab(SB), NOSPLIT, $8 `) if codes(diags)[CodeInvalidFlag] != 1 { t.Fatalf("want one invalid-textflag for the misplaced NOSPLIT, got %+v", diags) } // RODATA belongs to data declarations, not to TEXT. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), RODATA, $0 RET `) if codes(diags)[CodeInvalidFlag] != 1 { t.Fatalf("want one invalid-textflag for RODATA on TEXT, got %+v", diags) } // Correct placements stay silent, on both directives. diags = lintSrc(t, ` #include "textflag.h" TEXT ·f(SB), NOSPLIT|DUPOK, $0 RET GLOBL ·tab(SB), RODATA|NOPTR, $8 DATA ·tab+0(SB)/8, $0 `) if codes(diags)[CodeInvalidFlag] != 0 { t.Fatalf("correct flag placements must not be flagged: %+v", diags) } } func TestUnnamedResult(t *testing.T) { // The signature names its results, the body uses the generic spelling. diags := lintSrc(t, ` // func kevent(kq int, ch unsafe.Pointer) (n int, err error) TEXT ·kevent(SB), NOSPLIT, $0-40 MOVL kq+0(FP), AX MOVQ ch+8(FP), BX MOVL AX, ret+16(FP) MOVL $0, err+24(FP) RET `) if codes(diags)[CodeUnnamedResult] != 1 { t.Fatalf("want one unnamed-result hint, got %+v", diags) } // Referencing the result by its declared name is the clean spelling. diags = lintSrc(t, ` // func kevent(kq int, ch unsafe.Pointer) (n int, err error) TEXT ·kevent(SB), NOSPLIT, $0-40 MOVL kq+0(FP), AX MOVQ ch+8(FP), BX MOVL AX, n+16(FP) MOVL $0, err+24(FP) RET `) if codes(diags)[CodeUnnamedResult] != 0 { t.Fatalf("named result reference must not be flagged: %+v", diags) } // An unnamed result is exactly what the ret spelling documents. diags = lintSrc(t, ` // func f(a int) int TEXT ·f(SB), NOSPLIT, $0-16 MOVQ a+0(FP), AX MOVQ AX, ret+8(FP) RET `) if codes(diags)[CodeUnnamedResult] != 0 { t.Fatalf("unnamed result via ret must not be flagged: %+v", diags) } // A result the signature itself names ret coincides with the generic // spelling; there is nothing better to suggest. diags = lintSrc(t, ` // func f(a int) (ret int) TEXT ·f(SB), NOSPLIT, $0-16 MOVQ a+0(FP), AX MOVQ AX, ret+8(FP) RET `) if codes(diags)[CodeUnnamedResult] != 0 { t.Fatalf("a result literally named ret must not be flagged: %+v", diags) } // A parameter named ret turns the spelling into a parameter reference. diags = lintSrc(t, ` // func f(ret int) (n int) TEXT ·f(SB), NOSPLIT, $0-16 MOVQ ret+0(FP), AX MOVQ AX, n+8(FP) RET `) if codes(diags)[CodeUnnamedResult] != 0 { t.Fatalf("a parameter named ret must not be flagged: %+v", diags) } }