// 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) { fi := riscvComputeFrame(t) prologue := riscvPrologue(fi) // First pass: compute label offsets (all RISC-V instructions are 4 bytes). // Labels are offset by the prologue length. offsets := map[string]int{} var instrs []*ast.Instr pos := len(prologue) 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 := append([]byte(nil), prologue...) for _, instr := range instrs { code, err := encodeRISCVInstr(instr, pos, offsets, fi) 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, fi riscvFrameInfo) ([]byte, error) { mnem := instr.Mnemonic.Text ops := instr.Operands var word uint32 // Handle pseudo-instructions and special cases first. switch mnem { case "RET": // RET = JALR X0, 0(X1) word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil case "CALL": // CALL is a pseudo-instruction; encode as NOP placeholder. word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil case "JMP": // JMP = JAL X0, target var target string if len(ops) >= 1 { target = labelFromOperand(ops[0]) } targetOff, ok := offsets[target] if !ok { return nil, fmt.Errorf("undefined label %q", target) } offset := int32(targetOff - pc) word = riscvJType(0, offset) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil case "JAL": rd := 0 var target string if len(ops) >= 2 { rd = regFromOperand(ops[0]) target = labelFromOperand(ops[1]) } else if len(ops) == 1 { 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) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } // FP conversion / move instructions use a separate table (rs2 encodes // the conversion type, not a register). Handle them before the main // table lookup. if cvtEnc, ok := riscvCvtTable[mnem]; ok { if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } rs1 := regFromOperand(ops[0]) rd := regFromOperand(ops[1]) if rd < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word := riscvCvtType(cvtEnc, rd, rs1) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } // R4-type fused multiply-add: INSTR rs1, rs2, rs3, rd (destination last). if fmaEnc, ok := riscvFmaTable[mnem]; ok { if len(ops) != 4 { return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops)) } rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[1]) rs3 := regFromOperand(ops[2]) rd := regFromOperand(ops[3]) if rd < 0 || rs1 < 0 || rs2 < 0 || rs3 < 0 { return nil, fmt.Errorf("invalid FP register in %s", mnem) } word := riscvFmaType(fmaEnc, rd, rs1, rs2, rs3) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } // CSR instructions: INSTR csr, rs1|uimm, rd (destination last). if csrEnc, ok := riscvCsrTable[mnem]; ok { if len(ops) != 3 { return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops)) } csr := immFromOperand(ops[0]) // CSR address (12-bit) rd := regFromOperand(ops[2]) // destination register if rd < 0 { return nil, fmt.Errorf("invalid destination register in %s", mnem) } var src int if csrEnc.imm { // Immediate variant: ops[1] is a 5-bit unsigned immediate. src = int(immFromOperand(ops[1])) if src < 0 || src > 31 { return nil, fmt.Errorf("%s: uimm out of range 0-31", mnem) } } else { // Register variant: ops[1] is a register. src = regFromOperand(ops[1]) if src < 0 { return nil, fmt.Errorf("invalid source register in %s", mnem) } } word := riscvCsrType(csrEnc, rd, src, csr) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } enc, ok := riscvInstrTable[mnem] if !ok { return nil, fmt.Errorf("unsupported RISC-V instruction %q", mnem) } switch { // R-type: Plan 9 order is INSTR src1, src2, dst (destination last). case len(ops) == 3 && isRTypeInstr(mnem): rs1 := regFromOperand(ops[0]) // source 1 (first operand) rs2 := regFromOperand(ops[1]) // source 2 (second operand) rd := regFromOperand(ops[2]) // destination (last operand) if rd < 0 || rs1 < 0 || rs2 < 0 { return nil, fmt.Errorf("invalid register in %s", mnem) } word = riscvRType(enc, rd, rs1, rs2) // AMO atomics: Plan 9 order is INSTR src, (addr), dst. case len(ops) == 3 && isAMOInstr(mnem): rs2 := regFromOperand(ops[0]) // source value rs1, _ := memFromOperandWithFrame(ops[1], fi) // memory address rd := regFromOperand(ops[2]) // destination (old value) if rd < 0 || rs1 < 0 || rs2 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvAMOType(enc, rd, rs1, rs2) // FP arithmetic: Plan 9 order is INSTR src1, src2, dst. case len(ops) == 3 && isFPArithInstr(mnem): rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[1]) rd := regFromOperand(ops[2]) if rd < 0 || rs1 < 0 || rs2 < 0 { return nil, fmt.Errorf("invalid FP register in %s", mnem) } word = riscvRType(enc, rd, rs1, rs2) // FP arithmetic (2-operand): FSQRT src, dst. case len(ops) == 2 && isFPArithInstr(mnem): rs1 := regFromOperand(ops[0]) rd := regFromOperand(ops[1]) if rd < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid FP register in %s", mnem) } word = riscvRType(enc, rd, rs1, 0) // FP loads: INSTR addr, freg (Plan 9: source first). case len(ops) == 2 && isFPLoadInstr(mnem): rd := regFromOperand(ops[1]) rs1, imm := memFromOperandWithFrame(ops[0], fi) if rd < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvIType(enc, rd, rs1, imm) // FP stores: INSTR freg, addr (Plan 9: source first). case len(ops) == 2 && isFPStoreInstr(mnem): rs2 := regFromOperand(ops[0]) rs1, imm := memFromOperandWithFrame(ops[1], fi) if rs2 < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvSType(enc, rs1, rs2, imm) // LR (load-reserved): INSTR (addr), dst — 2 operands. case len(ops) == 2 && isLRInstr(mnem): rs1, _ := memFromOperandWithFrame(ops[0], fi) rd := regFromOperand(ops[1]) if rd < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvAMOType(enc, rd, rs1, 0) // rs2=0 for LR // SC (store-conditional): INSTR src, (addr), dst — 3 operands. case len(ops) == 3 && isSCInstr(mnem): rs2 := regFromOperand(ops[0]) rs1, _ := memFromOperandWithFrame(ops[1], fi) rd := regFromOperand(ops[2]) if rd < 0 || rs1 < 0 || rs2 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvAMOType(enc, rd, rs1, rs2) // FP compare: INSTR src1, src2, dst(int) — result in integer register. case len(ops) == 3 && isFPCmpInstr(mnem): rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[1]) rd := regFromOperand(ops[2]) if rd < 0 || rs1 < 0 || rs2 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvRType(enc, rd, rs1, rs2) // I-type with immediate: Plan 9 order is INSTR src, imm, dst. case len(ops) == 3 && isITypeInstr(mnem): rs1 := regFromOperand(ops[0]) // source register imm := immFromOperand(ops[1]) // immediate rd := regFromOperand(ops[2]) // destination if rd < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid register in %s", mnem) } word = riscvIType(enc, rd, rs1, imm) // Loads: rd, offset(rs1) — Plan 9 order is LD src, dst. case len(ops) == 2 && isLoadInstr(mnem): rd := regFromOperand(ops[1]) // destination (last operand) rs1, imm := memFromOperandWithFrame(ops[0], fi) // memory source (first operand) if rd < 0 || rs1 < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } word = riscvIType(enc, rd, rs1, imm) // Stores: Plan 9 order is SD src, dst (src=register, dst=memory). case len(ops) == 2 && isStoreInstr(mnem): rs2 := regFromOperand(ops[0]) // source register (first operand) rs1, imm := memFromOperandWithFrame(ops[1], fi) // memory dest (last operand) 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) 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" } func isAMOInstr(m string) bool { switch m { case "AMOSWAPW", "AMOSWAPD", "AMOADDW", "AMOADDD", "AMOANDW", "AMOANDD", "AMOORW", "AMOORD", "AMOXORW", "AMOXORD", "AMOMAXW", "AMOMAXD", "AMOMINW", "AMOMIND", "AMOMAXUW", "AMOMAXUD", "AMOMINUW", "AMOMINUD": return true } return false } func isFPArithInstr(m string) bool { switch m { case "FADDS", "FSUBS", "FMULS", "FDIVS", "FADDD", "FSUBD", "FMULD", "FDIVD", "FSQRTS", "FSQRTD", "FMINS", "FMAXS", "FMIND", "FMAXD": return true } return false } func isFPLoadInstr(m string) bool { return m == "FLW" || m == "FLD" } func isFPStoreInstr(m string) bool { return m == "FSW" || m == "FSD" } func isLRInstr(m string) bool { return m == "LRW" || m == "LRD" } func isSCInstr(m string) bool { return m == "SCW" || m == "SCD" } func isFPCmpInstr(m string) bool { switch m { case "FEQS", "FLTS", "FLES", "FEQD", "FLTD", "FLED": return true } return false } func isFPCvtInstr(m string) bool { _, ok := riscvCvtTable[m] return ok } // 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 } // memFromOperandWithFrame resolves a memory operand, handling FP/SP // pseudo-registers via the frame mapping. func memFromOperandWithFrame(op *ast.Operand, fi riscvFrameInfo) (rs1 int, imm int32) { // Check for a pseudo-register reference (name+offset(FP) or name+offset(SP)). if op.Addr.Sym != nil && op.Addr.Sym.Pseudo != "" { return riscvResolvePseudo(op.Addr.Sym, fi) } // Plain register+offset memory reference. return memFromOperand(op) } func labelFromOperand(op *ast.Operand) string { if op.Addr.Sym != nil { return op.Addr.Sym.Name } return op.Raw }