Skip to content

[wasm] Implement WasmBase LeadingZeroCount/TrailingZeroCount JIT intrinsics#130938

Merged
lewing merged 2 commits into
dotnet:mainfrom
lewing:lewing-wasm-wasmbase-clz-ctz-intrinsics
Jul 18, 2026
Merged

[wasm] Implement WasmBase LeadingZeroCount/TrailingZeroCount JIT intrinsics#130938
lewing merged 2 commits into
dotnet:mainfrom
lewing:lewing-wasm-wasmbase-clz-ctz-intrinsics

Conversation

@lewing

@lewing lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

The internal System.Runtime.Intrinsics.Wasm.WasmBase class (already on main) declares LeadingZeroCount/TrailingZeroCount (int/uint/long/ulong) as self-recursive intrinsic stubs, but the RyuJIT-wasm backend left them unimplemented — the WasmBase IsaRange was { NI_Illegal, NI_Illegal }, so these fell back to the managed software implementation.

This wires the two intrinsics into the RyuJIT-wasm backend so they lower to native wasm clz/ctz:

  • hwintrinsic.cpp — give WasmBase a real IsaRange { FIRST_NI_WasmBase, LAST_NI_WasmBase }.
  • hwintrinsiclistwasm.h — add the two scalar HARDWARE_INTRINSIC entries (HW_Category_Scalar, HW_Flag_SpecialImport|HW_Flag_NoFloatingPointUsed), matching the established xarch scalar LeadingZeroCount/TrailingZeroCount pattern (simdSize 0).
  • hwintrinsicwasm.cpp — special-import each to a GT_INTRINSIC over the existing NI_PRIMITIVE_LeadingZeroCount/NI_PRIMITIVE_TrailingZeroCount, which wasm codegen already lowers to i32.clz/i64.clz and i32.ctz/i64.ctz (see codegenwasm.cpp).

Consumer: System.Numerics.BitOperations (LeadingZeroCount/Log2/TrailingZeroCount) routes through WasmBase.LeadingZeroCount(value) when WasmBase.IsSupported. Implementing these makes core bit operations use native wasm clz/ctz on RyuJIT-wasm instead of the software fallback.

API review

No new or changed public API. WasmBase is internal and already shipped on main; this PR only implements its JIT lowering. No API review required.

Validation

  • JIT compiles clean. The wasm cross-JIT (clrjit_universal_wasm_arm64) builds with 0 warnings / 0 errors after a full recompile of the changed sources (clr.jit -c Release). Baseline clr+libs -rc Release and clr -os browser -c Release also build clean.
  • Lowering target already exists. NI_PRIMITIVE_*ZeroCount GT_INTRINSIC nodes for TYP_INT/TYP_LONG are already handled by codegenwasm.cpp (emits i32/i64.clz/ctz), so the intrinsics reuse a proven codegen path.
  • ⚠️ Runtime execution not verified locally. Attempting to run under the experimental wasm-CoreCLR corerun.js fails at coreclr_initialize (0x80070057) even for the pre-built tieringtest.dll smoke test — a pre-existing bring-up limitation of the experimental runtime in this environment (note the System.Private.CoreLib.NotReadyYet.wasm artifact), unrelated to this change and occurring before any JIT/intrinsic code runs. Existing System.Numerics.BitOperations tests already cover LeadingZeroCount/TrailingZeroCount for all overloads and will exercise this path once the wasm runtime can execute.

Opening as draft because end-to-end runtime execution could not be verified locally (blocked by the experimental runtime, not by this change).

Note

This pull request was authored with the assistance of GitHub Copilot.

…insics

The internal `System.Runtime.Intrinsics.Wasm.WasmBase` class (already on main)
declares LeadingZeroCount/TrailingZeroCount (int/uint/long/ulong) as self-recursive
intrinsic stubs, but RyuJIT-wasm left them unimplemented (NI_Illegal in the WasmBase
IsaRange), so they fell back to the managed software implementation.

Wire the two intrinsics into the RyuJIT-wasm backend:

- hwintrinsic.cpp: give WasmBase a real IsaRange { FIRST_NI_WasmBase, LAST_NI_WasmBase }.
- hwintrinsiclistwasm.h: add the two scalar HARDWARE_INTRINSIC entries
  (HW_Category_Scalar, HW_Flag_SpecialImport|HW_Flag_NoFloatingPointUsed), matching the
  established xarch scalar LeadingZeroCount/TrailingZeroCount pattern.
- hwintrinsicwasm.cpp: special-import each to a GT_INTRINSIC over the existing
  NI_PRIMITIVE_LeadingZeroCount/TrailingZeroCount, which wasm codegen already lowers to
  i32/i64.clz and i32/i64.ctz.

System.Numerics.BitOperations (LeadingZeroCount/Log2/TrailingZeroCount) routes through
WasmBase.IsSupported, so this makes core bit operations use native wasm clz/ctz on
RyuJIT-wasm instead of the software fallback.

No public API change: WasmBase is internal and already shipped on main, so no API review
is required.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eddc353d-673e-4210-9bb6-6d0b5f6f9245
Copilot AI review requested due to automatic review settings July 17, 2026 01:02
@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 Jul 17, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
10 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 subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@lewing

lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Validated end-to-end under wasm ReadyToRun in the R2R-on-wasm prototype (which has the runtime execution path not yet on main). BitOperations routes LZC/Log2/TZC through WasmBase; R2R matches the interpreter:

LZ32(0x00F00000)=8  TZ32=20  LZ64=40  TZ64=20  Log2=23

Both cases lower into the existing clz/ctz codegen and run correctly (32- and 64-bit). Your local coreclr_initialize failure is the wasm runtime bring-up not being on main yet — orthogonal to this JIT change.

Note

AI-assisted comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements missing RyuJIT-wasm support for the System.Runtime.Intrinsics.Wasm.WasmBase scalar LeadingZeroCount / TrailingZeroCount intrinsics so they import as existing NI_PRIMITIVE_*ZeroCount GT_INTRINSIC nodes (which wasm codegen already lowers to i32/i64.clz and i32/i64.ctz).

Changes:

  • Enables the WasmBase instruction set by giving it a real IsaRange in the HW intrinsic ISA range table.
  • Adds WasmBase LeadingZeroCount / TrailingZeroCount entries to the wasm HW intrinsic list with HW_Flag_SpecialImport.
  • Special-imports the two intrinsics in hwintrinsicwasm.cpp by producing GT_INTRINSIC nodes over NI_PRIMITIVE_LeadingZeroCount / NI_PRIMITIVE_TrailingZeroCount.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/coreclr/jit/hwintrinsicwasm.cpp Special-import WasmBase LZC/TZC to existing NI_PRIMITIVE_*ZeroCount GT_INTRINSIC nodes.
src/coreclr/jit/hwintrinsiclistwasm.h Adds the WasmBase scalar intrinsic table entries and defines FIRST_NI_WasmBase / LAST_NI_WasmBase.
src/coreclr/jit/hwintrinsic.cpp Updates the wasm ISA range table so WasmBase maps to the new named-intrinsic range.

@lewing

lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

cc @dotnet/wasm-contrib

@adamperlin

Copy link
Copy Markdown
Contributor

Since these are technically in the hardware intrinsics table, I think we should construct the nodes with gtNewScalarIntrinsic() if possible and move the ctz, clz handling to genHWIntrinsic under a !isSimd or isScalar branch.

@lewing

lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Thanks @adamperlin. I looked at routing these through gtNewScalarHWIntrinsicNode + a scalar branch in genHWIntrinsic, but there's a wrinkle that makes special-importing to NI_PRIMITIVE_*ZeroCount the better fit here: every WasmBase overload returns int, so LeadingZeroCount(long)/(ulong) are mixed-type (i64 operand → i32 result). The existing NI_PRIMITIVE codegen in codegenwasm.cpp already picks the instruction by operand type (i64.clz vs i32.clz) and truncates i64→i32. A table-driven scalar HW node keys instruction selection off the node's return type (always TYP_INT here), so it'd emit i32.clz on an i64 operand unless I re-implement the operand-type selection + truncation in genHWIntrinsic — duplicating that codegen. This also matches how mainline importercalls.cpp already lowers these on wasm (identical hand-rolled GenTreeIntrinsic(retType, op1, NI_PRIMITIVE_LeadingZeroCount, …)). Happy to move to the HW-node form as a follow-up if you'd prefer it despite the truncation caveat — let me know.

Note

This comment was authored with the assistance of GitHub Copilot.

@lewing
lewing marked this pull request as ready for review July 17, 2026 17:55
@azure-pipelines

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

Comment thread src/coreclr/jit/hwintrinsiclistwasm.h Outdated
Address review feedback (@tannergooding): these two intrinsics are always
special-imported and rewritten into NI_PRIMITIVE_*ZeroCount GT_INTRINSIC nodes,
so a GenTreeHWIntrinsic with the NI_WasmBase_* id is never created. HW_Flag_InvalidNodeId
expresses that precisely -- HasSpecialImport() already covers it (so it still routes
through impSpecialIntrinsic), and it additionally asserts the id never materializes as
a HW intrinsic node -- matching how the Vector helper intrinsics are flagged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eddc353d-673e-4210-9bb6-6d0b5f6f9245
Copilot AI review requested due to automatic review settings July 17, 2026 18:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/jit/hwintrinsiclistwasm.h
@tannergooding
tannergooding enabled auto-merge (squash) July 17, 2026 19:05
@JulieLeeMSFT

Copy link
Copy Markdown
Member

Rerunning failed tests.

@lewing

lewing commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

/ba-g checked build timeouts are not related

@lewing
lewing merged commit 46ec2c1 into dotnet:main Jul 18, 2026
136 of 140 checks passed
@lewing
lewing deleted the lewing-wasm-wasmbase-clz-ctz-intrinsics branch July 18, 2026 15:38
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants