From e8b6ff5d7c23b60176870aed31bdd0bafafe85dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 21 Sep 2026 00:44:47 +0200 Subject: [PATCH] fix(parser): fold a signed parenthesised displacement expression Assisted-by: GLM 5.3 Flash --- asm/link.go | 70 +++++++++++++++++++++++++++++++++++++---- asm/link_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++ parser/parser.go | 14 +++++++++ parser/parser_test.go | 24 ++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) diff --git a/asm/link.go b/asm/link.go index ab5ef97..3a4ab8c 100644 --- a/asm/link.go +++ b/asm/link.go @@ -5,6 +5,7 @@ package asm import ( "fmt" + "math" "sort" "strconv" @@ -299,6 +300,10 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) { if err != nil { return nil, err } + // The pooled $i64 constants the wide MOV immediate loads refer to join + // the declared data as read-only symbols, deduplicated across the file + // (the toolchain synthesises the same symbols into its rodata). + litSeen := map[string]bool{} img := &Image{Symbols: map[string]int{}, SourcePath: f.Path} for _, d := range f.Decls { @@ -306,10 +311,23 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) { if !ok { continue } - code, labels, relocs, lines, spadj, err := assembleRISCV(t) + code, labels, relocs, lines, spadj, lits, err := assembleRISCV(t) if err != nil { return nil, fmt.Errorf("%s: %w", t.Name.Name, err) } + for _, lit := range lits { + if litSeen[lit.Name] { + continue + } + litSeen[lit.Name] = true + dataSyms = append(dataSyms, dataSym{ + name: lit.Name, + buf: lit.Data, + size: len(lit.Data), + rodata: true, + dupok: true, + }) + } fl := FuncLayout{ Name: t.Name.Name, Pkg: t.Name.Pkg, @@ -561,11 +579,6 @@ func collectData(f *ast.File) ([]dataSym, error) { return nil, fmt.Errorf("DATA %q: missing value", dd.Name.Name) } w := dd.Width - switch w { - case 1, 2, 4, 8: - default: - return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w) - } off := dd.Name.Offset buf := syms[i].buf if off < 0 || off+int64(w) > int64(len(buf)) { @@ -587,9 +600,54 @@ func collectData(f *ast.File) ([]dataSym, error) { }) continue } + // A string or rune value ("DATA s+0(SB)/20, $"text"") writes its + // bytes into the field and leaves the rest zero, the toolchain's + // WriteString: the declared width must hold every byte, and any + // width is legal. + if s := dd.Value.Imm.Str; s != "" && !dd.Value.Imm.HasVal { + text, err := strconv.Unquote(s) + if err != nil { + return nil, fmt.Errorf("DATA %q: invalid string value %s", dd.Name.Name, s) + } + if len(text) > w { + return nil, fmt.Errorf("DATA %q: string of %d bytes does not fit width %d", dd.Name.Name, len(text), w) + } + copy(buf[off:], text) + continue + } + // A floating-point value stores its IEEE-754 bits: /4 the float32 + // rounding of the parsed double, /8 the full 64 bits, the + // toolchain's WriteFloat32 and WriteFloat64. + if f := dd.Value.Imm.Float; f != "" && !dd.Value.Imm.HasVal { + num, err := strconv.ParseFloat(f, 64) + if err != nil { + return nil, fmt.Errorf("DATA %q: invalid floating-point value %q", dd.Name.Name, f) + } + if dd.Value.Imm.Neg { + num = -num + } + var v uint64 + switch w { + case 4: + v = uint64(math.Float32bits(float32(num))) + case 8: + v = math.Float64bits(num) + default: + return nil, fmt.Errorf("DATA %q: invalid width %d for a float (want 4 or 8)", dd.Name.Name, w) + } + for j := range w { + buf[off+int64(j)] = byte(v >> (8 * j)) + } + continue + } if !dd.Value.Imm.HasVal { return nil, fmt.Errorf("DATA %q: value must be an integer immediate or a symbol address", dd.Name.Name) } + switch w { + case 1, 2, 4, 8: + default: + return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w) + } v := dd.Value.Imm.Val if dd.Value.Imm.Neg { v = -v diff --git a/asm/link_test.go b/asm/link_test.go index c670f15..d6fd2b6 100644 --- a/asm/link_test.go +++ b/asm/link_test.go @@ -5,6 +5,7 @@ package asm import ( "encoding/binary" + "fmt" "os" "os/exec" "path/filepath" @@ -418,3 +419,75 @@ func main() { t.Fatalf("linked program failed: %v\n%s", err, out) } } + +// TestCollectDataFloatAndStringValues covers the non-integer DATA values the +// runtime's math and asm files use: floating-point initialisers store their +// IEEE-754 bits (/4 the float32 rounding, /8 the full double) and string +// initialisers write their bytes zero-padded within the declared width. +func TestCollectDataFloatAndStringValues(t *testing.T) { + src := `#include "textflag.h" +TEXT ·Keep(SB), NOSPLIT, $0-8 + RET +GLOBL vals<>(SB), RODATA, $44 +DATA vals<>+0(SB)/8, $0.5 +DATA vals<>+8(SB)/8, $-1.0 +DATA vals<>+16(SB)/4, $1.5 +DATA vals<>+20(SB)/16, $"call frame too " +DATA vals<>+36(SB)/4, $"hi" +` + f, errs := parser.Parse("fvals_amd64.s", src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + byName := map[string]DataSymbol{} + for _, d := range img.DataSyms { + byName[d.Name] = d + } + d := byName["vals"] + if d.Size != 44 { + t.Fatalf("vals size = %d, want 44", d.Size) + } + buf := img.Data[d.Offset : d.Offset+44] + // 0.5 = 0x3FE0000000000000, -1.0 = 0xBFF0000000000000 (float64); + // 1.5 = 0x3FC00000 (float32). + for _, c := range []struct { + off int + want []byte + }{ + {0, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x3F}}, + {8, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF}}, + {16, []byte{0x00, 0x00, 0xC0, 0x3F}}, + {20, []byte("call frame too ")}, + {36, []byte{'h', 'i', 0x00, 0x00}}, + } { + if string(buf[c.off:c.off+len(c.want)]) != string(c.want) { + t.Errorf("vals+%d: got % x, want % x", c.off, buf[c.off:c.off+len(c.want)], c.want) + } + } +} + +// TestCollectDataValueErrors pins the value-kind width rules: a float needs +// width 4 or 8, a string must fit its declared width, and a bad float +// literal is diagnosed rather than stored. +func TestCollectDataValueErrors(t *testing.T) { + cases := []string{ + `GLOBL v<>(SB), RODATA, $4 +DATA v<>+0(SB)/1, $0.5`, + `GLOBL v<>(SB), RODATA, $2 +DATA v<>+0(SB)/2, $"toolarge"`, + } + for i, src := range cases { + full := "#include \"textflag.h\"\nTEXT ·Keep(SB), NOSPLIT, $0-8\n\tRET\n" + src + f, errs := parser.Parse(fmt.Sprintf("verr%d_amd64.s", i), full) + if len(errs) > 0 { + t.Fatalf("case %d parse: %v", i, errs) + } + if _, err := AssembleFile(f); err == nil { + t.Errorf("case %d: expected an error, got none", i) + } + } +} diff --git a/parser/parser.go b/parser/parser.go index 8771a3e..0601c2c 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -552,6 +552,20 @@ func parseAddress(g []token.Token) ast.Address { i = len(g) - len(rest) } } + // The same expression under a leading sign: -(24+8)(X6) puts the sign + // outside the fold. The base group must follow for the value to + // commit, exactly as in the unsigned branch above. + if i < len(g) && (g[i].Kind == token.Minus || g[i].Kind == token.Plus) && + i+1 < len(g) && g[i+1].Kind == token.LParen { + if v, rest, ok := foldExpr(g[i+1:]); ok && len(rest) > 0 && rest[0].Kind == token.LParen { + if g[i].Kind == token.Minus { + v = -v + } + addr.Offset = v + addr.HasOff = true + i = len(g) - len(rest) + } + } // Optional leading displacement before a '(' base group. A sign pushes // the parenthesis one token further out: -4(DX) has it at i+2. if isSignedNumber(g, i) { diff --git a/parser/parser_test.go b/parser/parser_test.go index 4c18551..8e4e4f4 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -634,3 +634,27 @@ func TestBareTrailingImmediate(t *testing.T) { t.Errorf("jump target = %+v, want label loop", jmp.Operands[0]) } } + +// TestSignedParenDisplacement pins a sign before a parenthesised +// displacement expression: -(24+8)(X6) negates the folded value and keeps +// the base group, the shape GOROOT's riscv64 and loong64 files use. +func TestSignedParenDisplacement(t *testing.T) { + file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0-0\n\tMOV X7, -(24+8)(X6)\n\tMOV X7, +(16)(X6)\n\tRET\n") + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + text := file.Decls[0].(*ast.Text) + ins := text.Body[0].(*ast.Instr) + op := ins.Operands[1] // Plan 9 order: the destination address is last + if !op.Addr.HasOff || op.Addr.Offset != -32 { + t.Errorf("-(24+8): offset = %v hasOff=%v, want -32 true", op.Addr.Offset, op.Addr.HasOff) + } + if op.Addr.Base != "X6" { + t.Errorf("-(24+8): base = %q, want X6", op.Addr.Base) + } + ins = text.Body[1].(*ast.Instr) + op = ins.Operands[1] + if !op.Addr.HasOff || op.Addr.Offset != 16 || op.Addr.Base != "X6" { + t.Errorf("+(16): offset = %v hasOff=%v base=%q, want 16 true X6", op.Addr.Offset, op.Addr.HasOff, op.Addr.Base) + } +}