Repository navigation
Tracking Issue for ascii::Char (ACP 179) #110998
Copy link
Copy link
Open
Labels
A-UnicodeArea: UnicodeArea: UnicodeC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCCategory: An issue tracking the progress of sth. like the implementation of an RFCT-libsRelevant to the library team, which will review and decide on the PR/issue.Relevant to the library team, which will review and decide on the PR/issue.
Description
Feature gate: #![feature(ascii_char)] #![feature(ascii_char_variants)]
This is a tracking issue for the ascii::Char type from rust-lang/libs-team#179
https://doc.rust-lang.org/nightly/std/ascii/enum.Char.html
Public API
// core::ascii
#[repr(u8)]
enum Char {
Null = 0,
…
Tilde = 127,
}
impl Debug for Char { … }
impl Display for Char { … }
impl Default for Char { ... }
impl Step for Char { ... } // so `Range<Char>` is an Iterator
impl Char {
const fn from_u8(x: u8) -> Option<Self>;
const unsafe fn from_u8_unchecked(x: u8) -> Self;
const fn digit(d: u8) -> Option<Self>;
const unsafe fn digit_unchecked(d: u8) -> Self;
const fn as_u8(self) -> u8;
const fn as_char(self) -> char;
const fn as_str(&self) -> &str;
}
impl [Char] {
const fn as_str(&self) -> &str;
const fn as_bytes(&self) -> &[u8];
}
impl From<Char> for u8 { … }
impl From<Char> for char { … }
impl From<&[Char]> for &str { … }
// core::array
impl<const N: usize> [u8; N] {
const fn as_ascii(&self) -> Option<&[ascii::Char; N]>;
const unsafe fn as_ascii_unchecked(&self) -> &[ascii::Char; N];
}
// core::char
impl char {
const fn as_ascii(&self) -> Option<ascii::Char>;
}
// core::num
impl u8 {
const fn as_ascii(&self) -> Option<ascii::Char>;
}
// core::slice
impl [u8] {
const fn as_ascii(&self) -> Option<&[ascii::Char]>;
const unsafe fn as_ascii_unchecked(&self) -> &[ascii::Char];
}
// core::str
impl str {
const fn as_ascii(&self) -> Option<&[ascii::Char]>;
}
// alloc::String
impl FromIterator<core::ascii::Char> for String { ... }
impl<'a> FromIterator<&'a core::ascii::Char> for String { ... }
impl<'a> FromIterator<core::ascii::Char> for Cow<'a, str> {Steps / History
- Implementation:
- Final comment period (FCP)1
- Stabilization PR
Unresolved Questions
- What should it be named? Code mixing
charandCharmight be too confusing. - How should its
Debugimpl work? - Is allowing
as-casting from it a good or bad feature?- FWIW, there's no
char::to_u32, justas u32for it.
- FWIW, there's no
- Some of the
as_asciimethods take&selffor consistency withis_ascii. Should they takeselfinstead where possible, as the usually-better option, or stick with&selffor the consistency?
Footnotes
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
A-UnicodeArea: UnicodeArea: UnicodeC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCCategory: An issue tracking the progress of sth. like the implementation of an RFCT-libsRelevant to the library team, which will review and decide on the PR/issue.Relevant to the library team, which will review and decide on the PR/issue.
Activity
BurntSushi commentedon May 2, 2023
I guess I'll kick off the discussion about how to actually define the
ascii::Chartype. Right now, it's an enum like this:I'd like to first make sure I understand the pros of using this representation. Do I have them right?
Option<ascii::Char>is the same size asascii::Char.ch as u8wherechhas typeascii::Char.Are there more? Am I mistaken about the above?
And now, the mitigations:
ascii::Char::as_u8()method. (Which is already done in the implementation PR.) Is there anything else that being able to writech as u8buys us?In terms of negatives, why not do the enum in the first place? Good question. I'm not entirely sure. It feels to me like it stresses my weirdness budget. It's also a generally much bigger enum than most, and I wonder whether that will provoke any surprises in places. Maybe not.
ChayimFriedman2 commentedon May 4, 2023
Do we need both
u8::as_ascii()andascii::Char::from_u8()?Rollup merge of rust-lang#111009 - scottmcm:ascii-char, r=BurntSushi
scottmcm commentedon May 4, 2023
Regarding the enum, @kupiakos said the following in
And mentioned icu4x's enum,
https://github.com/unicode-org/icu4x/blob/b6c4018a736e79790898c5b91ff3ab25d33192c2/utils/tinystr/src/asciibyte.rs#L8-L11
Though given those names it's possible it's just doing that for the niche and not for direct use.
(Man I wish we had private variants as a possibility.)
To your questions, @BurntSushi ,
Yeah, there's at least three other possibilites here: a) put the enum in a newtype (like
ptr::Alignment) to still get the niches without exposing the variants b) use the rustc-internal magic c) just don't get a niche for now since that's not needed for its core scenario of convertibility to UTF-8 (though it's certainly nice to have)Given that we're overall not fond of
ascasts, and it also allows weirder things likeas i64, I actually wish the cast could be disabled, and have people useas_u8as you mention. But that might also just happen by deprecating or warning on these casts, and eventually havingascii::Char: AsRepr<Repr = u8>does seem reasonable.Now that Refactor core::char::EscapeDefault and co. structures #105076 has landed, I've been thinking of redoing the internals of those using
ascii::Charto try out how it feels using the names. That would mean code likebecause without the variants it ends up being more like
which is nice in that it keeps the escape sequences lining up, but having to copy-paste
.as_ascii().unwrap()all over like that doesn't fill me with joy.I guess this is yet another situation in which I'd like custom literals. Or something like
a'\n', I guess, but that's way too far a jump to propose right now.EDIT a while later: The PR to update
EscapeDebuglike that is #111524Rollup merge of rust-lang#111222 - scottmcm:constify-is_ascii, r=thomcc
Auto merge of rust-lang#111222 - scottmcm:constify-is_ascii, r=thomcc
clarfonthey commentedon May 13, 2023
I wasn't in the discussion when this type was created (which I fully support), but is there a reason to explicitly use an enum instead of a newtype + internal niche attribute like
NonZeroU8?If we decide to go the pattern types route (#107606) then
NonZeroU8could becomeu8 is 1..and this could becomeu8 as ..128. Alternatively, if we go the generic integers route (#2581) then this could becomeuint<7>or justu7.IMHO, explicitly going with an enum kind of prevents these kinds of optimisations later down the road, since it forces an enum as the representation.
BurntSushi commentedon May 13, 2023
@clarfonthey I think the discussion above answers your question about why we went with the enum for now. In summary, I think its principle upside is that it gives nice names to each character. Not particularly compelling, but nice.
My general feeling is that nobody is stuck on using an enum here. My best guess is that unless something compelling comes up, I wouldn't be surprised if we changed to an opaque representation such that we could change to an enum later in a compatible manner. (I think that's possible?) Purely because it's the conservative route.
I don't grok the second two paragraphs of your comment. Probably because I don't know what "pattern types" are. I also don't know what
uint<7>means. (Don't have time to follow your links at the moment.)clarfonthey commentedon May 13, 2023
I mean, having nice names is still achievable with constants, and this still would make match expressions work as you'd expect. Personally, the benefits I'm seeing here are the ability to have more-performant code without the need for extra unsafe code, since any slice of ASCII characters is automatically valid UTF-8. Otherwise, a type invariant would mostly be unnecessary, and you could just have constants for the names.
Explaining the two proposals:
T is patwhich is a new type that meansT, but only values which matchpat. So,Option<T> is Some(_)means "options which are some" andu8 is 1..is effectivelyNonZeroU8.uNandiNtypes into aliases for a more genericuint<N>andint<N>type, which has the benefit of allowing non-power-of-two sizes and sizes larger than 128. The idea behinduint<7>is that it would be equivalent tou8internally with the first bit always zero, although still allow all the normal operations on integers like pattern-matching and be castable tou8.The pattern type version is very enticing because it very naturally allows exhaustive matching, while still just being a compile-time constraint. And like I said, we could still have constants for the different names for characters without really affecting things.
BurntSushi commentedon May 13, 2023
Yes, as I said:
I don't know what the stabilization story is for pattern types or generic integers, but I would prefer we focus on finding a path to stabilization that doesn't require being blocked on hypothetical language features. Or at least, focusing discussion on whether we can utilize them later in a backwards compatible fashion.
I still don't really know what pattern types are (I just don't have time right now to grok it), but generic integer types seem like an implementation detail that's consistent with defining
ascii::Charas an opaque type?clarfonthey commentedon May 13, 2023
Right, the point here is that any opaque type would be equivalent to the primary hypothetical future implementations. So, explicitly not blocking on any particular path forward, but making sure that they're compatible.
scottmcm commentedon May 13, 2023
Though you can
usea variant, but can'tusean associated constant. Thususe ascii::Char::*;works with an enum, but not with constants for the names.I agree that this could absolutely be done with the magic attributes. But is there anything in particular that you think is better with that approach?
The enum version still gets the niche, notably:
clarfonthey commentedon May 14, 2023
I will admit that the lack of an ability to
useassociated constants is an immediate downgrade, although I do think that'll eventually be allowed to eventually deprecate thestd::fN::constsmodules.As far as benefits -- the main ones I see are the ability to unify this type with any one of the potential future features I mentioned. If we stabilise an enum now, we are explicitly locked out of this.
That said, there is precedent for separating out the
chartype from the other integer types, and I guess thatascii::Charfits right in with that. I'm just not convinced that locking in this as an enum is worth losing the benefits we could get later, especially considering how it could be converted from an opaque type into an enum publicly later if we eventually decide that is better than the alternatives.192 remaining items
tmke8 commentedon Oct 1, 2026
Oh, I thought the standard library can use unstable features? I agree that waiting for stabilization of pattern types is not a good idea!
EDIT: see for example how
NonNullis defined in the standard library:rust/library/core/src/ptr/non_null.rs
Lines 79 to 81 in c36f145
clarfonthey commentedon Oct 1, 2026
I would expect the main concern to be exposing pattern types to users for ergonomics, not for internal usage.
programmerjake commentedon Oct 1, 2026
If we don't use an enum, I'd expect we'll just have
#[repr(transparent)] pub struct Char(pattern_type!(u8 is 0..0x80));which should be stabilizable like that regardless of pattern types being stable or not.jhpratt commentedon Oct 1, 2026
I don't think so, because you'd be able to
matchon the inner type and not have to provide0x80..=0xFF. That would be an implementation detail if pattern types aren't stable.programmerjake commentedon Oct 1, 2026
the inner type is private so you can't directly access it.
you can't exhaustively match on
NonZeroU8which also uses pattern types internally: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d82372d2d6ba9174944aefcd212d4919I don't see how that could be a problem for
ascii::Charand not also a problem forNonZeroU8jhpratt commentedon Oct 1, 2026
Ah, I misread and thought it was a
pubfield. Yes, if it's internal, no concerns.CodesInChaos commentedon Oct 1, 2026
Would switching from a new-type wrapper around an enum to any of the other proposed representations be a breaking change? If not, such a newtype could be stabilized while keeping the choice of representation open.
AverageHelper commentedon Oct 1, 2026
+1 for making this a newtype with a private field. Unless I missed any others, I've seen only two
enumreasons mentioned in this thread that don't also apply to newtypes:ascasts;.as_u8()and variousFromimpls are probably fine, however, and would be wanted anywayusean associatedconst(still true, afaik); but it's simple enough to declare your own constant if you really need to reference a specific character by name this way:Using a newtype leaves the internal representation open to change or optimization or experimentation without being a breaking change later, while still allowing for
enum's other niceties now, including:const)enum, pattern type, or whatever else we want to try)There's perhaps the relative ease of writing
Space = 0x20,vsconst SPACE: Self = Self(...);, but that's sortof what macros are for.Did I miss any other points that would make stabilizing on a public exhaustive enum a better option than a newtype wrapper w/ private innards?
ronnodas commentedon Oct 2, 2026
Exhaustive matching has been brought up multiple times as a desirable feature. This is something currently possible (ignoring range patterns) with the enum variants but presumably would not be possible with a private field.
Note that
NonZeroU8is a relatively "homogeneous" type, in the sense that there are few operations that need to handle different values in different ways, especially few with many branches. On the other handascii::Charhas many natural "subclasses" that can show up in matches (as evidenced by the various methods on it returningbool:is_uppercase(),is_digit(), etc).tmke8 commentedon Oct 2, 2026
Perhaps for initial stabilization it's fine to drop this. It could be brought back later in a backwards compatible way. You can also just use
unreachable!()(or a[T; 128]table as the godbolt examples above showed) and the compiler will know that this cannot panic.This is an argument in support of range matching, right?
Kixunil commentedon Oct 2, 2026
Exactly what @ronnodas said.
Maybe, but pattern types don't have a monopoly on range matching. I think it should be allowed on C-like enums regardless.
ronnodas commentedon Oct 3, 2026
I agree that exhaustive matching does not need to be part of the initial stabilization, but it should be backwards compatible with it. I believe changing a struct (even one with all private fields) to an enum is breaking.
Indirectly yes, but range matching can currently be emulated (unpleasantly) by listing the variants explicitly, so this tracking issue seems to not be the right place for discussing that feature.
Kixunil commentedon Oct 3, 2026
Currently, yes. There was a proposal to allow
Foo { .. }to match enums which would make it non-breaking.ChaiTRex commentedon Oct 4, 2026
We have
impl From<AsciiChar> for u8and so forth for the unsigned integer types. I think that we should have that for the signed integer types as well.This fits perfectly with the semantics of
Fromwhere the conversion needs to always succeed, as 0 through 127 fit in the nonnegativei8values. Sometimes ASCII characters are represented as signed numbers in C, so this would make it easy to convertAsciiCharto the type aliasescore::ffi::c_charandcore::ffi::c_scharwithoutasortry_from.ChayimFriedman2 commentedon Oct 13, 2026
Per the naming guidelines,
as_ascii()should be namedto_ascii()since it has an O(N) check.