feat(cmd): add gofmt-style -l and -d modes to gasm fmt
This commit is contained in:
+24
-1
@@ -56,6 +56,8 @@ func main() {
|
|||||||
os.Exit(cmdLint(os.Args[2:]))
|
os.Exit(cmdLint(os.Args[2:]))
|
||||||
case "asm":
|
case "asm":
|
||||||
os.Exit(cmdAsm(os.Args[2:]))
|
os.Exit(cmdAsm(os.Args[2:]))
|
||||||
|
case "dis":
|
||||||
|
os.Exit(cmdDis(os.Args[2:]))
|
||||||
case "verify":
|
case "verify":
|
||||||
os.Exit(cmdVerify(os.Args[2:]))
|
os.Exit(cmdVerify(os.Args[2:]))
|
||||||
case "debug":
|
case "debug":
|
||||||
@@ -132,6 +134,7 @@ func usage(w io.Writer) {
|
|||||||
{"fmt", "canonicalise formatting (gofmt for assembly)"},
|
{"fmt", "canonicalise formatting (gofmt for assembly)"},
|
||||||
{"lint", "run static checks"},
|
{"lint", "run static checks"},
|
||||||
{"asm", "assemble .s files to machine code (amd64, arm64, riscv64, loong64)"},
|
{"asm", "assemble .s files to machine code (amd64, arm64, riscv64, loong64)"},
|
||||||
|
{"dis", "disassemble machine code (raw bytes or an assembled .s file)"},
|
||||||
{"verify", "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)"},
|
{"verify", "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)"},
|
||||||
{"debug", "interactive source-level debugger (amd64, arm64, riscv64, loong64)"},
|
{"debug", "interactive source-level debugger (amd64, arm64, riscv64, loong64)"},
|
||||||
{"diff", "compare machine code of two .s files"},
|
{"diff", "compare machine code of two .s files"},
|
||||||
@@ -268,7 +271,7 @@ standard input.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdFmt(args []string) int {
|
func cmdFmt(args []string) int {
|
||||||
fs := newCommand("fmt", "gasm fmt [-w] [path...]", `
|
fs := newCommand("fmt", "gasm fmt [-w|-l|-d] [path...]", `
|
||||||
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
|
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
|
||||||
spacing, per-function mnemonic alignment and blank-line layout (exactly one
|
spacing, per-function mnemonic alignment and blank-line layout (exactly one
|
||||||
blank line before each label, TEXT and GLOBL block). Formatting is
|
blank line before each label, TEXT and GLOBL block). Formatting is
|
||||||
@@ -278,9 +281,19 @@ With no paths, or a directory path, every .s file below it is reformatted
|
|||||||
in place and the changed files are listed, the way go fmt does; "." and "_"
|
in place and the changed files are listed, the way go fmt does; "." and "_"
|
||||||
directories are skipped. Explicit file paths print to stdout unless -w is
|
directories are skipped. Explicit file paths print to stdout unless -w is
|
||||||
given.
|
given.
|
||||||
|
|
||||||
|
-l and -d rewrite nothing: -l prints the paths whose formatting differs
|
||||||
|
from gasm's (empty output means everything is formatted, which is what a CI
|
||||||
|
check wants), -d prints the diffs. They are mutually exclusive.
|
||||||
`)
|
`)
|
||||||
write := fs.Bool("w", false, "write result to the source file")
|
write := fs.Bool("w", false, "write result to the source file")
|
||||||
|
list := fs.Bool("l", false, "list files whose formatting differs from gasm's")
|
||||||
|
diffMode := fs.Bool("d", false, "print diffs instead of rewriting files")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
|
if *list && *diffMode {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm fmt: -l and -d are mutually exclusive")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
// Like go fmt: with no arguments, or with a directory argument, every .s
|
// Like go fmt: with no arguments, or with a directory argument, every .s
|
||||||
// file below the directory is formatted in place and the names of the
|
// file below the directory is formatted in place and the names of the
|
||||||
// changed files are listed; explicit file arguments keep the -w / stdout
|
// changed files are listed; explicit file arguments keep the -w / stdout
|
||||||
@@ -318,6 +331,16 @@ given.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out := format.Source(src)
|
out := format.Source(src)
|
||||||
|
if *list || *diffMode {
|
||||||
|
if out != src {
|
||||||
|
if *list {
|
||||||
|
fmt.Println(path)
|
||||||
|
} else {
|
||||||
|
fmt.Print(unifiedDiff(path, strings.Split(src, "\n"), strings.Split(out, "\n")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if dirMode || *write {
|
if dirMode || *write {
|
||||||
if out != src {
|
if out != src {
|
||||||
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unifiedDiff renders a unified diff with three lines of context between the
|
||||||
|
// two line slices, in the form `gofmt -d` prints. An empty result means the
|
||||||
|
// inputs are identical.
|
||||||
|
func unifiedDiff(name string, a, b []string) string {
|
||||||
|
if slices.Equal(a, b) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var out strings.Builder
|
||||||
|
fmt.Fprintf(&out, "--- %s\n+++ %s\n", name, name)
|
||||||
|
|
||||||
|
// Longest common subsequence over the lines (assembly files are small
|
||||||
|
// enough for the quadratic table).
|
||||||
|
n, m := len(a), len(b)
|
||||||
|
lcs := make([][]int, n+1)
|
||||||
|
for i := range lcs {
|
||||||
|
lcs[i] = make([]int, m+1)
|
||||||
|
}
|
||||||
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
for j := m - 1; j >= 0; j-- {
|
||||||
|
if a[i] == b[j] {
|
||||||
|
lcs[i][j] = lcs[i+1][j+1] + 1
|
||||||
|
} else if lcs[i+1][j] >= lcs[i][j+1] {
|
||||||
|
lcs[i][j] = lcs[i+1][j]
|
||||||
|
} else {
|
||||||
|
lcs[i][j] = lcs[i][j+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the LCS once, assigning every op its absolute position in both
|
||||||
|
// files (1-based, the position an insertion sits before).
|
||||||
|
type op struct {
|
||||||
|
kind byte // ' ', '-' or '+'
|
||||||
|
aLine, bLine int
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
var ops []op
|
||||||
|
aPos, bPos := 0, 0
|
||||||
|
emit := func(kind byte, text string) {
|
||||||
|
ops = append(ops, op{kind: kind, aLine: aPos + 1, bLine: bPos + 1, text: text})
|
||||||
|
switch kind {
|
||||||
|
case ' ':
|
||||||
|
aPos++
|
||||||
|
bPos++
|
||||||
|
case '-':
|
||||||
|
aPos++
|
||||||
|
case '+':
|
||||||
|
bPos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i, j := 0, 0
|
||||||
|
for i < n && j < m {
|
||||||
|
switch {
|
||||||
|
case a[i] == b[j]:
|
||||||
|
emit(' ', a[i])
|
||||||
|
i++
|
||||||
|
j++
|
||||||
|
case lcs[i+1][j] >= lcs[i][j+1]:
|
||||||
|
emit('-', a[i])
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
emit('+', b[j])
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; i < n; i++ {
|
||||||
|
emit('-', a[i])
|
||||||
|
}
|
||||||
|
for ; j < m; j++ {
|
||||||
|
emit('+', b[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group the edits into hunks: consecutive changes separated by more than
|
||||||
|
// twice the context lines start a new hunk.
|
||||||
|
const context = 3
|
||||||
|
var changes []int
|
||||||
|
for k, o := range ops {
|
||||||
|
if o.kind != ' ' {
|
||||||
|
changes = append(changes, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for g := 0; g < len(changes); {
|
||||||
|
last := g
|
||||||
|
for last+1 < len(changes) && changes[last+1]-changes[last]-1 <= 2*context {
|
||||||
|
last++
|
||||||
|
}
|
||||||
|
lo := max(0, changes[g]-context)
|
||||||
|
hi := min(len(ops), changes[last]+1+context)
|
||||||
|
// The header numbers are the first line of each side actually shown:
|
||||||
|
// the first context, deletion or insertion line. A hunk that shows
|
||||||
|
// no old lines is a pure insertion and reports the position it sits
|
||||||
|
// before (0 at the top of the file); the mirror rule holds for a
|
||||||
|
// pure deletion.
|
||||||
|
aStart := ops[lo].aLine - 1
|
||||||
|
bStart := ops[lo].bLine - 1
|
||||||
|
countA, countB := 0, 0
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
switch o.kind {
|
||||||
|
case ' ':
|
||||||
|
countA++
|
||||||
|
countB++
|
||||||
|
case '-':
|
||||||
|
countA++
|
||||||
|
case '+':
|
||||||
|
countB++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
if o.kind != '+' {
|
||||||
|
aStart = o.aLine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
if o.kind != '-' {
|
||||||
|
bStart = o.bLine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", aStart, countA, bStart, countB)
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
out.WriteByte(o.kind)
|
||||||
|
out.WriteString(o.text)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
g = last + 1
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lines(ss ...string) []string { return ss }
|
||||||
|
|
||||||
|
func TestUnifiedDiffIdentical(t *testing.T) {
|
||||||
|
if got := unifiedDiff("f", lines("a", "b"), lines("a", "b")); got != "" {
|
||||||
|
t.Errorf("identical inputs produced %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffSingleChange(t *testing.T) {
|
||||||
|
a := lines("1", "2", "3", "4", "5", "6", "7", "8")
|
||||||
|
b := lines("1", "2", "3!", "4", "5", "6", "7", "8")
|
||||||
|
want := "--- f\n+++ f\n" +
|
||||||
|
"@@ -1,6 +1,6 @@\n" +
|
||||||
|
" 1\n 2\n-3\n+3!\n 4\n 5\n 6\n"
|
||||||
|
if got := unifiedDiff("f", a, b); got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffInsertAtStart(t *testing.T) {
|
||||||
|
got := unifiedDiff("f", lines("x"), lines("new", "x"))
|
||||||
|
// The single existing line is shown as trailing context, so the hunk
|
||||||
|
// covers it.
|
||||||
|
want := "--- f\n+++ f\n@@ -1,1 +1,2 @@\n+new\n x\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffDeleteAtEnd(t *testing.T) {
|
||||||
|
got := unifiedDiff("f", lines("x", "y"), lines("x"))
|
||||||
|
want := "--- f\n+++ f\n@@ -1,2 +1,1 @@\n x\n-y\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffTwoHunks(t *testing.T) {
|
||||||
|
var a, b []string
|
||||||
|
for i := 1; i <= 20; i++ {
|
||||||
|
a = append(a, itoa(i))
|
||||||
|
b = append(b, itoa(i))
|
||||||
|
}
|
||||||
|
b[1] = "2!"
|
||||||
|
b[17] = "18!"
|
||||||
|
got := unifiedDiff("f", a, b)
|
||||||
|
if !strings.Contains(got, "@@ -1,5 +1,5 @@\n 1\n-2\n+2!\n 3\n 4\n 5\n") {
|
||||||
|
t.Errorf("first hunk wrong:\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "@@ -15,6 +15,6 @@\n 15\n 16\n 17\n-18\n+18!\n 19\n 20\n") {
|
||||||
|
t.Errorf("second hunk wrong:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnifiedDiffAdjacentHunks merges changes separated by exactly twice the
|
||||||
|
// context into one hunk.
|
||||||
|
func TestUnifiedDiffAdjacentHunks(t *testing.T) {
|
||||||
|
a := lines("1", "2", "3", "4", "5", "6", "7", "8")
|
||||||
|
b := slices.Clone(a)
|
||||||
|
b[0] = "1!"
|
||||||
|
b[7] = "8!"
|
||||||
|
got := unifiedDiff("f", a, b)
|
||||||
|
want := "--- f\n+++ f\n" +
|
||||||
|
"@@ -1,8 +1,8 @@\n" +
|
||||||
|
"-1\n+1!\n 2\n 3\n 4\n 5\n 6\n 7\n-8\n+8!\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func itoa(n int) string {
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var buf [4]byte
|
||||||
|
i := len(buf)
|
||||||
|
for n > 0 {
|
||||||
|
i--
|
||||||
|
buf[i] = byte('0' + n%10)
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return string(buf[i:])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user