Skip to content

Commit 176f895

Browse files
committed
Allow non-ASCII characters in identifiers
Ruby accepts identifiers with non-ASCII code points: `Ú` and `Ω` are valid constants, `únicos` and `日本語` are valid local identifiers. The RBS lexer only recognised ASCII, so the same names could not be written in a signature file. See rubocop/rubocop-on-rbs#151. `rbs_next_char` already folded every non-ASCII character into a single sentinel code point. It now reports one of two, chosen by the encoding's `isupper_char` — the test Ruby's `rb_sym_constant_char_p` uses to tell a constant from a local — and the grammar matches those two classes directly. Widening `word` also lets every existing rule accept non-ASCII continuation characters. Deciding this in `rbs_next_char` rather than in a lexer action covers encodings where a non-ASCII character is a single byte: in ISO-8859-1, `À` is 0xC0 and `à` is 0xE0, and the old `byte_len == 1` branch passed both through as raw bytes that matched no identifier rule. It costs one `isupper_char` call per non-ASCII character rather than one per identifier; over the bundled core and stdlib that is below the noise floor, and even a corpus that is 46% Japanese prose is only ~12% slower. Builds on #2983: the two sentinels are distinct from the invalid-byte sentinel, so an invalid byte inside an identifier still surfaces as an ErrorToken rather than being absorbed. ASCII-8BIT needs an exception. It carries bytes rather than characters and its `char_width` reports 1 for every byte, so folding "every valid non-ASCII character" would turn arbitrary binary into identifiers. `TypeName#kind` answers the same question for names that never pass through the lexer, so it has to agree. Ask Prism — already a hard dependency — instead of restating Ruby's rule with regexps, which covers titlecase letters and non-UTF-8 names that a regexp got wrong. Only the leading `_` case stays, since that is RBS's own interface convention. `rust/ruby-rbs` has no Prism to defer to and keeps an explicit test. src/lexer.c is regenerated with re2c 4.5.1.
1 parent 432d955 commit 176f895

9 files changed

Lines changed: 2279 additions & 1939 deletions

File tree

include/rbs/lexer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ enum RBSTokenType {
100100
tANNOTATION, /* Annotation */
101101
};
102102

103+
/* Arbitrary values, kept in sync by hand with the classes in src/lexer.re. */
104+
#define RBS_MB_UPPER_CODE_POINT 0x30EB /* ル */
105+
#define RBS_MB_OTHER_CODE_POINT 0x30D3 /* ビ */
106+
103107
/**
104108
* The `byte_pos` (or `char_pos`) is the primary data.
105109
* The rest are cache.

lib/rbs/type_name.rb

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@ class TypeName
99
def initialize(namespace:, name:)
1010
@namespace = namespace
1111
@name = name
12-
@kind = case
13-
when name.match?(/\A[A-Z]/)
14-
:class
15-
when name.match?(/\A[a-z]/)
16-
:alias
17-
when name.start_with?("_")
18-
:interface
19-
else
20-
# Defaults to :class
21-
:class
22-
end
12+
# Not a regexp: `\p{...}` raises on a non-UTF-8 name, and no spelling of
13+
# "uppercase" covers titlecase. Not `to_s`: `name` is already frozen.
14+
@kind =
15+
if name.start_with?("_")
16+
:interface
17+
elsif Prism::StringQuery.constant?(name.name)
18+
:class
19+
else
20+
:alias
21+
end
2322
end
2423

2524
# Process-wide flyweight cache. Two-level Hash keyed by canonical

rust/ruby-rbs/src/type_name.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ pub enum Kind {
6060
Interface,
6161
}
6262

63+
/// `char::is_uppercase` excludes `Lt` and `std` cannot query a general
64+
/// category. Unicode has added no `Lt` since 3.0.
65+
const fn is_titlecase(c: char) -> bool {
66+
matches!(
67+
c,
68+
'\u{01C5}'
69+
| '\u{01C8}'
70+
| '\u{01CB}'
71+
| '\u{01F2}'
72+
| '\u{1F88}'..='\u{1F8F}'
73+
| '\u{1F98}'..='\u{1F9F}'
74+
| '\u{1FA8}'..='\u{1FAF}'
75+
| '\u{1FBC}'
76+
| '\u{1FCC}'
77+
| '\u{1FFC}'
78+
)
79+
}
80+
6381
/// An entry rooted in the absolute namespace (`::`).
6482
#[derive(Copy, Clone, Debug)]
6583
struct AbsoluteTypeNameEntry {
@@ -306,11 +324,21 @@ impl TypeNameInterner {
306324
#[must_use]
307325
pub fn kind(&self, name: TypeName, strings: &StringInterner) -> Option<Kind> {
308326
let seg = self.last_segment(name)?;
309-
let bytes = strings.resolve(seg).as_bytes();
310-
let first = *bytes.first()?;
311-
Some(if first == b'_' {
327+
let s = strings.resolve(seg);
328+
let first_byte = *s.as_bytes().first()?;
329+
Some(if first_byte == b'_' {
312330
Kind::Interface
313-
} else if first.is_ascii_uppercase() {
331+
} else if first_byte < 0x80 {
332+
if first_byte.is_ascii_uppercase() {
333+
Kind::Class
334+
} else {
335+
Kind::Alias
336+
}
337+
} else if s
338+
.chars()
339+
.next()
340+
.is_some_and(|c| c.is_uppercase() || is_titlecase(c))
341+
{
314342
Kind::Class
315343
} else {
316344
Kind::Alias
@@ -610,6 +638,32 @@ mod tests {
610638
assert_eq!(t.kind(root, &s), None);
611639
}
612640

641+
#[test]
642+
fn kind_uses_unicode_uppercase_property() {
643+
let (mut s, mut t) = setup();
644+
645+
let ultima = t.parse(&mut s, "Última");
646+
let omega = t.parse(&mut s, "Ωmega");
647+
let n_tilde = t.parse(&mut s, "Ñoño");
648+
assert_eq!(t.kind(ultima, &s), Some(Kind::Class));
649+
assert_eq!(t.kind(omega, &s), Some(Kind::Class));
650+
assert_eq!(t.kind(n_tilde, &s), Some(Kind::Class));
651+
652+
// Lt is outside `Uppercase`, but Ruby starts a constant on it.
653+
let dz = t.parse(&mut s, "Džfoo");
654+
let greek_iota = t.parse(&mut s, "ᾼfoo");
655+
assert_eq!(t.kind(dz, &s), Some(Kind::Class));
656+
assert_eq!(t.kind(greek_iota, &s), Some(Kind::Class));
657+
658+
let alpha = t.parse(&mut s, "αlpha");
659+
let kanji = t.parse(&mut s, "日本語");
660+
assert_eq!(t.kind(alpha, &s), Some(Kind::Alias));
661+
assert_eq!(t.kind(kanji, &s), Some(Kind::Alias));
662+
663+
let iface = t.parse(&mut s, "_Únicos");
664+
assert_eq!(t.kind(iface, &s), Some(Kind::Interface));
665+
}
666+
613667
#[test]
614668
fn to_absolute_to_relative() {
615669
let (mut s, mut t) = setup();

0 commit comments

Comments
 (0)