Skip to content

[RISC-V] NativeAOT soft-float target (riscv64-lp64) and builds without the A extension - #134456

Open
maximmenshikov wants to merge 16 commits into
dotnet:mainfrom
maximmenshikov:maximmenshikov/riscv64_zk_base
Open

maximmenshikov wants to merge 16 commits into
dotnet:mainfrom
maximmenshikov:maximmenshikov/riscv64_zk_base

Conversation

@maximmenshikov

Copy link
Copy Markdown
Contributor

Please don't review this yet. I opened it to run CI and to show the full series. The design will be discussed first in a separate design issue, which I'm about to file; I'll link it here. I'll keep this PR as a draft until the direction is agreed there, and then split it into smaller PRs.

Summary:

  • F/D/C/A modelled as riscv64 instruction sets; FP and compressed emission gated on them.
  • A new riscv64-lp64 NativeAOT target: lp64 calling convention, FP arithmetic via compiler-rt helpers, correct ELF e_flags. crossgen2 rejects the target.
  • Interlocked/CmpXchg lowering without the A extension, gated behind ilc's --single-hart.
  • Nothing changes for the default rv64gc/lp64d target.

Context: #132204.

@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Sep 22, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 7 pipeline(s).
9 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging @dotnet/jit-contrib for JIT-EE GUID update

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 22, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

maximmenshikov and others added 16 commits September 25, 2026 12:25
riscv64 already models its optional extensions - Zba, Zbb, Zbs and
Zicond - as InstructionSets, but the base rv64gc extensions are assumed
unconditionally, so there is no way to describe a target that does not
have them and no way for the JIT to ask.

Add F, D, C and A to InstructionSetDesc.txt with D implying F, seed the
rv64gc baseline from them in the VM and in the AOT drivers, and give
them the usual DOTNET_EnableRiscV64* opt-outs, the same shape the other
architectures use for their optional sets. The public names are the
psABI extension letters, so a reduced target is spelled the way the C
toolchain spells it: --instruction-set=-a,-c,-d,-f.

The R2R names are spelled RiscV64F/D/C/A. The JIT and managed enums
scope their members (InstructionSet.RiscV64_F), but the R2R one does
not, and it is the only one of the three that is a format contract and
cannot be renamed afterwards, so a bare READYTORUN_INSTRUCTION_A would
be a poor permanent name.

Defaults are unchanged: every riscv64 target still gets F, D, C and A
unless something explicitly removes them, so this is a no-op for the
supported rv64gc baseline.

The JIT/EE interface GUID is updated, as instruction set definitions are
part of that contract.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
With F, D, C and A described as instruction sets, let the riscv64 backend
ask before emitting an encoding that needs one:

* tryEmitCompressedIns_R_R_R - the only place the JIT emits a compressed
  encoding - returns false unless C is available.
* A Checked JIT asserts in emitOutput_Instr that no F/D-extension major
  opcode is emitted unless F is available, so an unexpanded floating
  point node fails on the method that produced it rather than at run
  time on the target.

The assert needs compIsaSupportedDebugOnly, which was a no-op returning
false outside xarch and arm64; enable the real check for riscv64 too.

Both are no-ops on the rv64gc baseline, where F, D, C and A are always
present.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The native runtime is compiled -march=rv64gc -mabi=lp64d unconditionally, so a
riscv64 target whose sysroot is built for another ABI cannot be built at all:
the objects claim lp64d, the sysroot CRT claims something else, and lld refuses
to reconcile the two. The only way out today is to append overriding flags after
these, which works by accident of ordering and hides what the build targets.

Turn the two into cache variables with the current values as defaults, so an
unconfigured build is byte-identical, and a target that needs a different ABI
states it once:

    cmake -DCLR_CMAKE_RISCV64_MABI=lp64 -DCLR_CMAKE_RISCV64_MARCH=rv64im ...

The float-ABI field of e_flags follows -mabi rather than -march, so setting the
ABI here is what makes the whole native build agree with its sysroot. The flags
are passed at link time as well, so the driver selects the matching CRT and
builtins.

This mirrors how armel selects -mfloat-abi=softfp a few lines above.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
configurecompiler.cmake settles the RISC-V ISA and ABI for the project, but
cmake compiles and links its own probes at project() time, before any of the
project's options exist. On a sysroot built for an ABI other than the compiler
default those probes fail to link - lld does not reconcile float ABIs - and
cmake reports the compiler as broken before the build starts.

Apply the same two variables from the cross toolchain file, which is in effect
for the probes as well, and list them in CMAKE_TRY_COMPILE_PLATFORM_VARIABLES so
they survive nested try_compile passes. Both are unset by default, so the
toolchain's own defaults stay in place and nothing changes for existing targets.

This follows what the arm/armel branch above already does for -mfpu and
-mfloat-abi.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The riscv64 port refuses to compile when __riscv_flen is not 32 or 64, so a
target built for the base integer ISA cannot build the PAL's unwinder at all:
libunwind-riscv.h has no unw_tdep_fpreg_t, asm.h has no load/store width, and
dwarf_getfp/dwarf_putfp hit an #error because __riscv_xlen != __riscv_flen.

There is nothing to unwind in that configuration - the target has no
floating-point registers - so the three cases are answered rather than made to
work:

  * unw_tdep_fpreg_t is part of the public API, so it stays, as an integer of
    the width the double-precision ABI uses.
  * asm.h leaves SZFREG/STORE_FP/LOAD_FP undefined; getcontext.S and
    setcontext.S already guard every use with #ifdef, so they save and restore
    the integer registers alone.
  * dwarf_getfp and dwarf_putfp return -UNW_EBADREG: with no floating-point
    registers, no floating-point location is valid.

Targets that have F or D are unaffected - the existing branches are untouched
and still the only ones taken.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The hand-written riscv64 assembly uses fld/fsd and the A-extension amo*
unconditionally, so a runtime built for a target without F/D or A does not
assemble at all - the failure is in the assembler, before any of the ISA
modelling in the compiler can help.

Guard those sequences on the __riscv_flen and __riscv_atomic predefined macros,
which are exactly what -march sets, and provide the lr/sc-free fallback for the
atomics. A build that has the extensions is unchanged: the guards are true and
the same instructions are emitted.

The .S sources are the one place where the instruction set cannot be a compiler
concept - the assembler is the consumer - so they switch on the predefined
macros rather than on the InstructionSet model used elsewhere.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
cpufeatures.c asserts the full rv64gc baseline at startup, so a runtime built
for a narrower ISA fails the assert on hardware that matches what it was built
for. Assert what the build actually requires instead.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Under CORJIT_FLAG_SOFTFP_ABI - the flag armel already uses - riscv64 passes and
returns floating-point scalars and floating-point struct fields by the integer
calling convention, which is what the lp64 psABI prescribes for a target
without F/D. Only the ABI classifier changes here; code generation still uses
the FP registers, so this commit alone is not a usable target. It is separated
because it is the part that is purely a calling convention.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Fourteen helpers - {FLT,DBL}{ADD,SUB,MUL,DIV}, {FLT,DBL}CMP_{LE,GE}, FLT2DBL and
DBL2FLT - bound by ilc to the toolchain's compiler-rt builtins (__adddf3,
__ledf2, ...), the same way CORINFO_HELP_DBLREM is already bound to fmod. No
floating-point arithmetic is implemented in the runtime or the libraries.

The target is selected explicitly as TargetAbi.NativeAotRiscV64SoftFloat,
spelled riscv64-lp64 on the ilc command line, following the armel precedent. It
drives the JIT flag, the RISC-V ELF float-ABI field in e_flags (which is what
lets the linker reject a mix of lp64 and lp64d objects), the instruction-set
defaults and a two-way validation: lp64d requires F and D, lp64 must not have
them. crossgen2 rejects the target - the helpers have no ReadyToRun encoding.

Value numbering models the helpers as the operations they implement, as
CORINFO_HELP_LMUL is modelled on 32-bit targets; only the two three-way
compares need VNFuncs of their own.

Note: the RiscV64ObjectWriterTests addition was dropped - ILCompiler.Compiler.Tests
was deleted upstream in dotnet#133474 and needs a new home.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The IR keeps TYP_FLOAT and TYP_DOUBLE; what changes under the soft-float ABI is
their register class, which becomes VTR_INT. Most of the backend already decides
by register class, so FP values are then allocated, spilled, moved, passed and
returned in integer registers with no further work; the places that decide by
varTypeIsFloating instead are adjusted.

The operations are expanded in global morph through the existing hooks:
arithmetic and conversions into helper calls, comparisons into a compare helper
plus an integer relop, negation into a sign-bit XOR, and CKFINITE into a bounds
check on the exponent field. FP Math intrinsics are declined by type and remain
calls to the managed implementations. The debug JIT asserts on any F/D opcode
reaching the emitter when F is not in the instruction set, so an unexpanded node
fails on the method that produced it.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Bit-exact expectations for arithmetic rounding, NaN comparison semantics
including the unordered branch forms, saturating and checked conversions for
every integer width, conversions from every integer width, float <-> double,
negation, remainder, the Math intrinsics that become calls, CSE of repeated
helper calls, values across call boundaries including stack-passed arguments,
and float-bits-as-int sign extension.

Every expected value is the exact IEEE 754 result, so the test is valid on every
target and passes on hard-float today; on a soft-float image the same assertions
exercise the compiler-rt helpers.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The ILCompiler is published as a NativeAOT application for the target, and it
gets --targetarch from _targetArchitectureWithAbi. On a riscv64 target that
value is plain "riscv64", so the object writer stamps
EF_RISCV_FLOAT_ABI_DOUBLE; against a soft-float sysroot the link then fails:

    ld.lld: error: ilc.o: cannot link object files with different
    floating-point ABI from Scrt1.o

Add the riscv64 case next to the armel one that is already there. It is
conditional on a property rather than derived from the RID, because
linux-musl-riscv64 covers both lp64d and lp64 userspaces - unlike linux-bionic
on ARM, where armel follows from the RID alone.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
…or riscv64-lp64

The aggregate-executable shim is compiled from C and linked by clang straight from
MSBuild; nothing there knows the target ABI, so on a soft-float sysroot the shim
object comes out lp64d and lld refuses to link it against Scrt1.o:

    ld.lld: error: /tmp/dotnet-dev-certs-b44bf8.o: cannot link object files
    with different floating-point ABI from /crossrootfs/riscv64/usr/lib/Scrt1.o

Derive the flag from _targetArchitectureWithAbi, the same property that already
selects --targetarch for ilc, and mark it Shim="true" so both the shim and the
main link get it.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
genLockedInstructions and genCodeForCmpXchg emit amo* and lr/sc
unconditionally. A build that selects an ISA without the A extension gets
instructions the target cannot execute, on any Interlocked call.

Gate them on InstructionSet_A. Without it, lower to a plain read/modify/write
and, for CmpXchg, to a compare and store with no reservation pair. This is
the only lowering the ISA allows, and it is correct on a target with a single
hart and no preemption.

BuildNode in lsrariscv64 extends the address and data lifetimes for the plain
sequence, which reuses both registers after the first instruction, and gives
the arithmetic and bitwise forms one scratch register for the new value.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
…/EE GUID

The A-extension test was a substring match for "a" against the whole -march
string, which is wrong in both directions. rv64gc has the extension, through
the "g" shorthand for imafd, but contains no letter "a". rv64im_zba does not
have it, but contains "a" in the name of Zba. So the default baseline linked
libatomic it does not need, and a build that really lacks A would fail on the
atomic-alignment warning under -Werror.

Match on the single-letter part of the string only, after stripping the rv<XLEN>
prefix and any multi-letter extensions, and treat "g" as implying A.

Also generate a new JITEEVersionIdentifier: the new CorInfoHelpFunc entries
shift the values of every helper after them, so a JIT and an EE from different
sides of this change must not be considered compatible.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Without A there is no atomic memory operation to emit, so Interlocked and
CmpXchg lower to a plain read/modify/write. Whether that is sufficient depends
on the target running on one hart and never being preempted. Neither the ISA
nor the ABI says anything about that: a soft-float lp64 image is perfectly
normal on rv64imac hardware with threads, and would lose atomicity silently.

So it has to be asserted rather than inferred. Add --single-hart to ilc and
reject an ISA without A unless it is passed. The flag names the property it
claims, and the failure is at compile time rather than at run time.

Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
@maximmenshikov
maximmenshikov force-pushed the maximmenshikov/riscv64_zk_base branch from 384a139 to b78b9cd Compare September 25, 2026 11:26
@maximmenshikov
maximmenshikov marked this pull request as ready for review September 25, 2026 11:57
@am11

am11 commented Sep 25, 2026

Copy link
Copy Markdown
Member

Since this is out of draft: the goal reads clearly (a soft-float lp64 NativeAOT target plus a reduced ISA for RV64 without F/D/A/C, with rv64gc unchanged).

One naming concern: --single-hart understates the contract. The non-atomic Interlocked/CmpXchg lowering needs one hart and no preemption, as the option's own help text says. A preemptive single-core RTOS satisfies the name but breaks correctness. Suggest --assume-no-concurrency, or putting both conditions in the name. Also, EnableRiscV64Atomic=0 in the JIT config reaches the same lowering without that gate; is that intended?

This branch has not been deployed

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

Labels

arch-riscv Related to the RISC-V architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants