Skip to content

RISC-V code generator (assembly-text emitter, not JIT machine-code path) — worth pursuing this direction?#169

Open
steven-studio wants to merge 15 commits into
dstogov:masterfrom
steven-studio:riscv-backend-poc
Open

RISC-V code generator (assembly-text emitter, not JIT machine-code path) — worth pursuing this direction?#169
steven-studio wants to merge 15 commits into
dstogov:masterfrom
steven-studio:riscv-backend-poc

Conversation

@steven-studio

Copy link
Copy Markdown

Hi Dmitry,
Following up on the RISC-V thread — I put together a working RISC-V backend, but want to flag upfront that it's architecturally different from the x86/aarch64 backends: it emits RISC-V assembly text (via fprintf, in the same spirit as ir_emit_c.c/ir_emit_llvm.c) rather than machine code through ir_match()/ir_ra.c into an in-memory code buffer. It needs an external assembler afterward, so it isn't a drop-in for the PHP-JIT in-memory codegen path — more suited to the standalone ir tool / AOT use cases.
I did this partly to sidestep the missing RISC-V DynAsm backend you mentioned, without waiting on the DynAsm replacement work.
Known limitations I haven't addressed yet: the loop-carried PHI register pool is capped at 6 int + 6 FP registers (breaks down for functions with more concurrently-live loop variables), and basic-block/if-else pairing is done by scanning forward through the instruction stream rather than using the CFG the framework already builds.
Before I put more time into this, wanted your take: is a text-emission backend like this worth having in the tree at all (given the emit_c/emit_llvm precedent), or would you rather RISC-V support eventually be a "real" backend integrated into ir_match/ir_ra.c once the DynAsm situation is sorted? Happy to go either direction.

@dstogov

dstogov commented Jul 14, 2026

Copy link
Copy Markdown
Owner

interesting step, but it's not very suitable.

it emits RISC-V assembly text (via fprintf, in the same spirit as ir_emit_c.c/ir_emit_llvm.c) rather than machine code through ir_match()/ir_ra.c into an in-memory code buffer. It needs an external assembler afterward, so it isn't a drop-in for the PHP-JIT in-memory codegen path — more suited to the standalone ir tool / AOT use cases.

Generating assembler text doesn't make a lot of sense for the JIT related project.
I suppose, binary encoding for RISC processor shouldn't be very difficult, but this is only the first step.
The back-end should be verified with RCC and thousand tests from GCC test suite...

or would you rather RISC-V support eventually be a "real" backend integrated into ir_match/ir_ra.c once the DynAsm situation is sorted?

Of course, I would prefer the real back-end. And as I told, I think about getting rid of DynAsm.
This is also a big job that requires careful design.

@steven-studio

Copy link
Copy Markdown
Author

That makes sense — agreed the assembly-text route isn't the right fit for a JIT framework, and thanks for confirming the direction.
I'd like to start exploring a direct binary-encoding RISC-V backend that hooks into ir_match()/ir_ra.c properly (skipping DynAsm entirely, encoding instructions by hand), rather than going through the text-emission path. My expectation is this will take a while before it's anywhere close to being validated against RCC or the GCC test suite — the near-term goal is just a working skeleton that can select instructions and allocate registers through the existing pipeline and produce runnable machine code for the same set of cases I already tested (arithmetic, comparisons, control flow, float conversions).
A couple of questions before I sink time into this, in case it saves both of us some back-and-forth later:

Is there anything about the ir_match_insn/rules interface (currently living inside the .dasc files) that you'd want done differently for a hand-written encoder, given you're also rethinking the DynAsm situation? I don't want to build against an interface you're about to change.
Any preference on how far along you'd want to see this before another PR/discussion — a minimal single-op proof (e.g. just ADD/RETURN working end-to-end through ir_match/ir_ra/ir_emit_code) vs. waiting until it covers more ground?

No urgency on my end — happy to just work on this quietly and check back in once there's something concrete to look at, if that's easier for you.

@dstogov

dstogov commented Jul 14, 2026

Copy link
Copy Markdown
Owner

A couple of questions before I sink time into this, in case it saves both of us some back-and-forth later:

Is there anything about the ir_match_insn/rules interface (currently living inside the .dasc files) that you'd want done differently for a hand-written encoder, given you're also rethinking the DynAsm situation? I don't want to build against an interface you're about to change.

You may try to be close to the X86/ARM back-end design.

  • ir_match_insn() selects code-generation rules (it should select "locally optimal" solution)
  • ir_match_insn2() allows second chance optimization
  • ir_get_target_constraints() provides information about rules to register allocator
  • ir_emit_code() generates executable code in a memory buffer

Ideally this should be rewritten using declarative DSL. The more compact and clean the better. Something like the following...

MATCH ADD(I*|U* op1,  I*|U* LOAD(ADDR mem)) {
  HINTS:
    def_reg = op1_reg
  ASM:
     if (def_reg != op1_reg) {
|      mov %def_reg, %op1_reg
     }
|    add %def_reg, [mem]
END

MATCH ADD(I*|U* op1,  I*|U* CONST op2) {
  CONDITIONS:
    IS_SIGNED_32BIT(op2) 
  HINTS:
    def_reg = op1_reg
  ASM:
     if (def_reg != op1_reg) {
|      mov %def_reg, %op1_reg
     }
|    add %def_reg, $op2
END

MATCH ADD(I*|U* op1,  I*,|U* op2) {
  HINTS:
    def_reg = op1_reg
  CONSTRINTS:
    def_reg != op2_reg
  ASM:
    if (def_reg != op1_reg) {
|     mov %def_reg, %op1_reg
    }
|  add %def_reg, %op2_reg
END

I don't have the final picture yet. Some ideas might be found in BEG, TWIG, iburg, lburg, ...

Any preference on how far along you'd want to see this before another PR/discussion — a minimal single-op proof (e.g. just ADD/RETURN working end-to-end through ir_match/ir_ra/ir_emit_code) vs. waiting until it covers more ground?

It should pass simple IR tests in "tests/run" (not necessary all). You may add tests to cover simple instruction (ADD, SUB, MUL, ...). [Now I'm creating similar tests for SIMD using RCC]

No urgency on my end — happy to just work on this quietly and check back in once there's something concrete to look at, if that's easier for you.

Yeah, sorry. I'm deep inside x86 SIMD back-end development... crying because of SSE*/AVX* design :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants