Skip to content

Source: moonlive_asm_riscv.h

moonlive_asm_riscv

The ESP32-P4 backend, written against the same named-instruction interface as the other two.

Fixed four-byte instructions and a standard call ABI make it the simplest of the three. Every register maps into the caller-saved set, so a call saves the live pool explicitly, and branch displacements are back-patched against bound labels.

Enumerations

Name Description
[Reg] Fourteen registers, which is what the caller-saved set alone provides.
[Cond] A branch condition, holding the ones the IR needs.

Reg

enum Reg

Fourteen registers, which is what the caller-saved set alone provides.

Value Description
R0
R1
R2
R3
R4
R5
R6
R7
R8
R9
R10
R11
R12
R13
kRegCount
R0
R1
R2
R3
R4
R5
R6
R7
R8
R9
R10
R11
R12
R13
kRegCount
R0
R1
R2
R3
R4
R5
R6
R7
R8
R9
kRegCount

Cond

enum Cond

A branch condition, holding the ones the IR needs.

Value Description
Lo
Hs
Ne
Ge
Lo
Hs
Lo
Hs

RiscvAssembler

class RiscvAssembler
src/platform/esp32/moonlive_asm_riscv.h:43

Public Methods

inline ~RiscvAssembler() : Frees the buffer only when this emitter owns it; a copy would double-free, so copying is deleted.

inline explicit RiscvAssembler(size_t cap = kCodeCap) : Allocate a cap-byte code buffer, sized per script because backends differ by up to 1.9x.

inline RiscvAssembler(uint8_t * out, size_t cap) : Emit straight into the caller's staging buffer, which halves a compile's transient heap.

RiscvAssembler(const RiscvAssembler &) = delete : Not copied: a copied owner would free the same buffer twice.

RiscvAssembler & operator=(const RiscvAssembler &) = delete : Not copy-assigned, for the same reason.

inline void finalize() : Resolve every fixup against its bound label; call it once, after the last instruction.

inline void alignForEntry() : Pad to the alignment a function entry needs, which on this architecture is nothing.

inline const uint8_t * bytes() const : The finished bytes, valid only after finalize.

inline size_t size() const : How many bytes were emitted.

inline bool overflowed() const : Whether any write was dropped for want of room.

void prologue(uint8_t slots = 0) : Open a frame for slots spilled values; no slots emits nothing, so a plain script pays none.

void spillStore(Reg r, uint8_t slot) : Write a register into a spill slot.

void spillLoad(Reg r, uint8_t slot) : Read a spill slot back into a register.

void slotAddr(Reg d, uint8_t slot) : Address one slot, which is how a call builds its argument block.

Label newLabel() : A fresh label, to be bound once and branched to any number of times.

void bind(Label l) : Fix this label's position at the current offset.

void movPtr(Reg d, const void * p) : A full-width address into a register (ConstPtr).

void movImm(Reg d, int32_t imm) : An immediate of any width into a register, through the small-immediate add where it fits.

void movReg(Reg d, Reg a) : Mv rd, ra (addi rd, ra, 0).

void addImm(Reg d, Reg a, int32_t imm) : Addi rd, ra, imm.

void addReg(Reg d, Reg a, Reg b) : add rd, ra, rb.

void mulReg(Reg d, Reg a, Reg b) : Mul rd, ra, rb.

void mulhi(Reg d, Reg a, Reg b) : Mulh rd, ra, rb: the SIGNED high 32 bits.

void shlImm(Reg d, Reg a, uint8_t n) : slli rd, ra, #n.

void sarImm(Reg d, Reg a, uint8_t n) : srai rd, ra, #n: arithmetic, sign-filling.

void shrImm(Reg d, Reg a, uint8_t n) : srli rd, ra, #n: logical, zero-filling.

void store8(Reg base, Reg off, Reg val) : add tmp,base,off ; sb val,0(tmp).

void load8(Reg d, Reg base, int32_t imm) : Lbu rDst, imm(rBase): a control read.

void load32(Reg d, Reg base, int32_t imm) : Lw rDst, imm(rBase): a whole 4-byte slot.

void store32(Reg base, int32_t imm, Reg val) : sw rVal, imm(rBase) (offset IMMEDIATE).

void load32Idx(Reg d, Reg base, Reg off) : add tmp,base,off ; lw d,0(tmp).

void store32Idx(Reg base, Reg off, Reg val) : add tmp,base,off ; sw val,0(tmp).

void load8Idx(Reg d, Reg base, Reg off) : add tmp,base,off ; lbu d,0(tmp).

void branchIfZero(Reg a, Label l) : Beqz a, l (bge x0, a... use bgeu against x0).

void branchGeU(Reg a, Reg b, Label l) : Bgeu a, b, l.

void branchGeS(Reg a, Reg b, Label l) : Bge a, b, l.

void branchNe(Reg a, Reg b, Label l) : Bne a, b, l.

void call(Reg d, Reg a, Reg b, Reg c, const void * fn) : Standard call to a host built-in.

void callLabel(Label l, Reg d = R0, bool take = false) : Call a function in this block by label: the script-to-script call.

void epilogue() : Undo prologue's frame (if any), then ret.

void retValue(Reg a) : Park a where the ABI returns a value, before the epilogue tears the frame down.

void ret() : Return to the caller.

Public Static Attributes

constexpr uint8_t kMaxSpillSlots = kTotalSlots : The allocator's slot range plus the parked host arguments.

Public Types

using RegType = Reg : The register type the shared lowering works in, each backend's Reg being its own enum.

More info

Fourteen registers

That is what the caller-saved set alone provides, and a nested loop needs more than the twelve every backend started with. It briefly reached eighteen by mapping callee-saved registers too, on the reasoning that the routine saves what it uses. It does not, having no prologue, so it would have returned with four of them clobbered.

Slots address from the frame pointer

A call moves the stack pointer underneath them, and reading a spilled value after a built-in call is the ordinary case.

Why the buffer is heap

The assembler is a stack local, so a cap-sized member put 2 KB on the compile chain's stack and overflowed the task on a classic ESP32.