feat: gasm-devkit 0.1.0 — GAsm lexer, parser, linter, formatter, LSP and amd64 assembler

Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
2026-07-06 09:49:50 +02:00
commit d5a4a6de45
53 changed files with 13166 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
)
const clean = "#include \"textflag.h\"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"\tMOVQ AX, BX\n" +
"loop:\n" +
"\tJMP loop\n" +
"\tRET\n"
const buggy = "#include \"textflag.h\"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"\tBOGUS AX, BX\n" +
"\tJMP nowhere\n" +
"\tRET\n"
func writeTemp(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
// capture runs fn with stdout and stderr redirected and returns both plus the
// exit code fn produced.
func capture(fn func() int) (stdout, stderr string, code int) {
oldOut, oldErr := os.Stdout, os.Stderr
rOut, wOut, _ := os.Pipe()
rErr, wErr, _ := os.Pipe()
os.Stdout, os.Stderr = wOut, wErr
code = fn()
wOut.Close()
wErr.Close()
os.Stdout, os.Stderr = oldOut, oldErr
ob, _ := io.ReadAll(rOut)
eb, _ := io.ReadAll(rErr)
return string(ob), string(eb), code
}
func TestCmdTokens(t *testing.T) {
path := writeTemp(t, "f_amd64.s", clean)
out, _, code := capture(func() int { return cmdTokens([]string{path}) })
if code != 0 {
t.Fatalf("code = %d", code)
}
if !strings.Contains(out, "IDENT") || !strings.Contains(out, "TEXT") {
t.Errorf("token dump missing expected tokens:\n%s", out)
}
}
func TestCmdParseOK(t *testing.T) {
path := writeTemp(t, "f_amd64.s", clean)
out, _, code := capture(func() int { return cmdParse([]string{path}) })
if code != 0 {
t.Fatalf("code = %d", code)
}
if !strings.Contains(out, "OK") || !strings.Contains(out, "1 functions") {
t.Errorf("parse output = %q", out)
}
}
func TestCmdParseError(t *testing.T) {
path := writeTemp(t, "bad_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n) (\n\tRET\n")
_, errOut, code := capture(func() int { return cmdParse([]string{path}) })
if code != 1 {
t.Fatalf("code = %d, want 1", code)
}
if errOut == "" {
t.Error("expected a parse error on stderr")
}
}
func TestCmdParseMissingFile(t *testing.T) {
_, _, code := capture(func() int { return cmdParse([]string{"/nonexistent/file.s"}) })
if code != 1 {
t.Fatalf("code = %d, want 1", code)
}
}
func TestCmdLintClean(t *testing.T) {
path := writeTemp(t, "f_amd64.s", clean)
_, _, code := capture(func() int { return cmdLint([]string{path}) })
if code != 0 {
t.Fatalf("clean file should lint with code 0, got %d", code)
}
}
func TestCmdLintErrors(t *testing.T) {
path := writeTemp(t, "f_amd64.s", buggy)
out, _, code := capture(func() int { return cmdLint([]string{path}) })
if code != 1 {
t.Fatalf("code = %d, want 1", code)
}
if !strings.Contains(out, "unknown-instruction") || !strings.Contains(out, "undefined-label") {
t.Errorf("lint output missing expected codes:\n%s", out)
}
}
func TestCmdLintDisable(t *testing.T) {
path := writeTemp(t, "f_amd64.s", buggy)
args := []string{"-disable", "unknown-instruction,undefined-label", path}
_, _, code := capture(func() int { return cmdLint(args) })
if code != 0 {
t.Fatalf("disabling both rules should yield code 0, got %d", code)
}
}
func TestCmdFmtStdout(t *testing.T) {
path := writeTemp(t, "f_amd64.s", "TEXT ·f(SB),NOSPLIT,$0\nMOVQ AX,BX\nRET\n")
out, _, code := capture(func() int { return cmdFmt([]string{path}) })
if code != 0 {
t.Fatalf("code = %d", code)
}
if !strings.Contains(out, "TEXT ·f(SB), NOSPLIT, $0") || !strings.Contains(out, "\tMOVQ AX, BX") {
t.Errorf("formatted output unexpected:\n%s", out)
}
}
func TestCmdFmtWrite(t *testing.T) {
path := writeTemp(t, "f_amd64.s", "TEXT ·f(SB),NOSPLIT,$0\nMOVQ AX,BX\nRET\n")
_, _, code := capture(func() int { return cmdFmt([]string{"-w", path}) })
if code != 0 {
t.Fatalf("code = %d", code)
}
b, _ := os.ReadFile(path)
if !strings.Contains(string(b), "\tMOVQ AX, BX") {
t.Errorf("file not rewritten:\n%s", b)
}
// Idempotent: a second -w pass leaves the file unchanged.
_, _, _ = capture(func() int { return cmdFmt([]string{"-w", path}) })
b2, _ := os.ReadFile(path)
if string(b) != string(b2) {
t.Error("fmt -w is not idempotent")
}
}
func TestUsage(t *testing.T) {
var b bytes.Buffer
usage(&b)
if !strings.Contains(b.String(), "gasm") {
t.Errorf("usage text unexpected:\n%s", b.String())
}
}
func TestCmdArgErrors(t *testing.T) {
// Missing file arguments produce a usage error (code 2).
if _, _, code := capture(func() int { return cmdFmt(nil) }); code != 2 {
t.Errorf("cmdFmt() code = %d, want 2", code)
}
if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 {
t.Errorf("cmdLint() code = %d, want 2", code)
}
if _, _, code := capture(func() int { return cmdTokens(nil) }); code != 2 {
t.Errorf("cmdTokens() code = %d, want 2", code)
}
if _, _, code := capture(func() int { return cmdParse(nil) }); code != 2 {
t.Errorf("cmdParse() code = %d, want 2", code)
}
}