feat(asm): encode the explicit riscv64 compressed instructions

The C extension's own spellings were names the table carried and the
encoder refused: CLWSP stopped the corpus audit's riscv64 file first.
Thirty-eight mnemonics now encode directly to their halfword, with the
toolchain's operand spellings and validation: the stack loads and stores
pin their base to SP, the register-based loads, stores and arithmetic
carry prime registers, CLUI refuses zero and SP, CADDI4SPN scales by
four, CADDI16SP by sixteen, and CJ, CBEQZ and CBNEZ resolve their N(PC)
targets against the final layout, taking a two-byte placeholder in the
early passes so the offsets stay honest.  CAND with an immediate is the
toolchain's C.ANDI spelling.  The toolchain's whole C extension testdata
block is a differential test, halfword for halfword, beside a range test
at the toolchain's own boundaries.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 00:47:27 +02:00
1 parent ddfa33ccc1
commit e43fa39dc9
2 files changed
+716 -8

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"os"
"path/filepath"
"testing"
)
// TestRISCVCompressedDifferential proves the explicit compressed-instruction
// mnemonics against the toolchain: the "C" extension block of the toolchain's
// own testdata (every stack, register, control-transfer, constant-generation,
// shift and register-register spelling it carries) assembled by gasm and by
// go tool asm must agree halfword for halfword. The lines are the oracle's
// own, so a wrong bit pattern, scale or register field names itself through
// the first differing halfword.
func TestRISCVCompressedDifferential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·compressed(SB), NOSPLIT, $0
CLWSP 20(SP), X10
CLDSP 24(SP), X10
CFLDSP 32(SP), F10
CSWSP X10, 20(SP)
CSDSP X10, 24(SP)
CFSDSP F10, 32(SP)
CLW 20(X10), X11
CLD 24(X10), X11
CFLD 32(X10), F11
CSW X11, 20(X10)
CSD X11, 24(X10)
CFSD F11, 32(X10)
CJ 1(PC)
CJR X5
CJALR X5
CBEQZ X10, 1(PC)
CBNEZ X10, 1(PC)
CLI $-32, X5
CLI $31, X5
CLUI $-32, X5
CLUI $31, X5
CADD $-32, X5
CADD $31, X5
CADDI $-32, X5
CADDI $31, X5
CADDW $-32, X5
CADDW $31, X5
CADDIW $-32, X5
CADDIW $31, X5
CADDI16SP $-512, SP
CADDI16SP $496, SP
CADDI4SPN $4, SP, X10
CADDI4SPN $1020, SP, X10
CSLLI $63, X5
CSRLI $63, X10
CSRAI $63, X10
CAND $-32, X10
CAND $31, X10
CANDI $-32, X10
CANDI $31, X10
CMV X6, X5
CADD X9, X8
CAND X9, X8
COR X9, X8
CXOR X9, X8
CSUB X9, X8
CADDW X9, X8
CSUBW X9, X8
CNOP
CEBREAK
RET
`
dir := t.TempDir()
path := filepath.Join(dir, "compressed_riscv64.s")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
assertRISCVDifferential(t, path, src, "compressed")
}
// TestRISCVCompressedRange pins the compressed immediate and offset ranges at
// the toolchain's own boundaries: a stack load off the scale or range, a
// non-prime register in the CL/CS and CA shapes, a zero immediate where the
// toolchain forbids one and a CLUI into SP are all rejected on sight.
func TestRISCVCompressedRange(t *testing.T) {
asmOne := func(t *testing.T, stmt string) error {
t.Helper()
fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·c(SB), NOSPLIT, $0\n\t"+stmt+"\n\tRET\n")
_, _, _, _, _, _, err := assembleRISCV(fn)
return err
}
for _, s := range []string{
"CLWSP $0(SP), X10", // never spelled; the parser rejects the shape
} {
if err := asmOne(t, s); err == nil {
t.Errorf("%s must be rejected", s)
}
}
for _, s := range []string{
"CLWSP 21(SP), X10", // not a multiple of 4
"CLWSP 256(SP), X10", // out of range
"CLDSP 25(SP), X10", // not a multiple of 8
"CFLDSP 33(SP), F10", // not a multiple of 8
"CLWSP 20(X10), X10", // base must be SP
"CLW 22(X10), X11", // not a multiple of 4
"CLW 128(X10), X11", // out of range
"CLW 20(X5), X11", // base must be prime
"CLW 20(X10), X5", // rd must be prime
"CLI $32, X5", // out of range
"CLI $-33, X5", // out of range
"CLUI $0, X5", // zero
"CLUI $3, X2", // SP as destination
"CSLLI $0, X5", // zero shift
"CSLLI $64, X5", // out of range
"CSRLI $63, X5", // rd must be prime
"CANDI $63, X10", // out of range
"CMV X0, X5", // X0 in rd
"CMV X5, X0", // X0 in rs2
"CADD X5, X0", // X0 in rs2
"CSUB X5, X5", // X0-free but rd prime required
"CADDI4SPN $4, X5, X10", /* base must be SP */
} {
if err := asmOne(t, s); err == nil {
t.Errorf("%s must be rejected, as go tool asm rejects it", s)
}
}
for _, s := range []string{
"CLWSP 20(SP), X10", "CLDSP 24(SP), X10",
"CLW 20(X10), X11", "CSD X11, 24(X10)",
"CLI $-32, X5", "CLUI $-32, X5",
"CADD $-32, X5", "CADDIW $31, X5",
"CSLLI $63, X5", "CSRLI $63, X10", "CANDI $-32, X10",
"CMV X6, X5", "CADD X9, X8", "CSUB X9, X8", "CADDW X9, X8",
"CADDI16SP $496, SP", "CADDI4SPN $1020, SP, X10",
} {
if err := asmOne(t, s); err != nil {
t.Errorf("%s must assemble: %v", s, err)
}
}
}