diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go new file mode 100644 index 0000000..e9618ca --- /dev/null +++ b/asm/riscv_assemble.go @@ -0,0 +1,237 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "fmt" + + "sourcedock.dev/petrbalvin/gasm-devkit/ast" +) + +// assembleRISCV assembles a RISC-V TEXT function body into machine code. +// It handles the core RV64I/RV64M instruction set with fixed 32-bit encoding. +func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) { + // First pass: compute label offsets (all RISC-V instructions are 4 bytes). + offsets := map[string]int{} + var instrs []*ast.Instr + pos := 0 + for _, stmt := range t.Body { + switch s := stmt.(type) { + case *ast.Label: + offsets[s.Name.Text] = pos + case *ast.Instr: + instrs = append(instrs, s) + pos += 4 // all RISC-V instructions are 4 bytes + } + } + + // Second pass: encode instructions. + out := make([]byte, 0, pos) + for _, instr := range instrs { + code, err := encodeRISCVInstr(instr, pos, offsets) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", instr.Mnemonic.Text, err) + } + out = append(out, code...) + pos -= 4 // track remaining + } + return out, offsets, nil +} + +// encodeRISCVInstr encodes a single RISC-V instruction. +func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int) ([]byte, error) { + mnem := instr.Mnemonic.Text + enc, ok := riscvInstrTable[mnem] + if !ok { + return nil, fmt.Errorf("unsupported RISC-V instruction %q", mnem) + } + + ops := instr.Operands + var word uint32 + + switch { + // R-type: rd, rs1, rs2 (3 register operands). + case len(ops) == 3 && isRTypeInstr(mnem): + rd := regFromOperand(ops[0]) + rs1 := regFromOperand(ops[1]) + rs2 := regFromOperand(ops[2]) + if rd < 0 || rs1 < 0 || rs2 < 0 { + return nil, fmt.Errorf("invalid register in %s", mnem) + } + word = riscvRType(enc, rd, rs1, rs2) + + // I-type with immediate: rd, rs1, imm. + case len(ops) == 3 && isITypeInstr(mnem): + rd := regFromOperand(ops[0]) + rs1 := regFromOperand(ops[1]) + imm := immFromOperand(ops[2]) + if rd < 0 || rs1 < 0 { + return nil, fmt.Errorf("invalid register in %s", mnem) + } + word = riscvIType(enc, rd, rs1, imm) + + // Loads: rd, offset(rs1) — parsed as rd, (rs1)+offset. + case len(ops) == 2 && isLoadInstr(mnem): + rd := regFromOperand(ops[0]) + rs1, imm := memFromOperand(ops[1]) + if rd < 0 || rs1 < 0 { + return nil, fmt.Errorf("invalid operand in %s", mnem) + } + word = riscvIType(enc, rd, rs1, imm) + + // Stores: rs2, offset(rs1). + case len(ops) == 2 && isStoreInstr(mnem): + rs2 := regFromOperand(ops[0]) + rs1, imm := memFromOperand(ops[1]) + if rs2 < 0 || rs1 < 0 { + return nil, fmt.Errorf("invalid operand in %s", mnem) + } + word = riscvSType(enc, rs1, rs2, imm) + + // Branches: rs1, rs2, label. + case len(ops) == 3 && isBranchInstr(mnem): + rs1 := regFromOperand(ops[0]) + rs2 := regFromOperand(ops[1]) + target := labelFromOperand(ops[2]) + targetOff, ok := offsets[target] + if !ok { + return nil, fmt.Errorf("undefined label %q", target) + } + rs1Off := 0 // placeholder + _ = rs1Off + offset := int32(targetOff - pc) + if rs1 < 0 || rs2 < 0 { + return nil, fmt.Errorf("invalid register in %s", mnem) + } + word = riscvBType(enc, rs1, rs2, offset) + + // U-type: rd, imm. + case len(ops) == 2 && isUTypeInstr(mnem): + rd := regFromOperand(ops[0]) + imm := immFromOperand(ops[1]) + if rd < 0 { + return nil, fmt.Errorf("invalid register in %s", mnem) + } + word = riscvUType(enc, rd, imm) + + // JAL: rd, label (or just label for JMP). + case mnem == "JAL" || mnem == "JMP": + rd := 0 // JMP uses rd=0 (X0) + if len(ops) >= 1 && mnem == "JAL" { + rd = regFromOperand(ops[0]) + } + var target string + if len(ops) >= 2 { + target = labelFromOperand(ops[len(ops)-1]) + } else if len(ops) == 1 && mnem == "JMP" { + target = labelFromOperand(ops[0]) + } + targetOff, ok := offsets[target] + if !ok { + return nil, fmt.Errorf("undefined label %q", target) + } + offset := int32(targetOff - pc) + word = riscvJType(rd, offset) + + // RET: JALR X0, 0(X1). + case mnem == "RET": + word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0) + + // CALL: AUIPC + JALR sequence (simplified: just encode as NOP placeholder). + case mnem == "CALL": + // CALL is a pseudo-instruction that expands to AUIPC + JALR. + // For now, encode as a NOP (ADDI X0, X0, 0). + word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0) + + default: + return nil, fmt.Errorf("cannot encode %s with %d operands", mnem, len(ops)) + } + + // Emit as little-endian 32-bit word. + return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil +} + +// Instruction type classifiers. +func isRTypeInstr(m string) bool { + switch m { + case "ADD", "SUB", "SLL", "SLT", "SLTU", "XOR", "SRL", "SRA", "OR", "AND", + "ADDW", "SUBW", "SLLW", "SRLW", "SRAW", + "MUL", "MULH", "MULHSU", "MULHU", "DIV", "DIVU", "REM", "REMU", + "MULW", "DIVW", "DIVUW", "REMW", "REMUW": + return true + } + return false +} + +func isITypeInstr(m string) bool { + switch m { + case "ADDI", "ADDIW", "SLTI", "SLTIU", "XORI", "ORI", "ANDI", "JALR": + return true + } + return false +} + +func isLoadInstr(m string) bool { + switch m { + case "LB", "LH", "LW", "LD", "LBU", "LHU", "LWU": + return true + } + return false +} + +func isStoreInstr(m string) bool { + switch m { + case "SB", "SH", "SW", "SD": + return true + } + return false +} + +func isBranchInstr(m string) bool { + switch m { + case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU": + return true + } + return false +} + +func isUTypeInstr(m string) bool { + return m == "LUI" || m == "AUIPC" +} + +// Operand helpers. +func regFromOperand(op *ast.Operand) int { + // Register is in Addr.Base (from (base) syntax) or Addr.Sym.Name (bare ident). + if op.Addr.Base != "" { + return riscvRegNum(op.Addr.Base) + } + if op.Addr.Sym != nil && op.Addr.Sym.Name != "" { + return riscvRegNum(op.Addr.Sym.Name) + } + return -1 +} + +func immFromOperand(op *ast.Operand) int32 { + if op.Imm.HasVal { + v := op.Imm.Val + if op.Imm.Neg { + v = -v + } + return int32(v) + } + return 0 +} + +func memFromOperand(op *ast.Operand) (rs1 int, imm int32) { + rs1 = riscvRegNum(op.Addr.Base) + imm = int32(op.Addr.Offset) + return +} + +func labelFromOperand(op *ast.Operand) string { + if op.Addr.Sym != nil { + return op.Addr.Sym.Name + } + return op.Raw +} diff --git a/asm/riscv_encode.go b/asm/riscv_encode.go new file mode 100644 index 0000000..fde4241 --- /dev/null +++ b/asm/riscv_encode.go @@ -0,0 +1,271 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +// RISC-V register encoding: maps register names to their 5-bit numbers. +// The Go assembler uses the standard RISC-V ABI naming. + +// riscvRegNum returns the 5-bit register number for a RISC-V register name. +// Returns -1 if the register is not recognized. +func riscvRegNum(name string) int { + switch name { + // Numbered integer registers. + case "X0", "ZERO": + return 0 + case "X1", "RA": + return 1 + case "X2", "SP": + return 2 + case "X3", "GP": + return 3 + case "X4", "TP": + return 4 + case "X5", "T0", "LR": + return 5 + case "X6", "T1", "TMP": + return 6 + case "X7", "T2": + return 7 + case "X8", "S0", "FP": + return 8 + case "X9", "S1": + return 9 + case "X10", "A0": + return 10 + case "X11", "A1": + return 11 + case "X12", "A2": + return 12 + case "X13", "A3": + return 13 + case "X14", "A4": + return 14 + case "X15", "A5": + return 15 + case "X16", "A6": + return 16 + case "X17", "A7": + return 17 + case "X18", "S2": + return 18 + case "X19", "S3": + return 19 + case "X20", "S4": + return 20 + case "X21", "S5": + return 21 + case "X22", "S6": + return 22 + case "X23", "S7": + return 23 + case "X24", "S8": + return 24 + case "X25", "S9": + return 25 + case "X26", "S10": + return 26 + case "X27", "S11": + return 27 + case "X28", "T3": + return 28 + case "X29", "T4": + return 29 + case "X30", "T5": + return 30 + case "X31", "T6": + return 31 + // Floating-point registers (F0-F31). + case "F0", "FT0": + return 0 + case "F1", "FT1": + return 1 + case "F2", "FT2": + return 2 + case "F3", "FT3": + return 3 + case "F4", "FT4": + return 4 + case "F5", "FT5": + return 5 + case "F6", "FT6": + return 6 + case "F7", "FT7": + return 7 + case "F8", "FS0": + return 8 + case "F9", "FS1": + return 9 + case "F10", "FA0": + return 10 + case "F11", "FA1": + return 11 + case "F12", "FA2": + return 12 + case "F13", "FA3": + return 13 + case "F14", "FA4": + return 14 + case "F15", "FA5": + return 15 + case "F16", "FA6": + return 16 + case "F17", "FA7": + return 17 + case "F18", "FS2": + return 18 + case "F19", "FS3": + return 19 + case "F20", "FS4": + return 20 + case "F21", "FS5": + return 21 + case "F22", "FS6": + return 22 + case "F23", "FS7": + return 23 + case "F24", "FS8": + return 24 + case "F25", "FS9": + return 25 + case "F26", "FS10": + return 26 + case "F27", "FS11": + return 27 + case "F28", "FT8": + return 28 + case "F29", "FT9": + return 29 + case "F30", "FT10": + return 30 + case "F31", "FT11": + return 31 + default: + return -1 + } +} + +// RISC-V instruction encoding parameters. +type riscvEnc struct { + opcode uint32 // bits [6:0] + funct3 uint32 // bits [14:12] + funct7 uint32 // bits [31:25] +} + +// riscvInstrTable maps RISC-V mnemonics to their encoding. +var riscvInstrTable = map[string]riscvEnc{ + // RV64I — R-type arithmetic/logic. + "ADD": {0x33, 0x0, 0x00}, + "SUB": {0x33, 0x0, 0x20}, + "SLL": {0x33, 0x1, 0x00}, + "SLT": {0x33, 0x2, 0x00}, + "SLTU": {0x33, 0x3, 0x00}, + "XOR": {0x33, 0x4, 0x00}, + "SRL": {0x33, 0x5, 0x00}, + "SRA": {0x33, 0x5, 0x20}, + "OR": {0x33, 0x6, 0x00}, + "AND": {0x33, 0x7, 0x00}, + // RV64I — 32-bit variants (W suffix). + "ADDW": {0x3B, 0x0, 0x00}, + "SUBW": {0x3B, 0x0, 0x20}, + "SLLW": {0x3B, 0x1, 0x00}, + "SRLW": {0x3B, 0x5, 0x00}, + "SRAW": {0x3B, 0x5, 0x20}, + // RV64M — multiply/divide. + "MUL": {0x33, 0x0, 0x01}, + "MULH": {0x33, 0x1, 0x01}, + "MULHSU": {0x33, 0x2, 0x01}, + "MULHU": {0x33, 0x3, 0x01}, + "DIV": {0x33, 0x4, 0x01}, + "DIVU": {0x33, 0x5, 0x01}, + "REM": {0x33, 0x6, 0x01}, + "REMU": {0x33, 0x7, 0x01}, + // RV64M — 32-bit variants. + "MULW": {0x3B, 0x0, 0x01}, + "DIVW": {0x3B, 0x4, 0x01}, + "DIVUW": {0x3B, 0x5, 0x01}, + "REMW": {0x3B, 0x6, 0x01}, + "REMUW": {0x3B, 0x7, 0x01}, + // RV64I — I-type arithmetic. + "ADDI": {0x13, 0x0, 0x00}, + "ADDIW": {0x1B, 0x0, 0x00}, + "SLTI": {0x13, 0x2, 0x00}, + "SLTIU": {0x13, 0x3, 0x00}, + "XORI": {0x13, 0x4, 0x00}, + "ORI": {0x13, 0x6, 0x00}, + "ANDI": {0x13, 0x7, 0x00}, + // Loads (I-type). + "LB": {0x03, 0x0, 0x00}, + "LH": {0x03, 0x1, 0x00}, + "LW": {0x03, 0x2, 0x00}, + "LD": {0x03, 0x3, 0x00}, + "LBU": {0x03, 0x4, 0x00}, + "LHU": {0x03, 0x5, 0x00}, + "LWU": {0x03, 0x6, 0x00}, + // Stores (S-type). + "SB": {0x23, 0x0, 0x00}, + "SH": {0x23, 0x1, 0x00}, + "SW": {0x23, 0x2, 0x00}, + "SD": {0x23, 0x3, 0x00}, + // Branches (B-type). + "BEQ": {0x63, 0x0, 0x00}, + "BNE": {0x63, 0x1, 0x00}, + "BLT": {0x63, 0x4, 0x00}, + "BGE": {0x63, 0x5, 0x00}, + "BLTU": {0x63, 0x6, 0x00}, + "BGEU": {0x63, 0x7, 0x00}, + // U-type. + "LUI": {0x37, 0x0, 0x00}, + "AUIPC": {0x17, 0x0, 0x00}, + // System. + "ECALL": {0x73, 0x0, 0x00}, + "EBREAK": {0x73, 0x0, 0x00}, + "FENCE": {0x0F, 0x0, 0x00}, +} + +// riscvRType encodes an R-type instruction: funct7 | rs2 | rs1 | funct3 | rd | opcode. +func riscvRType(enc riscvEnc, rd, rs1, rs2 int) uint32 { + return (enc.funct7 << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) | + (enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode +} + +// riscvIType encodes an I-type instruction: imm[11:0] | rs1 | funct3 | rd | opcode. +func riscvIType(enc riscvEnc, rd, rs1 int, imm int32) uint32 { + return (uint32(imm&0xFFF) << 20) | (uint32(rs1) << 15) | + (enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode +} + +// riscvSType encodes an S-type instruction: imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode. +func riscvSType(enc riscvEnc, rs1, rs2 int, imm int32) uint32 { + immU := uint32(imm) & 0xFFF + return ((immU >> 5) << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) | + (enc.funct3 << 12) | ((immU & 0x1F) << 7) | enc.opcode +} + +// riscvBType encodes a B-type instruction (branches). +func riscvBType(enc riscvEnc, rs1, rs2 int, offset int32) uint32 { + imm := uint32(offset) & 0x1FFE // bits [12:1], bit 0 is always 0 + return (((imm >> 12) & 1) << 31) | // imm[12] + (((imm >> 5) & 0x3F) << 25) | // imm[10:5] + (uint32(rs2) << 20) | (uint32(rs1) << 15) | + (enc.funct3 << 12) | + (((imm >> 1) & 0xF) << 8) | // imm[4:1] + (((imm >> 11) & 1) << 7) | // imm[11] + enc.opcode +} + +// riscvUType encodes a U-type instruction: imm[31:12] | rd | opcode. +func riscvUType(enc riscvEnc, rd int, imm int32) uint32 { + return (uint32(imm) & 0xFFFFF000) | (uint32(rd) << 7) | enc.opcode +} + +// riscvJType encodes a J-type instruction (JAL). +func riscvJType(rd int, offset int32) uint32 { + imm := uint32(offset) & 0x1FFFFE // bits [20:1] + return (((imm >> 20) & 1) << 31) | // imm[20] + (((imm >> 1) & 0x3FF) << 21) | // imm[10:1] + (((imm >> 11) & 1) << 20) | // imm[11] + (((imm >> 12) & 0xFF) << 12) | // imm[19:12] + (uint32(rd) << 7) | + 0x6F // JAL opcode +}