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
+241
View File
@@ -0,0 +1,241 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package parser
import (
"os"
"path/filepath"
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
)
func mustParse(t *testing.T, path string) *ast.File {
t.Helper()
src, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
file, errs := Parse(path, string(src))
if len(errs) > 0 {
t.Fatalf("parse %s: %v", path, errs)
}
return file
}
func texts(f *ast.File) []*ast.Text {
var out []*ast.Text
for _, d := range f.Decls {
if t, ok := d.(*ast.Text); ok {
out = append(out, t)
}
}
return out
}
func TestParseSample(t *testing.T) {
f := mustParse(t, "../testdata/sample_amd64.s")
// Includes, GLOBL/DATA and two TEXT functions.
var includes, globls, datas int
for _, d := range f.Decls {
switch d.(type) {
case *ast.Include:
includes++
case *ast.Globl:
globls++
case *ast.Data:
datas++
}
}
if includes != 1 {
t.Errorf("includes = %d, want 1", includes)
}
if globls != 2 {
t.Errorf("globls = %d, want 2", globls)
}
if datas != 4 {
t.Errorf("datas = %d, want 4", datas)
}
txts := texts(f)
if len(txts) != 2 {
t.Fatalf("text functions = %d, want 2", len(txts))
}
fn := txts[0]
if fn.Name.Name != "analyzeO1RangeAVX2" {
t.Errorf("name = %q, want analyzeO1RangeAVX2", fn.Name.Name)
}
if fn.Name.Pseudo != "SB" {
t.Errorf("pseudo = %q, want SB", fn.Name.Pseudo)
}
if len(fn.Flags) != 1 || fn.Flags[0] != "NOSPLIT" {
t.Errorf("flags = %v, want [NOSPLIT]", fn.Flags)
}
if fn.Frame == nil || !fn.Frame.Imm.HasVal || fn.Frame.Imm.Val != 0 {
t.Errorf("frame = %+v, want $0", fn.Frame)
}
if fn.Args == nil || fn.Args.Imm.Val != 65 {
t.Errorf("args = %+v, want 65", fn.Args)
}
if fn.Doc == "" {
t.Error("expected a doc comment on the first TEXT")
}
// The body must contain the two labels vec1 and vec1done.
labels := map[string]bool{}
for _, s := range fn.Body {
if l, ok := s.(*ast.Label); ok {
labels[l.Name.Text] = true
}
}
for _, want := range []string{"vec1", "vec1done"} {
if !labels[want] {
t.Errorf("missing label %q", want)
}
}
}
func TestOperandStructure(t *testing.T) {
f := mustParse(t, "../testdata/sample_amd64.s")
fn := texts(f)[0]
// Index instructions by mnemonic for targeted checks.
byMnem := map[string]*ast.Instr{}
for _, s := range fn.Body {
if in, ok := s.(*ast.Instr); ok {
byMnem[in.Mnemonic.Text] = in
}
}
// MOVQ swin_base+0(FP), SI — the first MOVQ in the body.
var mov *ast.Instr
for _, s := range fn.Body {
if in, ok := s.(*ast.Instr); ok && in.Mnemonic.Text == "MOVQ" {
mov = in
break
}
}
if mov == nil {
t.Fatal("MOVQ not found")
}
src := mov.Operands[0]
if src.Kind != ast.OpAddr || src.Addr.Sym == nil {
t.Fatalf("src operand = %+v, want symbol address", src)
}
if src.Addr.Sym.Name != "swin_base" || src.Addr.Sym.Pseudo != "FP" || src.Addr.Sym.Offset != 0 {
t.Errorf("src symbol = %+v, want swin_base+0(FP)", src.Addr.Sym)
}
if mov.Operands[1].Addr.Sym.Name != "SI" {
t.Errorf("dst = %+v, want SI", mov.Operands[1].Addr)
}
// LEAQ (SI)(BX*4), R9
leaq := byMnem["LEAQ"]
if leaq == nil {
t.Fatal("LEAQ not found")
}
mem := leaq.Operands[0].Addr
if mem.Base != "SI" || mem.Index != "BX" || mem.Scale != 4 {
t.Errorf("LEAQ addr = %+v, want base SI index BX scale 4", mem)
}
// ANDQ $-8, R10
andq := byMnem["ANDQ"]
if andq == nil {
t.Fatal("ANDQ not found")
}
imm := andq.Operands[0]
if imm.Kind != ast.OpImmediate || !imm.Imm.Neg || imm.Imm.Val != 8 {
t.Errorf("ANDQ imm = %+v, want -8", imm.Imm)
}
}
func TestAVX512Operands(t *testing.T) {
f := mustParse(t, "../testdata/sample_amd64.s")
fn := texts(f)[1]
byMnem := map[string]*ast.Instr{}
for _, s := range fn.Body {
if in, ok := s.(*ast.Instr); ok {
byMnem[in.Mnemonic.Text] = in
}
}
// VALIGND $15, Z9, Z0, Z1 — four operands.
val := byMnem["VALIGND"]
if val == nil {
t.Fatal("VALIGND not found")
}
if len(val.Operands) != 4 {
t.Errorf("VALIGND operands = %d, want 4", len(val.Operands))
}
if val.Operands[0].Kind != ast.OpImmediate || val.Operands[0].Imm.Val != 15 {
t.Errorf("VALIGND first operand = %+v, want $15", val.Operands[0])
}
// VMOVDQU32 Z0, 4(SI)(AX*1)
vmov := byMnem["VMOVDQU32"]
if vmov == nil {
t.Fatal("VMOVDQU32 not found")
}
dst := vmov.Operands[len(vmov.Operands)-1].Addr
if dst.Offset != 4 || dst.Base != "SI" || dst.Index != "AX" || dst.Scale != 1 {
t.Errorf("VMOVDQU32 dst = %+v, want 4(SI)(AX*1)", dst)
}
// KTESTW K1, K1 — mask registers parse as bare names.
kt := byMnem["KTESTW"]
if kt == nil || len(kt.Operands) != 2 {
t.Fatalf("KTESTW = %+v, want two operands", kt)
}
}
func TestDataWidthAndStatic(t *testing.T) {
f := mustParse(t, "../testdata/sample_amd64.s")
var datas []*ast.Data
for _, d := range f.Decls {
if dd, ok := d.(*ast.Data); ok {
datas = append(datas, dd)
}
}
if datas[0].Width != 4 {
t.Errorf("first DATA width = %d, want 4", datas[0].Width)
}
if datas[0].Name.Pseudo != "SB" || datas[0].Name.Offset != 0 {
t.Errorf("first DATA name = %+v, want +0(SB)", datas[0].Name)
}
if datas[0].Value.Kind != ast.OpImmediate || datas[0].Value.Imm.Val != 1 {
t.Errorf("first DATA value = %+v, want $1", datas[0].Value)
}
// The mask24<> entries are static.
if !datas[2].Name.Static {
t.Errorf("mask24 DATA should be static, got %+v", datas[2].Name)
}
}
// TestParseRealGoLibraries parses every .s file in the sibling go-libraries
// repository when it is checked out, asserting a clean, error-free parse. It
// is skipped when the repository is not present.
func TestParseRealGoLibraries(t *testing.T) {
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
if len(matches) == 0 {
t.Skip("go-libraries repository not present next to gasm-devkit")
}
for _, path := range matches {
src, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
file, errs := Parse(path, string(src))
if len(errs) > 0 {
t.Errorf("parse %s: %v", path, errs)
continue
}
if len(texts(file)) == 0 {
t.Errorf("parse %s: no TEXT functions found", path)
}
t.Logf("%s: %d decls, %d functions", filepath.Base(path), len(file.Decls), len(texts(file)))
}
}