Skip to content

Commit 98eca2f

Browse files
committed
feat(nrepl): completion parity — special-form/class/static-member/keyword sources + the built-in's fuzzy matchers
Smell-audited: 2: resolveJavaSurface's rules moved to the shared runtime/host_class_resolve.zig so completion and the analyzer cannot drift (F-011 commonization; zone: runtime may not import eval); the class-universe divergence is the recorded AD-054, not an unexplained DIFF; oracle exact-match 15/20 → the 5 remaining are AD-054 (x2), D-561/TYPE, D-562 var gaps, and environment-interned keywords. introspect.zig gains the nREPL built-in's completion model: dash-fuzzy var/ns matching (fuzzy-matches?), camelCase member matching (camel-case-matches?), the fixed special-form + literal set, class candidates from the closed rt.types registry (FQN by short name, bare names for java.lang/java.math defaults + (:import) entries, deftypes), Class/ static members (static_fields → static-field, host method_table → static-method), interned-keyword candidates, and a private-var filter matching resolution. opCompletions wires the sources by prefix shape and sorts by candidate (the built-in's sort-by). CLI TAB completion inherits the fuzzy matching via the shared introspect. Character: +isEmoji/isEmojiPresentation/isEmojiModifier(Base)/ isEmojiComponent/isExtendedPictographic (generator now reads UCD emoji/emoji-data.txt) and the deprecated isJavaLetter(OrDigit) aliases; corpus +15 golden (0 DIFF). e2e phase14_nrepl_completion (fixture-driven, 23 probes incl. the require-less Character/ statics, library-ns aliases, fuzzy, sortedness; registered in run_all.sh).
1 parent 2539149 commit 98eca2f

16 files changed

Lines changed: 1037 additions & 79 deletions

File tree

‎.dev/accepted_divergences.yaml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,3 +858,17 @@ accepted:
858858
a closed set.
859859
derives_from: "ADR-0171 (rt merge — internals need a public qualified home for macro expansions) + ADR-0059 no-JVM (cljw owns the cljw.* namespace surface) + F-011 (every public var's home ns now MATCHES clj; only the extra internal ns is cljw-specific)."
860860
pin: "test/e2e/phase15_namespace.sh — `ad053-internal-ns` locks (find-ns 'cljw.internal) present, `ad053-rt-gone` locks the old `rt` ns absent, `ad053-no-leak` locks __-internals out of clojure.core's publics."
861+
- id: "AD-054"
862+
summary: "Class-name completion (nREPL `completions` on an uppercase / dotted prefix) offers cljw's CLOSED class universe — the `rt.types` registry (host surfaces, deftypes, the java.lang/java.math default imports) — where mainline scans the whole JVM classpath and leaks internal classes (`java.lang.CharacterData00`, `sun.nio.cs.ext.Big5`, …)."
863+
example: "prefix \"Character\" => cljw [Character, java.lang.Character] / mainline additionally java.lang.CharacterData00…, com.sun.… (26 candidates). Every cljw candidate resolves; the mainline extras are not even loadable classes for user code."
864+
why: |-
865+
cljw has no classpath and no JVM-internal classes to enumerate
866+
(ADR-0059); its class universe IS the rt.types registry, which is
867+
exactly the set `resolveJavaSurface` resolves — completion never
868+
offers a class name that would not resolve (a stronger guarantee than
869+
mainline's, whose classpath scan surfaces package-private internals
870+
user code cannot construct). Faking the JVM-internal names would be a
871+
lie with zero utility. The completion fixtures therefore assert
872+
cljw ⊆ mainline + a curated MUST list for class groups, not equality.
873+
derives_from: "ADR-0059 no-JVM (no classpath, no JVM-internal classes) + ADR-0029/ADR-0050 (rt.types is the closed host-class SSOT; resolveJavaSurface defines reachability) + F-011 (every class a user can actually USE completes identically; only the un-usable classpath leak differs)."
874+
pin: "test/e2e/phase14_nrepl_completion.sh — classes|Character / classes|Big subset assertions + the MUST-NOT java.lang.CharacterData00 lock (cljw never fakes the leak)."

‎.dev/handover.md‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,23 @@
5050
`__`-internals live in the new `cljw.internal` ns (macro expansions
5151
call them qualified). Retired AD-011/038/049 as PARITY (pins flipped);
5252
new AD-053 pins cljw.internal's existence. serialize VERSION 6.
53-
- **ARC (user-directed 2026-07-15, the landing point)**: (1) CIDER
54-
completion parity — kind annotations, class + static-member candidates
55-
(java-interop completion without require), keywords, special forms,
56-
sort; oracle harness `scripts/completion_oracle.py` + fixtures landed
57-
(red baseline 5/18). (2) AD full inventory + convergence (**D-562**).
58-
(3) THEN cut a release tag. Cron re-fire 2026-07-16 03:02 JST is set
59-
(session-only).
53+
- **COMPLETION PARITY LANDED (2026-07-16)**: nREPL `completions` now
54+
carries the built-in's sources — special forms + literals, vars
55+
(dash-fuzzy `ma-i`→`map-indexed`), namespaces/aliases (dot-fuzzy),
56+
classes (closed rt.types universe via the shared
57+
`runtime/host_class_resolve.zig`; **AD-054** pins the
58+
no-classpath-leak divergence), `Class/` static members
59+
(camelCase-aware — the require-less java-interop ask), interned
60+
keywords — by-name sorted. Fixture-driven e2e
61+
`phase14_nrepl_completion` (23 probes, JVM-free; capture/audit =
62+
`scripts/completion_oracle.py`). Character gained isEmoji×6 (UCD
63+
emoji-data.txt via the generator) + isJavaLetter(OrDigit); statics
64+
126/128 (out: TYPE by design, codePointOf = D-561). Var-existence
65+
gaps mainline completion shows (defmacro-as-var /
66+
default-data-readers / definline / defstruct) → the D-562 inventory.
67+
- **ARC (user-directed 2026-07-15, the landing point)**: (1) completion
68+
parity — DONE. (2) AD full inventory + convergence (**D-562**) —
69+
NEXT. (3) THEN cut a release tag.
6070
- **First task on resume**: self-select from the live `active:` list,
6171
easiest-first (D-523's architecture/wasm-demo residual + D-522 drain 3
6272
landed 2026-07-14; D-430 is DISCHARGED — the prior pointer here was

‎data/compat_tiers.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ tiers:
178178
# 16.0.0 tables; corpus test/diff/clj_corpus/character.txt locks
179179
# clj parity). getName is explicit-unsupported (D-561). Instance
180180
# methods on .char: charValue / compareTo (+ universal Object).
181-
methods: [isDigit, isLetter, isLetterOrDigit, isWhitespace, isUpperCase, isLowerCase, isTitleCase, isSpaceChar, isDefined, isMirrored, isIdeographic, isAlphabetic, isJavaIdentifierStart, isJavaIdentifierPart, isUnicodeIdentifierStart, isUnicodeIdentifierPart, isIdentifierIgnorable, toUpperCase, toLowerCase, toTitleCase, digit, getNumericValue, getType, getDirectionality, forDigit, codePointAt, codePointBefore, codePointCount, offsetByCodePoints, toChars, toString, charCount, isSurrogate, isHighSurrogate, isLowSurrogate, isISOControl, isBmpCodePoint, isValidCodePoint, isSupplementaryCodePoint, highSurrogate, lowSurrogate, toCodePoint, isSurrogatePair, hashCode, compare, valueOf, isSpace, reverseBytes]
181+
methods: [isDigit, isLetter, isLetterOrDigit, isWhitespace, isUpperCase, isLowerCase, isTitleCase, isSpaceChar, isDefined, isMirrored, isIdeographic, isAlphabetic, isJavaIdentifierStart, isJavaIdentifierPart, isJavaLetter, isJavaLetterOrDigit, isUnicodeIdentifierStart, isUnicodeIdentifierPart, isIdentifierIgnorable, isEmoji, isEmojiPresentation, isEmojiModifier, isEmojiModifierBase, isEmojiComponent, isExtendedPictographic, toUpperCase, toLowerCase, toTitleCase, digit, getNumericValue, getType, getDirectionality, forDigit, codePointAt, codePointBefore, codePointCount, offsetByCodePoints, toChars, toString, charCount, isSurrogate, isHighSurrogate, isLowSurrogate, isISOControl, isBmpCodePoint, isValidCodePoint, isSupplementaryCodePoint, highSurrogate, lowSurrogate, toCodePoint, isSurrogatePair, hashCode, compare, valueOf, isSpace, reverseBytes]
182182
static_fields: [MIN_VALUE, MAX_VALUE, MIN_RADIX, MAX_RADIX, SIZE, BYTES, MIN_CODE_POINT, MAX_CODE_POINT, MIN_SUPPLEMENTARY_CODE_POINT, MIN_HIGH_SURROGATE, MAX_HIGH_SURROGATE, MIN_LOW_SURROGATE, MAX_LOW_SURROGATE, MIN_SURROGATE, MAX_SURROGATE, UNASSIGNED, UPPERCASE_LETTER, LOWERCASE_LETTER, TITLECASE_LETTER, MODIFIER_LETTER, OTHER_LETTER, NON_SPACING_MARK, ENCLOSING_MARK, COMBINING_SPACING_MARK, DECIMAL_DIGIT_NUMBER, LETTER_NUMBER, OTHER_NUMBER, SPACE_SEPARATOR, LINE_SEPARATOR, PARAGRAPH_SEPARATOR, CONTROL, FORMAT, PRIVATE_USE, SURROGATE, DASH_PUNCTUATION, START_PUNCTUATION, END_PUNCTUATION, CONNECTOR_PUNCTUATION, OTHER_PUNCTUATION, MATH_SYMBOL, CURRENCY_SYMBOL, MODIFIER_SYMBOL, OTHER_SYMBOL, INITIAL_QUOTE_PUNCTUATION, FINAL_QUOTE_PUNCTUATION, DIRECTIONALITY_UNDEFINED, DIRECTIONALITY_LEFT_TO_RIGHT, DIRECTIONALITY_RIGHT_TO_LEFT, DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC, DIRECTIONALITY_EUROPEAN_NUMBER, DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR, DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR, DIRECTIONALITY_ARABIC_NUMBER, DIRECTIONALITY_COMMON_NUMBER_SEPARATOR, DIRECTIONALITY_NONSPACING_MARK, DIRECTIONALITY_BOUNDARY_NEUTRAL, DIRECTIONALITY_PARAGRAPH_SEPARATOR, DIRECTIONALITY_SEGMENT_SEPARATOR, DIRECTIONALITY_WHITESPACE, DIRECTIONALITY_OTHER_NEUTRALS, DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING, DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE, DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING, DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE, DIRECTIONALITY_POP_DIRECTIONAL_FORMAT, DIRECTIONALITY_LEFT_TO_RIGHT_ISOLATE, DIRECTIONALITY_RIGHT_TO_LEFT_ISOLATE, DIRECTIONALITY_FIRST_STRONG_ISOLATE, DIRECTIONALITY_POP_DIRECTIONAL_ISOLATE]
183183
clojure_peer_vars: []
184184
- fqn: java.lang.Math

‎scripts/gen_unicode_case.py‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,15 @@
5757
"Other_ID_Start", "Other_ID_Continue", "Ideographic",
5858
]
5959

60+
# UCD emoji/emoji-data.txt properties (JVM Character.isEmoji* family).
61+
EMOJI_PROPS = [
62+
"Emoji", "Emoji_Presentation", "Emoji_Modifier",
63+
"Emoji_Modifier_Base", "Emoji_Component", "Extended_Pictographic",
64+
]
65+
6066

6167
def fetch(name: str) -> str:
62-
cache = Path(f"/tmp/ucd_{UCD_VERSION}_{name}")
68+
cache = Path(f"/tmp/ucd_{UCD_VERSION}_{name.replace('/', '_')}")
6369
if cache.exists():
6470
return cache.read_text()
6571
print(f"downloading {name} …", file=sys.stderr)
@@ -205,9 +211,10 @@ def numeric_others(rows):
205211
return sorted(out)
206212

207213

208-
def parse_prop_list(text: str):
209-
"""{prop_name: [(lo,hi),...]} for the PROPS subset of PropList.txt."""
210-
props = {p: [] for p in PROPS}
214+
def parse_prop_list(text: str, wanted=None):
215+
"""{prop_name: [(lo,hi),...]} for the wanted subset of a
216+
range;prop-format UCD file (PropList.txt / emoji-data.txt)."""
217+
props = {p: [] for p in (wanted or PROPS)}
211218
for line in text.splitlines():
212219
line = line.split("#", 1)[0].strip()
213220
if not line:
@@ -261,7 +268,7 @@ def table(name, ranges):
261268
f" .{{ .lo = 0x{lo:X}, .hi = 0x{hi:X}, .v = {v} }}," for lo, hi, v in bidi_tagged
262269
)
263270
prop_tables = "\n\n".join(
264-
table(f"PROP_{p.upper()}", props[p]) for p in PROPS
271+
table(f"PROP_{p.upper()}", props[p]) for p in (PROPS + EMOJI_PROPS)
265272
)
266273
mirrored_table = table("BIDI_MIRRORED", mirrored)
267274
dec_rows = "\n".join(
@@ -374,14 +381,21 @@ def table(name, ranges):
374381
return inRanges(&BIDI_MIRRORED, cp);
375382
}}
376383
377-
/// The contributory properties the JVM classification formulas reference.
384+
/// The contributory properties the JVM classification formulas reference,
385+
/// plus the emoji/emoji-data.txt properties (Character.isEmoji* family).
378386
pub const Prop = enum {{
379387
other_uppercase,
380388
other_lowercase,
381389
other_alphabetic,
382390
other_id_start,
383391
other_id_continue,
384392
ideographic,
393+
emoji,
394+
emoji_presentation,
395+
emoji_modifier,
396+
emoji_modifier_base,
397+
emoji_component,
398+
extended_pictographic,
385399
}};
386400
387401
/// True iff `cp` carries the PropList.txt contributory property.
@@ -393,6 +407,12 @@ def table(name, ranges):
393407
.other_id_start => &PROP_OTHER_ID_START,
394408
.other_id_continue => &PROP_OTHER_ID_CONTINUE,
395409
.ideographic => &PROP_IDEOGRAPHIC,
410+
.emoji => &PROP_EMOJI,
411+
.emoji_presentation => &PROP_EMOJI_PRESENTATION,
412+
.emoji_modifier => &PROP_EMOJI_MODIFIER,
413+
.emoji_modifier_base => &PROP_EMOJI_MODIFIER_BASE,
414+
.emoji_component => &PROP_EMOJI_COMPONENT,
415+
.extended_pictographic => &PROP_EXTENDED_PICTOGRAPHIC,
396416
}};
397417
return inRanges(ranges, cp);
398418
}}
@@ -465,6 +485,12 @@ def table(name, ranges):
465485
try std.testing.expectEqual(@as(?i32, -2), numericValue(0xBD)); // ½
466486
try std.testing.expect(isMirrored('(') and isMirrored('<'));
467487
try std.testing.expect(!isMirrored('a'));
488+
try std.testing.expect(hasProp(.emoji, 0x1F600)); // 😀
489+
try std.testing.expect(hasProp(.emoji, '#')); // keycap base is Emoji
490+
try std.testing.expect(!hasProp(.emoji_presentation, '#'));
491+
try std.testing.expect(hasProp(.emoji_component, '#'));
492+
try std.testing.expect(hasProp(.emoji_modifier, 0x1F3FB)); // skin tone
493+
try std.testing.expect(hasProp(.extended_pictographic, 0x1F600));
468494
}}
469495
"""
470496

@@ -706,6 +732,7 @@ def main():
706732
cat_tagged = tagged_ranges(rows, 2, JVM_CAT)
707733
bidi_tagged = tagged_ranges(rows, 4, JVM_BIDI)
708734
props = parse_prop_list(fetch("PropList.txt"))
735+
props.update(parse_prop_list(fetch("emoji/emoji-data.txt"), EMOJI_PROPS))
709736
dec = decimal_digits(rows)
710737
nums = numeric_others(rows)
711738
mirrored = mirrored_ranges(rows)

‎src/app/nrepl/ops.zig‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ const CompletionCollector = struct {
290290
self.items.append(self.scratch, .{ .dict = entries }) catch return false;
291291
return true;
292292
}
293+
294+
fn candidateLessThan(_: void, a: bencode.Decoded, b: bencode.Decoded) bool {
295+
return std.mem.order(u8, a.dict[0].value.str, b.dict[0].value.str) == .lt;
296+
}
297+
298+
/// The nREPL built-in sorts its reply by candidate name
299+
/// (`sort-by :candidate`); CIDER preserves server order.
300+
fn sortByCandidate(self: *CompletionCollector) void {
301+
std.mem.sort(bencode.Decoded, self.items.items, {}, candidateLessThan);
302+
}
293303
};
294304

295305
fn contextNs(ctx: *Ctx, sess: *session_mod.Session) ?*env_mod.Namespace {
@@ -305,15 +315,24 @@ fn opCompletions(ctx: *Ctx) anyerror!void {
305315
var collector = CompletionCollector{ .scratch = ctx.scratch };
306316
if (prefix.len > 0) {
307317
const ns = contextNs(ctx, sess);
308-
if (std.mem.findScalar(u8, prefix, '/')) |slash| {
318+
if (prefix[0] == ':') {
319+
introspect.forEachKeyword(ctx.env.rt, ctx.scratch, prefix[1..], &collector, CompletionCollector.add);
320+
} else if (std.mem.findScalar(u8, prefix, '/')) |slash| {
309321
collector.qualifier = prefix[0..slash];
310322
if (introspect.resolveQualifier(ctx.env, ns, prefix[0..slash])) |target| {
311323
introspect.forEachNsVar(target, prefix[slash + 1 ..], &collector, CompletionCollector.add);
324+
} else {
325+
// Not a namespace/alias — a `Class/member` static prefix
326+
// (`Character/BY` → Character/BYTES <sf>).
327+
introspect.forEachStaticMember(ctx.env.rt, ns, prefix[0..slash], prefix[slash + 1 ..], &collector, CompletionCollector.add);
312328
}
313329
} else {
330+
introspect.forEachSpecialForm(prefix, &collector, CompletionCollector.add);
314331
introspect.forEachUnqualified(ctx.env, ns, prefix, &collector, CompletionCollector.add);
332+
introspect.forEachClass(ctx.env.rt, ns, prefix, &collector, CompletionCollector.add);
315333
}
316334
}
335+
collector.sortByCandidate();
317336
try ctx.respond(&.{
318337
.{ .key = "completions", .value = .{ .list = collector.items.items } },
319338
.{ .key = "status", .value = try transport.statusValue(ctx.scratch, &.{"done"}) },

‎src/eval/analyzer/special_forms.zig‎

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -51,52 +51,16 @@ const type_descriptor_mod = @import("../../runtime/type_descriptor.zig");
5151
const TypeDescriptor = type_descriptor_mod.TypeDescriptor;
5252

5353
/// Resolve a head namespace string from `(Class/method)` or `(Class. ...)`
54-
/// to a `*const TypeDescriptor` in `rt.types`. ADR-0029 D5 keys
55-
/// descriptors by their cljw-prefixed FQCN (e.g. `"cljw.java.util.UUID"`),
56-
/// but user source writes the JVM form (e.g. `"java.util.UUID"`). This
57-
/// helper bridges by trying, in order:
58-
/// 1. Literal `rt.types.get(head)` (e.g. `"java.util.UUID"`; also a
59-
/// user `(deftype Math …)` shadowing the auto-import — local wins).
60-
/// 2. `rt.types.get("cljw." ++ head)` for the Java prefix translation
61-
/// (`"java.util.UUID"` → `"cljw.java.util.UUID"`).
62-
/// 3. `rt.types.get("cljw.java.lang." ++ head)` for the `java.lang.*`
63-
/// auto-import (ADR-0050 § R3): a bare class name like `Math` /
64-
/// `System` resolves the way JVM Clojure default-imports
65-
/// `java.lang.*` into every ns. Gated to dot-free heads so a
66-
/// qualified head (already handled by 1/2) is not re-probed.
67-
/// Returns `null` if none hit. `env` is read for the current ns's
68-
/// per-ns `(:import …)` simple-name map (D-235), checked before the
69-
/// always-on java.lang auto-import.
54+
/// to a `*const TypeDescriptor` in `rt.types`. The resolution rules
55+
/// (literal key → `cljw.` prefix → per-ns `(:import …)` map → java.lang
56+
/// auto-import → java.math Big* defaults) live in
57+
/// `runtime/host_class_resolve.zig`; `env` supplies the current ns for
58+
/// the import map (D-235).
7059
pub fn resolveJavaSurface(rt: *Runtime, env: *Env, head: []const u8) ?*const TypeDescriptor {
71-
if (rt.types.get(head)) |td| return td;
72-
var buf: [256]u8 = undefined;
73-
const prefixed = std.fmt.bufPrint(&buf, "cljw.{s}", .{head}) catch return null;
74-
if (rt.types.get(prefixed)) |td| return td;
75-
if (std.mem.findScalar(u8, head, '.') == null) {
76-
// A `(:import …)` simple name resolves to its FQCN first (D-235),
77-
// before the always-on java.lang auto-import.
78-
if (env.current_ns) |ns| {
79-
if (ns.imports.get(head)) |fqcn| {
80-
if (rt.types.get(fqcn)) |td| return td;
81-
var ibuf: [256]u8 = undefined;
82-
const iprefixed = std.fmt.bufPrint(&ibuf, "cljw.{s}", .{fqcn}) catch return null;
83-
if (rt.types.get(iprefixed)) |td| return td;
84-
}
85-
}
86-
var buf2: [256]u8 = undefined;
87-
const auto = std.fmt.bufPrint(&buf2, "cljw.java.lang.{s}", .{head}) catch return null;
88-
if (rt.types.get(auto)) |td| return td;
89-
// Beyond java.lang.*, the JVM default-imports `java.math.BigDecimal` and
90-
// `java.math.BigInteger` into every ns (Clojure inherits this). Mirror it
91-
// so a bare `BigDecimal`/`BigInteger` resolves to its cljw.java.math
92-
// surface (e.g. `BigDecimal/ROUND_FLOOR`, numeric-tower D-097/D-420).
93-
if (std.mem.eql(u8, head, "BigDecimal") or std.mem.eql(u8, head, "BigInteger")) {
94-
var buf3: [256]u8 = undefined;
95-
const m = std.fmt.bufPrint(&buf3, "cljw.java.math.{s}", .{head}) catch return null;
96-
if (rt.types.get(m)) |td| return td;
97-
}
98-
}
99-
return null;
60+
// Delegates to the shared runtime resolver so the completion surface
61+
// (introspect.zig) and the analyzer can never drift: a class name
62+
// completes exactly when it resolves here.
63+
return @import("../../runtime/host_class_resolve.zig").resolve(rt, env.current_ns, head);
10064
}
10165

10266
/// Lexical class-name resolution for `instance?`'s class argument, the

‎src/runtime/charset.zig‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,27 @@ pub fn isIdentifierIgnorableCodepoint(cp: u21) bool {
282282
};
283283
}
284284

285+
// JVM `Character.isEmoji*` family (JDK 21) — direct reads of the UCD
286+
// emoji-data.txt properties, generated alongside the contributory ones.
287+
pub fn isEmojiCodepoint(cp: u21) bool {
288+
return unicode_category.hasProp(.emoji, cp);
289+
}
290+
pub fn isEmojiPresentationCodepoint(cp: u21) bool {
291+
return unicode_category.hasProp(.emoji_presentation, cp);
292+
}
293+
pub fn isEmojiModifierCodepoint(cp: u21) bool {
294+
return unicode_category.hasProp(.emoji_modifier, cp);
295+
}
296+
pub fn isEmojiModifierBaseCodepoint(cp: u21) bool {
297+
return unicode_category.hasProp(.emoji_modifier_base, cp);
298+
}
299+
pub fn isEmojiComponentCodepoint(cp: u21) bool {
300+
return unicode_category.hasProp(.emoji_component, cp);
301+
}
302+
pub fn isExtendedPictographicCodepoint(cp: u21) bool {
303+
return unicode_category.hasProp(.extended_pictographic, cp);
304+
}
305+
285306
/// JVM `Character.toUpperCase` mirror — the SIMPLE 1:1 Unicode map (D-057:
286307
/// ä→Ä, σ→Σ; ß stays ß — SpecialCasing 1:n belongs to STRING upper-case
287308
/// only, the JVM full-vs-simple split).

0 commit comments

Comments
 (0)