Skip to content

Conversation

@RossSmyth
Copy link
Contributor

This is mainly to test the waters to see if this is something that is even desired.

What this does

For a single instruction set, make the power of two sizes up to a register width emit single instructions, as one would expect, in asm. I chose thumb because I work on thumbv6m. But it is not possible to choose that specifically.

For other weird sizes that are basically meaningless in the context of MMIO it is the status quo because there's really nothing more sane that can be done.

I would appreciate if someone could review the asm because while I am an embedded dev, I don't write asm that much.

The murky past

What is volatile? Who knows! The C standard definitely says something. What is that something is not quite clear. It's all about the Vibes.

"If something isn't working quite right, chuck a volatile in there"

  • Ancient embedded wisdom

Well, what do compilers do? So users aren't unhappy they general do something that matches the vibes. But you can coax them to not in weird edge cases. One that was shown off recently on the Rust Community Discord is this
https://godbolt.org/z/WxP14vqPa

Well the vibes aren't great.

The controversial present

People like to debate about volatile all the time. What does it do, what do they want it to do, what does it mean for opsem? Well these are all great questions I will not answer.

Some of the largest users of volatile are embedded developers (disclosure: I am an embedded dev :ferrisclueless:). It should almost exclusively be used for MMIO if the devs are being good. In those cases it is generally desired that the assembly output is a single load.

Unfortunately libstd make the (imo) mistake that things larger than a register (someone will probably say "erm on X arch you can do MMIO for other things, but that's not the point) can be read with read_volatile. But for sizes we can just turn it into a quality of implementation issue. Emit a load. Emit a store.

The shiny future

Hopefully people can fill in their favorite arches so Rust's volatile loads and stores are the best they can be. I would also like it if volatile loads and stores were relaxed operations so that people's broken code is slightly less broken. But that's a future discussion.

r? wg-embedded-arm

@rustbot

This comment was marked as outdated.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Apr 3, 2025
@RossSmyth

This comment was marked as outdated.

@rustbot

This comment was marked as outdated.

@rust-log-analyzer

This comment has been minimized.

@KittyBorgX

This comment was marked as outdated.

@rustbot

This comment was marked as outdated.

@Noratrieb
Copy link
Member

Noratrieb commented Apr 3, 2025

@rust-lang/opsem This came out of a discussion about volatile not being usable for MMIO in some cases, where the (virtualized) implementation does not support MMIO through all instructions, but only some basic ones. What do you think about this, guaranteeing that volatile accesses turn into specific instructions for specific sized? (ignoring the details of the implementation)

@RalfJung
Copy link
Member

RalfJung commented Apr 3, 2025

Sorry, could someone explain the context for non-embedded people here?

Well, what do compilers do? So users aren't unhappy they general do something that matches the vibes. But you can coax them to not in weird edge cases. One that was shown off recently on the Rust Community Discord is this
https://godbolt.org/z/WxP14vqPa

This example looks like the output of pwgen to my eyes. What is it supposed to tell me? :)
(And why do so many people assume that everyone can read assembly and it's okay to show examples without any comments or explanation? ;)

I chose thumb because I work on thumbv6m. But it is not possible to choose that specifically.

Why is it not possible to choose...? You seem to be doing quite well choosing it for this PR. confused

@RalfJung
Copy link
Member

RalfJung commented Apr 3, 2025

FWIW there have been many hundreds of comments in various issues around "volatile" on the UCG and opsem issue tracker and Zulip. We'd be happy to work with someone who wants to put in the time of properly re-doing our entire volatile story, but unfortunately we don't have the resources to do it entirely by ourselves.

"It's basically inline assembly" is IMO the most promising approach, though it is unfortunate that apparently we cannot rely on LLVM doing that.

Copy link
Member

Choose a reason for hiding this comment

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

I don't understand the point of these macro contraptions and why the docs are now in a separate file. Can't we have a single function with a cfg_match! inside? Why does the function itself need to be emitted by a macro?

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably what resulted in this is that naive use of cfg_match!'s current implementation doesn't behave great in an expression position that wants to produce a value. That, or being first written with a cfg_if! that requires the arms to be made up of items.

You can write cfg_match! {{ … }} with two brackets to get it to consistently work as an expression block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just found this way more readable. And macros and doc comments don't interact well.

Copy link
Member

@RalfJung RalfJung Apr 3, 2025

Choose a reason for hiding this comment

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

It's quite hard to read to have the comment out-of-line for one of many functions. And the macro shouldn't have anything to do with the doc comment in the first place. The macro should generate the body of {read,write}_volatile, not the entire function.

This function isn't so special that it needs to be so different from everything else in core::ptr.

dest = in(reg) dst,
),
4 => asm!(
"str {val}, [{dest}]",
Copy link
Member

Choose a reason for hiding this comment

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

Is it meaningful that str etc are lower-case but LDR etc are upper-case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No

@Noratrieb
Copy link
Member

@RalfJung the way this came up in discussion again on the community discord is that someone was having problems with their non-embedded x86-64 OS in KVM (virtualization). KVM apparently only allows MMIO to happen with a specific subset of instructions, and LLVM sometimes codegened the volatile with other instructions (merging it with operations for example) such that while the write was still present, KVM no longer understood it.
What they'd have needed is for volatile to not just guarantee that the write isn't elided, and not just that it doesn't tear, but also that it compiles to a "normal, simple" (I'd guess a mov in this case for x86-64) instruction that KVM can understand.

@RalfJung
Copy link
Member

RalfJung commented Apr 3, 2025 via email

@saethlin
Copy link
Member

saethlin commented Apr 3, 2025

What do you think about this, guaranteeing that volatile accesses turn into specific instructions for specific sized?

I do not think we should guarantee anything. In the referenced discussion I encouraged a PR such as this (though I'll admit I didn't anticipate so much diff) as a pure quality-of-implementation concern. No changes to any documentation.

But in addition, since people like to read the standard library and take its internal comments as normative, we will need to be very clear in the implementation here to "document" with internal comments that any particular codegen is not guaranteed.

@RossSmyth
Copy link
Contributor Author

RossSmyth commented Apr 3, 2025

Why is it not possible to choose...? You seem to be doing quite well choosing it for this PR. confused

It uses cfg to select arm, thumb, 32-bit. Which is a wider range that thumv6m.

This example looks like the output of pwgen to my eyes. What is it supposed to tell me? :)

Many people expect volatile not to merge anything. This does.

The PR does not change anything for x86 though?

Yes, this is a first step to see if people even want to see a change like this. Rather than touching the precious x86, this just touches an embedded platform.

I think ideally this would be fixed in LLVM

Probably! But I will not be the one to do that.

From my POV LLVM is great at optimizing code, but it seems that all the different passes often forget things. So saying "well fix it in LLVM," means that LLVM should not forget things throughout its pipeline. Which would be great! But history has suggested that is a hard problem.

@RalfJung
Copy link
Member

RalfJung commented Apr 3, 2025

Many people expect volatile not to merge anything. This does.

What is being merged? With what? Where? Again, your example is entirely unreadable to me. So please explain whatever you think the example should demonstrate.

Probably! But I will not be the one to do that.

I don't think we should merge anything here without at least filing an issue on the LLVM side. And if LLVM considers this not-a-bug, we'll have to determine if we are ready to carry our own implementation of this ~forever.

Working around upstream behavior is a last resort, not the first thing we should try.

@RossSmyth
Copy link
Contributor Author

RossSmyth commented Apr 3, 2025

Once again, history suggests that LLVM is not that reliable at carrying information through its pipeline. So while they can probably fix this specific example, it is questionable at best that they can prove that for all operations they will never merge instructions.

If you take a look at this example here:
https://godbolt.org/z/se1x1PoWs

While I am not an expert on x86 I believe what is happening is the following:

  1. read, the baseline case
    [rdi] represents dereferencing the pointer in the rdi register
    So this copies the value at the location rdi points to into the eax register, then returns

  2. norm_read, the non-volatile case

    1. Copies the constant 260 to the eax register.
    2. Does a combined read at the location in memory rdi points to, shift, and and, stores the result in the eax register
    3. Return
      This is good, LLVM did the thing it is supposed to do. Optimize code :)
  3. vol_lsb, the volatile
    It is the same as above. Which for embedded devs, the vibes aren't great. Ideally it would have a dedicated copy out of the memory location first. Instead it does the same as above and has a merged load, store, and shift.

  4. vol_lsb, the Clang version
    This is identical to the rust version above vol_lsb and is just for reference.

  5. fix_lsb, the Clang version but what one would expect

    1. Copy the value at the location pointed to be rdi to eax
    2. Load a constant into the register ecx
    3. Do the shift and and with the bextr instruction
    4. Return

This is what the average embedded dev would expect from a volatile read. It just reads the location in memory, then does other stuff. No funny business. While it is now _Atomic, that isn't really the part people (=embedded devs) care about.

Another funny idea is to internally tell llvm it is an atomic relaxed volatile load :3c (which could also quell concerns about people trying to do atomic things with volatile, which is still very common because of cargo-culting no matter how much you tell people not to).

@RalfJung
Copy link
Member

RalfJung commented Apr 3, 2025

Another funny idea is to internally tell llvm it is an atomic relaxed volatile load

Yes, I want all our volatile accesses to compile to atomic accesses in LLVM. However, sadly we allow volatile accesses on all types, so we'll have to figure something out for too-large and misaligned accesses.

So if the issue you are having can be fixed by marking volatile accesses as atomic for types that are sufficiently small and where size==align, that would be a much better fix than the current proposal IMO.

The goal is to ensure LLVM doesn't do anything
silly (:3) with the loads.
@rust-log-analyzer

This comment has been minimized.

Copy link
Contributor

@CAD97 CAD97 left a comment

Choose a reason for hiding this comment

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

With my opsem hat, I'm with Ralf here: making Rust's "volatile" mean "volatile atomic" for "appropriately sized" accesses better matches what developers generally expect when they reach for volatile. But also that if LLVM isn't preserving the exact read size of "appropriately sized" volatile reads, that this is an LLVM bug and we should attempt to fix it in LLVM before working around it, or at least link to and track an LLVM issue tracking a fix so that we can remove the workaround once it isn't needed anymore.

Copy link
Contributor

Choose a reason for hiding this comment

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

This file is now redundant again and should be removed.

is_zst: bool = T::IS_ZST,
) => crate::ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
);
cfg_match! {
Copy link
Contributor

Choose a reason for hiding this comment

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

Of course ideally all of this magic would happen in the intrinsic / in LLVM. If we're going to implement volatile semantics in lib code for any appreciable amount of time, though, it should probably be done in sys PAL modules rather than inline due to the quantity of different platforms which will likely want to provide asm! volatile semantics.

Copy link
Member

Choose a reason for hiding this comment

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

sys is a std thing, we can't use it here. And anyway it's for abstracting OS/platform differences, not CPU architecture differences.

@saethlin
Copy link
Member

saethlin commented Apr 3, 2025

The only libs reviewer here is @Noratrieb. There is no libs reviewer assigned, and libs hasn't been contacted generally, only opsem. I very strongly prefer that questions about the feasibility of carrying a library implementation such as this (as well as guidance on how to implement this well) be answered by or with the input of libs maintainers.

@rust-lang/libs

@RossSmyth RossSmyth changed the title Implment (thumb) volatile with asm Implement (thumb) volatile with asm Apr 4, 2025
@RossSmyth
Copy link
Contributor Author

RossSmyth commented Apr 4, 2025

Ok here is an alternative with volatile atomics. I think this is probably better.

@rustbot
Copy link
Collaborator

rustbot commented Apr 4, 2025

Some changes occurred in compiler/rustc_codegen_ssa

cc @WaffleLapkin

Some changes occurred to the intrinsics. Make sure the CTFE / Miri interpreter
gets adapted for the changes, if necessary.

cc @rust-lang/miri, @RalfJung, @oli-obk, @lcnr

@rust-log-analyzer
Copy link
Collaborator

The job mingw-check-tidy failed! Check out the build log: (web) (plain)

Click to see the possible cause of the failure (guessed by this bot)
info: removing rustup binaries
info: rustup is uninstalled
##[group]Image checksum input
mingw-check-tidy
# We use the ghcr base image because ghcr doesn't have a rate limit
# and the mingw-check-tidy job doesn't cache docker images in CI.
FROM ghcr.io/rust-lang/ubuntu:22.04

ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
  g++ \
  make \
---

COPY host-x86_64/mingw-check/validate-toolstate.sh /scripts/
COPY host-x86_64/mingw-check/validate-error-codes.sh /scripts/

# NOTE: intentionally uses python2 for x.py so we can test it still works.
# validate-toolstate only runs in our CI, so it's ok for it to only support python3.
ENV SCRIPT TIDY_PRINT_DIFF=1 python2.7 ../x.py test \
           --stage 0 src/tools/tidy tidyselftest --extra-checks=py,cpp
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
#    pip-compile --allow-unsafe --generate-hashes reuse-requirements.in
---
#12 2.847 Building wheels for collected packages: reuse
#12 2.849   Building wheel for reuse (pyproject.toml): started
#12 3.072   Building wheel for reuse (pyproject.toml): finished with status 'done'
#12 3.073   Created wheel for reuse: filename=reuse-4.0.3-cp310-cp310-manylinux_2_35_x86_64.whl size=132719 sha256=5bb60f62728aaedff7162745ce743c7f2f55069b3e7f82e6a37d70df455797cc
#12 3.073   Stored in directory: /tmp/pip-ephem-wheel-cache-nbokbbr7/wheels/3d/8d/0a/e0fc6aba4494b28a967ab5eaf951c121d9c677958714e34532
#12 3.076 Successfully built reuse
#12 3.076 Installing collected packages: boolean-py, binaryornot, tomlkit, reuse, python-debian, markupsafe, license-expression, jinja2, chardet, attrs
#12 3.496 Successfully installed attrs-23.2.0 binaryornot-0.4.4 boolean-py-4.0 chardet-5.2.0 jinja2-3.1.4 license-expression-30.3.0 markupsafe-2.1.5 python-debian-0.1.49 reuse-4.0.3 tomlkit-0.13.0
#12 3.496 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
#12 4.059 Collecting virtualenv
#12 4.097   Downloading virtualenv-20.30.0-py3-none-any.whl (4.3 MB)
#12 4.190      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.3/4.3 MB 48.1 MB/s eta 0:00:00
#12 4.230 Collecting distlib<1,>=0.3.7
#12 4.234   Downloading distlib-0.3.9-py2.py3-none-any.whl (468 kB)
#12 4.241      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 469.0/469.0 KB 91.6 MB/s eta 0:00:00
#12 4.275 Collecting platformdirs<5,>=3.9.1
#12 4.279   Downloading platformdirs-4.3.7-py3-none-any.whl (18 kB)
#12 4.315 Collecting filelock<4,>=3.12.2
#12 4.320   Downloading filelock-3.18.0-py3-none-any.whl (16 kB)
#12 4.404 Installing collected packages: distlib, platformdirs, filelock, virtualenv
#12 4.606 Successfully installed distlib-0.3.9 filelock-3.18.0 platformdirs-4.3.7 virtualenv-20.30.0
#12 4.607 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
#12 DONE 4.7s

#13 [7/8] COPY host-x86_64/mingw-check/validate-toolstate.sh /scripts/
#13 DONE 0.0s
---
DirectMap4k:      118720 kB
DirectMap2M:     5124096 kB
DirectMap1G:    13631488 kB
##[endgroup]
Executing TIDY_PRINT_DIFF=1 python2.7 ../x.py test            --stage 0 src/tools/tidy tidyselftest --extra-checks=py,cpp
+ TIDY_PRINT_DIFF=1 python2.7 ../x.py test --stage 0 src/tools/tidy tidyselftest --extra-checks=py,cpp
##[group]Building bootstrap
    Finished `dev` profile [unoptimized] target(s) in 0.05s
##[endgroup]
WARN: currently no CI rustc builds have rustc debug assertions enabled. Please either set `rust.debug-assertions` to `false` if you want to use download CI rustc or set `rust.download-rustc` to `false`.
[TIMING] core::build_steps::tool::LibcxxVersionTool { target: x86_64-unknown-linux-gnu } -- 0.231
---
fmt check
fmt: checked 5942 files
tidy check
tidy: Skipping binary file check, read-only filesystem
##[error]tidy error: /checkout/library/core/src/ptr/volatile.rs:84: TODO is used for tasks that should be done before merging a PR; If you want to leave a message in the codebase use FIXME
##[error]tidy error: /checkout/library/core/src/ptr/volatile.rs:191: TODO is used for tasks that should be done before merging a PR; If you want to leave a message in the codebase use FIXME
removing old virtual environment
creating virtual environment at '/checkout/obj/build/venv' using 'python3.10' and 'venv'
creating virtual environment at '/checkout/obj/build/venv' using 'python3.10' and 'virtualenv'
Requirement already satisfied: pip in ./build/venv/lib/python3.10/site-packages (25.0.1)
linting python files
All checks passed!
checking python file formatting
26 files already formatted
checking C++ file formatting
/checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:607:68: error: code should be clang-formatted [-Wclang-format-violations]
                        const char *Name, LLVMAtomicOrdering Order, LLVMBool isVolatile) {
                                                                   ^
/checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:618:24: error: code should be clang-formatted [-Wclang-format-violations]
extern "C" LLVMValueRef LLVMRustBuildAtomicStore(LLVMBuilderRef B,
                       ^
/checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:618:67: error: code should be clang-formatted [-Wclang-format-violations]
extern "C" LLVMValueRef LLVMRustBuildAtomicStore(LLVMBuilderRef B,
                                                                  ^
/checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:619:65: error: code should be clang-formatted [-Wclang-format-violations]
                                                 LLVMValueRef V,
                                                                ^
/checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:620:70: error: code should be clang-formatted [-Wclang-format-violations]
                                                 LLVMValueRef Target,
                                                                     ^
/checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:621:75: error: code should be clang-formatted [-Wclang-format-violations]
                                                 LLVMAtomicOrdering Order,
                                                                          ^

clang-format linting failed! Printing diff suggestions:
--- /checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp (actual)
+++ /checkout/compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp (formatted)
@@ -603,9 +603,10 @@
 }
 
 extern "C" LLVMValueRef
 LLVMRustBuildAtomicLoad(LLVMBuilderRef B, LLVMTypeRef Ty, LLVMValueRef Source,
-                        const char *Name, LLVMAtomicOrdering Order, LLVMBool isVolatile) {
+                        const char *Name, LLVMAtomicOrdering Order,
+                        LLVMBool isVolatile) {
   Value *Ptr = unwrap(Source);
   LoadInst *LI = unwrap(B)->CreateLoad(unwrap(Ty), Ptr, Name);
   LI->setAtomic(fromRust(Order));
 
@@ -614,13 +615,11 @@
     LI->setVolatile(true);
   return wrap(LI);
 }
 
-extern "C" LLVMValueRef LLVMRustBuildAtomicStore(LLVMBuilderRef B,
-                                                 LLVMValueRef V,
-                                                 LLVMValueRef Target,
-                                                 LLVMAtomicOrdering Order,
-                                                 LLVMBool isVolatile) {
+extern "C" LLVMValueRef
+LLVMRustBuildAtomicStore(LLVMBuilderRef B, LLVMValueRef V, LLVMValueRef Target,
+                         LLVMAtomicOrdering Order, LLVMBool isVolatile) {
   StoreInst *SI = unwrap(B)->CreateStore(unwrap(V), unwrap(Target));
   SI->setAtomic(fromRust(Order));
 
   // atomic volatile

some tidy checks failed
tidy error: checks with external tool 'clang-format' failed
Command has failed. Rerun with -v to see more details.
Build completed unsuccessfully in 0:01:51
  local time: Fri Apr  4 21:37:05 UTC 2025
  network time: Fri, 04 Apr 2025 21:37:05 GMT
##[error]Process completed with exit code 1.
Post job cleanup.

@Amanieu
Copy link
Member

Amanieu commented Apr 4, 2025

I don't believe this change belong in the standard library. It would be very surprising for volatile in Rust to work differently than volatile in Clang, so if this change has merit then it should be implemented directly in LLVM so that other languages can benefit.

With that said I did review the Discord discussion and I don't think changing the volatile codegen is the correct solution here. Fundamentally the issue is that the KVM hypervisor handles virtual MMIO by causing instructions accessing an MMIO region to trap and then decoding the trapping instruction to figure out what it does. This means that to work with KVM, the instruction which accesses MMIO must be one of the memory access instructions that KVM can decode. This is a much strong guarantee than what volatile provides, even in C, and you really should be using inline assembly in your code instead of volatile operations.

I think this is a good opportunity for a MMIO crate which explicitly guarantees that it emits KVM-compatible instructions on supported architectures.

@RossSmyth RossSmyth changed the title Implement (thumb) volatile with asm Make volatile opportunistically relaxed Apr 5, 2025
@RossSmyth
Copy link
Contributor Author

RossSmyth commented Apr 5, 2025

I think opportunistically making volatile to be relaxed atomic when it is able to would be better. We are allowed to be better than Clang. And I doubt LLVM would accept making volatile atomic by default.

It would be much clearer to the many people who ask what volatile means wrt atomics, and people do and will continue to write code assuming volatile means something in terms of synchronization, so it would mean their code is less broken.

@Amanieu
Copy link
Member

Amanieu commented Apr 5, 2025

I don't think this actually makes things better, it just encourages people to rely on stronger guarantees than what we actually provide. The definition of volatile in Rust has always been "whatever C does" so it doesn't make sense to diverge from C here.

@taiki-e
Copy link
Member

taiki-e commented Apr 6, 2025

FWIW, my atomic-maybe-uninit crate uses "volatile" inline assembly and provides at least atomic load/store on almost all CPU architectures supported by Rust.

It was originally created for use with crossbeam, but I guess it can be used for this purpose as well, since relaxed atomic load stores up to at least a pointer width use normal load/store instructions.

The functionality this crate provides is something that is considered to be impossible to implement without inline assembly at this time, and since there are so few instruction choices reasonably available for within-register-size integer relaxed atomic load/store, there should be no problem in providing more specific implementation guarantees, including instructions that are actually used.

@RalfJung
Copy link
Member

RalfJung commented Apr 7, 2025

It would be very surprising for volatile in Rust to work differently than volatile in Clang

volatile in clang (and C more broadly) is notoriously under-defined. I don't think that should stop us from improving the situation in Rust.

Indeed ideally this should be done by fixing our definition from "whatever C does" to something actually well-defined.

@RossSmyth RossSmyth closed this Jun 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants