trex/token.rs
1//! The typed-token contract shared by the lexer and the engine.
2//!
3//! Every trex atom (`\N`, `\W`, `\Q`, `\I`, ...) names a
4//! [`TokenKind`]. The lexer produces a flat `Vec<Token>` over the
5//! input; the derivative engine consumes that slice. Bracket
6//! pairing is recorded on the token itself ([`Token::mate`]) so
7//! that matching a balanced, nestable group is a constant-time
8//! jump rather than a recursive descent at match time.
9//!
10//! This module defines only the data contract. The lexer that fills it
11//! ([`crate::lexer`]) and the engine that reads it ([`crate::engine`])
12//! live in their own modules.
13
14use std::num::NonZeroU32;
15
16/// The three bracket pairs the lexer balances.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub enum BracketKind {
19 /// `(` and `)`.
20 Paren,
21 /// `[` and `]`.
22 Square,
23 /// `{` and `}`.
24 Brace,
25}
26
27/// The typed class of a single token.
28///
29/// Each variant is the target of one surface atom. Promoting a
30/// span to one of these classes is the lexer's job; once classed,
31/// a whole number or a whole quoted string is one atom, which is
32/// what lets a trex pattern stay on one line.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34pub enum TokenKind {
35 /// Integer or floating-point literal. Surface atom: `\N`.
36 Number,
37 /// Word or identifier run. Surface atom: `\W`.
38 Word,
39 /// Quoted string, including its delimiters, with escapes
40 /// resolved by the lexer. A string closes on its own line, or a
41 /// backslash carries it past the newline; a quote its line does not
42 /// close is punctuation. Surface atom: `\Q`.
43 Quoted,
44 /// IPv4 or IPv6 address. Surface atom: `\I`.
45 Ip,
46 /// URL. Surface atom: `\U`.
47 Url,
48 /// Email address. Surface atom: `\E`.
49 Email,
50 /// Date or timestamp. Surface atom: `\T`.
51 Timestamp,
52 /// A semantic-version string, `MAJOR.MINOR.PATCH` with an optional
53 /// `-prerelease` and/or `+build` suffix. Surface atom: `\V`.
54 Version,
55 /// A UUID / GUID: `8-4-4-4-12` hex digits. Surface atom: `\{uuid}`.
56 Uuid,
57 /// A MAC / EUI-48 hardware address: six `:`- or `-`-separated hex
58 /// pairs. Surface atom: `\A`.
59 Mac,
60 /// A hex colour literal, `#rgb` or `#rrggbb`. Surface atom: `\H`.
61 HexColor,
62 /// A CIDR block: an IPv4 address with a `/prefix`. Surface atom: `\C`.
63 Cidr,
64 /// A percentage, `N%` or `N.N%`. Surface atom: `\%`.
65 Percent,
66 /// A byte size with a unit, `10MB` / `1.5GiB` / `512KB`. Surface atom: `\Z`.
67 ByteSize,
68 /// A money amount, `$1,234.56` / `$5`. Surface atom: `\$`.
69 Money,
70 /// A hash digest: a run of exactly 32 / 40 / 64 hex characters with at
71 /// least one hex letter (md5 / sha1 / sha256). Surface atom: `\D`.
72 HashDigest,
73 /// A duration: one or more `number + time-unit` segments
74 /// (`1500ms`, `2.5s`, `3h20m`). Surface atom: `\R`.
75 Duration,
76 /// A filesystem path (`/usr/bin/x`, `./rel`, `../up`, `~/home`,
77 /// `C:\dir\file`, `\\server\share`). Surface atom: `\L`.
78 Path,
79 /// A JSON Web Token: three base64url segments separated by dots
80 /// (`header.payload.signature`). Surface atom: `\{jwt}`.
81 Jwt,
82 /// A payment-card number, 13-19 digits, contiguous or grouped the way an
83 /// issuer prints them (`4-4-4-4`, `4-6-5`, `4-6-4` or `4-4-4-4-3`, with
84 /// one separator throughout, a space or a hyphen), that passes the Luhn
85 /// check. Surface atom: `\{creditcard}`.
86 CreditCard,
87 /// A base64 / base64url blob: a `[A-Za-z0-9+/]` run of length a multiple of
88 /// four and at least 16, with charset diversity (so a plain word is not one)
89 /// and optional `=` padding. Surface atom: `\{base64}`.
90 Base64,
91 /// A geographic coordinate in decimal degrees, `lat,long` with both in range
92 /// (lat -90..90, long -180..180), each carrying four fractional digits, and
93 /// the pair standing alone rather than inside a longer comma-separated run.
94 /// Surface atom: `\{geo}`.
95 Geo,
96 /// A telephone number: international, a `+` then an ITU-T E.164 country
97 /// calling code and 7-15 digits in all; or North American written
98 /// nationally, `NPA-NXX-XXXX` hyphenated with an optional `1-` prefix.
99 /// Surface atom: `\{phone}`.
100 Phone,
101 /// A physical quantity: a number, signed where nothing alphanumeric
102 /// precedes the sign, then a unit symbol of [`crate::quantity`]'s table,
103 /// attached or one space apart (`5kg`, `-40°C`, `3.2 GHz`, `40 %`).
104 /// Surface atom: `\{quantity}`; `\{qty}` is the class of every kind a
105 /// quantity predicate reads.
106 Quantity,
107 /// Run of insignificant whitespace. Surface atom: `\S`.
108 /// Skipped between atoms in token-mode unless matched
109 /// explicitly.
110 Whitespace,
111 /// A single punctuation token. Surface atom: `\P`.
112 Punct,
113 /// An opening bracket of the given kind. Its [`Token::mate`]
114 /// points at the matching close.
115 Open(BracketKind),
116 /// A closing bracket of the given kind. Its [`Token::mate`]
117 /// points back at the matching open.
118 Close(BracketKind),
119 /// A span matching a user-declared shape, identified by its id in the
120 /// [`crate::custom::ShapeSet`] that lexed it. Surface atom: `\{name}`,
121 /// the name that shape was declared under.
122 Custom(u8),
123 /// A token the lexer did not assign a more specific class.
124 Other,
125}
126
127impl TokenKind {
128 /// Whether this kind is skipped between atoms in token-mode.
129 #[must_use]
130 pub fn is_insignificant(self) -> bool {
131 matches!(self, TokenKind::Whitespace)
132 }
133
134 /// The bracket a kind code stands for and whether it opens, or `None` for
135 /// a code that is no bracket.
136 ///
137 /// Beside [`TokenKind::code`] so both directions of the encoding read from
138 /// the one table, and derived from it rather than written out a second
139 /// time. A reader holding codes and not kinds - the significant stream
140 /// stores codes - needs this to match a close against the open it wants.
141 #[must_use]
142 pub fn bracket_of_code(code: u32) -> Option<(bool, BracketKind)> {
143 for bk in [BracketKind::Paren, BracketKind::Square, BracketKind::Brace] {
144 if TokenKind::Open(bk).code() == code {
145 return Some((true, bk));
146 }
147 if TokenKind::Close(bk).code() == code {
148 return Some((false, bk));
149 }
150 }
151 None
152 }
153
154 /// A small, total, distinct integer code for this kind, used to
155 /// carry the token-kind stream and the atom-kind table to a backend
156 /// that cannot hold the Rust enum (the GPU kernel). The bracket kind
157 /// is folded into the code so `Open(Paren)` and `Open(Square)` stay
158 /// distinct.
159 #[must_use]
160 pub fn code(self) -> u32 {
161 match self {
162 TokenKind::Number => 0,
163 TokenKind::Word => 1,
164 TokenKind::Quoted => 2,
165 TokenKind::Ip => 3,
166 TokenKind::Url => 4,
167 TokenKind::Email => 5,
168 TokenKind::Timestamp => 6,
169 TokenKind::Whitespace => 7,
170 TokenKind::Punct => 8,
171 TokenKind::Other => 9,
172 TokenKind::Open(BracketKind::Paren) => 10,
173 TokenKind::Open(BracketKind::Square) => 11,
174 TokenKind::Open(BracketKind::Brace) => 12,
175 TokenKind::Close(BracketKind::Paren) => 13,
176 TokenKind::Close(BracketKind::Square) => 14,
177 TokenKind::Close(BracketKind::Brace) => 15,
178 // Codes for the richer typed kinds continue past the bracket
179 // block. The GPU kernel compares these as opaque integers
180 // (`kernels/scan.cu`), so a new code needs no kernel change.
181 TokenKind::Version => 16,
182 TokenKind::Uuid => 17,
183 TokenKind::Mac => 18,
184 TokenKind::HexColor => 19,
185 TokenKind::Cidr => 20,
186 TokenKind::Percent => 21,
187 TokenKind::ByteSize => 22,
188 TokenKind::Money => 23,
189 TokenKind::HashDigest => 24,
190 TokenKind::Duration => 25,
191 TokenKind::Path => 26,
192 TokenKind::Jwt => 27,
193 TokenKind::CreditCard => 28,
194 TokenKind::Base64 => 29,
195 TokenKind::Geo => 30,
196 TokenKind::Phone => 31,
197 TokenKind::Quantity => 32,
198 // Custom codes start past the built-in block. `shape_class`
199 // shifts a code left by 16, so the range stays inside a u32.
200 TokenKind::Custom(id) => 33 + u32::from(id),
201 }
202 }
203
204 /// The kind a code names: the inverse of [`Self::code`] on every kind.
205 ///
206 /// # Panics
207 /// On a code no kind has, which only a stream written by something other
208 /// than [`Self::code`] could carry.
209 #[must_use]
210 pub fn from_code(code: u32) -> TokenKind {
211 match code {
212 0 => TokenKind::Number,
213 1 => TokenKind::Word,
214 2 => TokenKind::Quoted,
215 3 => TokenKind::Ip,
216 4 => TokenKind::Url,
217 5 => TokenKind::Email,
218 6 => TokenKind::Timestamp,
219 7 => TokenKind::Whitespace,
220 8 => TokenKind::Punct,
221 9 => TokenKind::Other,
222 10 => TokenKind::Open(BracketKind::Paren),
223 11 => TokenKind::Open(BracketKind::Square),
224 12 => TokenKind::Open(BracketKind::Brace),
225 13 => TokenKind::Close(BracketKind::Paren),
226 14 => TokenKind::Close(BracketKind::Square),
227 15 => TokenKind::Close(BracketKind::Brace),
228 16 => TokenKind::Version,
229 17 => TokenKind::Uuid,
230 18 => TokenKind::Mac,
231 19 => TokenKind::HexColor,
232 20 => TokenKind::Cidr,
233 21 => TokenKind::Percent,
234 22 => TokenKind::ByteSize,
235 23 => TokenKind::Money,
236 24 => TokenKind::HashDigest,
237 25 => TokenKind::Duration,
238 26 => TokenKind::Path,
239 27 => TokenKind::Jwt,
240 28 => TokenKind::CreditCard,
241 29 => TokenKind::Base64,
242 30 => TokenKind::Geo,
243 31 => TokenKind::Phone,
244 32 => TokenKind::Quantity,
245 33..=288 => TokenKind::Custom((code - 33) as u8),
246 _ => panic!("{code} is no token kind's code"),
247 }
248 }
249}
250
251/// One lexed token: a typed class plus the half-open byte span
252/// `[start, end)` it covers in the input.
253///
254/// The bracket-pairing result rides on the token: for an
255/// [`TokenKind::Open`] / [`TokenKind::Close`] token [`Token::mate`] is the
256/// index of the partner token in the stream, and `None` for every other
257/// kind.
258///
259/// A whole input's tokens are one flat vector that every consumer streams,
260/// so the struct's width is bandwidth. The mate is stored as the partner's
261/// index plus one in a [`NonZeroU32`], which puts the `None` in the zero
262/// niche and costs four bytes rather than the sixteen an `Option<usize>`
263/// takes.
264impl TokenKind {
265 /// The kind's name, as `\{name}` writes it in a pattern.
266 #[must_use]
267 pub fn name(self) -> &'static str {
268 match self {
269 TokenKind::Number => "number",
270 TokenKind::Word => "word",
271 TokenKind::Quoted => "quoted",
272 TokenKind::Ip => "ip",
273 TokenKind::Url => "url",
274 TokenKind::Email => "email",
275 TokenKind::Timestamp => "timestamp",
276 TokenKind::Version => "version",
277 TokenKind::Uuid => "uuid",
278 TokenKind::Mac => "mac",
279 TokenKind::HexColor => "hexcolor",
280 TokenKind::Cidr => "cidr",
281 TokenKind::Percent => "percent",
282 TokenKind::ByteSize => "bytesize",
283 TokenKind::Money => "money",
284 TokenKind::HashDigest => "hash",
285 TokenKind::Duration => "duration",
286 TokenKind::Path => "path",
287 TokenKind::Jwt => "jwt",
288 TokenKind::CreditCard => "creditcard",
289 TokenKind::Base64 => "base64",
290 TokenKind::Geo => "geo",
291 TokenKind::Phone => "phone",
292 TokenKind::Quantity => "quantity",
293 TokenKind::Whitespace => "whitespace",
294 TokenKind::Punct => "punct",
295 TokenKind::Open(_) => "open",
296 TokenKind::Close(_) => "close",
297 TokenKind::Custom(_) => "custom",
298 TokenKind::Other => "other",
299 }
300 }
301
302 /// The kind written under `name`, the inverse of [`Self::name`] for the
303 /// kinds a name reaches.
304 ///
305 /// A bracket is named by its side rather than its shape, since that is
306 /// what [`Self::name`] reports; a declared kind has no name here, because
307 /// the name belongs to the declaration rather than to the enum.
308 #[must_use]
309 pub fn named(name: &str) -> Option<TokenKind> {
310 [
311 TokenKind::Number,
312 TokenKind::Word,
313 TokenKind::Quoted,
314 TokenKind::Ip,
315 TokenKind::Url,
316 TokenKind::Email,
317 TokenKind::Timestamp,
318 TokenKind::Version,
319 TokenKind::Uuid,
320 TokenKind::Mac,
321 TokenKind::HexColor,
322 TokenKind::Cidr,
323 TokenKind::Percent,
324 TokenKind::ByteSize,
325 TokenKind::Money,
326 TokenKind::HashDigest,
327 TokenKind::Duration,
328 TokenKind::Path,
329 TokenKind::Jwt,
330 TokenKind::CreditCard,
331 TokenKind::Base64,
332 TokenKind::Geo,
333 TokenKind::Phone,
334 TokenKind::Quantity,
335 TokenKind::Whitespace,
336 TokenKind::Punct,
337 TokenKind::Open(BracketKind::Paren),
338 TokenKind::Close(BracketKind::Paren),
339 TokenKind::Other,
340 ]
341 .into_iter()
342 .find(|k| k.name() == name)
343 }
344
345 /// The single letter that names the kind as `\X` in a pattern, for the
346 /// kinds that have one.
347 #[must_use]
348 pub fn escape(self) -> Option<char> {
349 Some(match self {
350 TokenKind::Number => 'N',
351 TokenKind::Word => 'W',
352 TokenKind::Quoted => 'Q',
353 TokenKind::Ip => 'I',
354 TokenKind::Url => 'U',
355 TokenKind::Email => 'E',
356 TokenKind::Timestamp => 'T',
357 TokenKind::Punct => 'P',
358 TokenKind::Whitespace => 'S',
359 TokenKind::Version => 'V',
360 TokenKind::HexColor => 'H',
361 TokenKind::Cidr => 'C',
362 TokenKind::ByteSize => 'Z',
363 TokenKind::Percent => '%',
364 TokenKind::Money => '$',
365 TokenKind::HashDigest => 'D',
366 TokenKind::Duration => 'R',
367 TokenKind::Path => 'L',
368 TokenKind::Uuid
369 | TokenKind::Mac
370 | TokenKind::Jwt
371 | TokenKind::CreditCard
372 | TokenKind::Base64
373 | TokenKind::Geo
374 | TokenKind::Phone
375 | TokenKind::Quantity
376 | TokenKind::Open(_)
377 | TokenKind::Close(_)
378 | TokenKind::Custom(_)
379 | TokenKind::Other => return None,
380 })
381 }
382}
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
385pub struct Token {
386 /// The token's typed class.
387 pub kind: TokenKind,
388 /// Inclusive start byte offset into the input.
389 pub start: u32,
390 /// Exclusive end byte offset into the input.
391 pub end: u32,
392 /// The mate's index plus one, or `None`. Read it through
393 /// [`Token::mate`] and write it through [`Token::set_mate`], which hold
394 /// the encoding in one place; the field is named for what it stores so
395 /// that reading it as an index does not compile.
396 pub mate_plus_one: Option<NonZeroU32>,
397}
398
399impl Token {
400 /// Construct a token with no bracket mate.
401 ///
402 /// # Panics
403 /// When an offset does not fit the stored width, which needs an input
404 /// over four gigabytes. Truncating instead would point a token at the
405 /// wrong bytes.
406 #[must_use]
407 #[inline]
408 pub fn new(kind: TokenKind, start: usize, end: usize) -> Self {
409 Self {
410 kind,
411 start: u32::try_from(start).expect("a byte offset within the stored width"),
412 end: u32::try_from(end).expect("a byte offset within the stored width"),
413 mate_plus_one: None,
414 }
415 }
416
417 /// The token's byte span, for indexing the input it was lexed from.
418 #[must_use]
419 #[inline]
420 pub fn span(&self) -> std::ops::Range<usize> {
421 self.start as usize..self.end as usize
422 }
423
424 /// The start offset as a `usize`, for arithmetic and indexing against
425 /// everything else, which counts bytes in the machine's width. The field
426 /// is the storage and this is the view of it; the two never disagree,
427 /// and a site that needs the wider one fails to compile rather than
428 /// silently taking the narrower.
429 #[must_use]
430 #[inline]
431 pub fn start(&self) -> usize {
432 self.start as usize
433 }
434
435 /// The end offset as a `usize`. See [`Token::start`].
436 #[must_use]
437 #[inline]
438 pub fn end(&self) -> usize {
439 self.end as usize
440 }
441
442 /// Move the span by `by` bytes, for a token lexed from a chunk and
443 /// rebased onto the whole input.
444 ///
445 /// # Panics
446 /// When the shifted offset does not fit the stored width.
447 #[inline]
448 pub fn shift(&mut self, by: usize) {
449 let shifted = |v: u32| -> u32 {
450 u32::try_from(v as usize + by).expect("a byte offset within the stored width")
451 };
452 self.start = shifted(self.start);
453 self.end = shifted(self.end);
454 }
455
456 /// The index of this token's matching bracket, or `None` when it has
457 /// none.
458 #[must_use]
459 #[inline]
460 pub fn mate(&self) -> Option<usize> {
461 self.mate_plus_one.map(|m| m.get() as usize - 1)
462 }
463
464 /// Record this token's matching bracket, or clear it.
465 ///
466 /// # Panics
467 /// When the index does not fit the stored width. A stream that long
468 /// would need more than four billion tokens, which no input this lexer
469 /// can hold produces, and silently storing a wrapped index would pair
470 /// the wrong brackets.
471 #[inline]
472 pub fn set_mate(&mut self, index: Option<usize>) {
473 self.mate_plus_one = index.map(|i| {
474 let plus_one = u32::try_from(i + 1).expect("a token index within the stored width");
475 NonZeroU32::new(plus_one).expect("one more than an index is never zero")
476 });
477 }
478
479 /// The byte length of the token's span.
480 #[must_use]
481 pub fn len(&self) -> usize {
482 (self.end - self.start) as usize
483 }
484
485 /// Whether the token's span is empty.
486 #[must_use]
487 pub fn is_empty(&self) -> bool {
488 self.start == self.end
489 }
490
491 /// Whether this token participates in token-mode matching
492 /// (everything except insignificant whitespace).
493 #[must_use]
494 pub fn is_significant(&self) -> bool {
495 !self.kind.is_insignificant()
496 }
497}
498
499/// The class key of a token: a generic token-to-string mapping over the typed-token kinds, used by
500/// the compression-based structure layer and the field axes. Numbers / strings / literals collapse
501/// to a tag, short words stay literal, long words collapse to `#id`, brackets and punctuation stay
502/// themselves. A substrate primitive over the typed tokens, independent of any domain.
503#[must_use]
504pub fn code_class(t: &Token, code: &[u8]) -> String {
505 let text = || String::from_utf8_lossy(&code[t.span()]).into_owned();
506 match t.kind {
507 TokenKind::Punct => text(),
508 TokenKind::Number => "#num".into(),
509 TokenKind::Quoted => "#str".into(),
510 TokenKind::Ip
511 | TokenKind::Url
512 | TokenKind::Email
513 | TokenKind::Timestamp
514 | TokenKind::Version
515 | TokenKind::Uuid
516 | TokenKind::Mac
517 | TokenKind::HexColor
518 | TokenKind::Cidr
519 | TokenKind::Percent
520 | TokenKind::ByteSize
521 | TokenKind::Money
522 | TokenKind::HashDigest
523 | TokenKind::Duration
524 | TokenKind::Path
525 | TokenKind::Jwt
526 | TokenKind::CreditCard
527 | TokenKind::Base64
528 | TokenKind::Geo
529 | TokenKind::Phone
530 | TokenKind::Quantity => "#lit".into(),
531 TokenKind::Word => {
532 let w = text();
533 if w.len() <= 5 { w } else { "#id".into() }
534 }
535 TokenKind::Open(BracketKind::Paren) => "(".into(),
536 TokenKind::Open(BracketKind::Square) => "[".into(),
537 TokenKind::Open(BracketKind::Brace) => "{".into(),
538 TokenKind::Close(BracketKind::Paren) => ")".into(),
539 TokenKind::Close(BracketKind::Square) => "]".into(),
540 TokenKind::Close(BracketKind::Brace) => "}".into(),
541 TokenKind::Custom(id) => format!("#shape{id}"),
542 TokenKind::Other | TokenKind::Whitespace => "#other".into(),
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn span_length_matches_offsets() {
552 let t = Token::new(TokenKind::Number, 4, 7);
553 assert_eq!(t.len(), 3);
554 assert!(!t.is_empty());
555 assert!(t.is_significant());
556 }
557
558 #[test]
559 fn whitespace_is_insignificant() {
560 let t = Token::new(TokenKind::Whitespace, 0, 1);
561 assert!(!t.is_significant());
562 assert!(t.kind.is_insignificant());
563 }
564
565 #[test]
566 fn bracket_mate_defaults_none_then_sets() {
567 let mut t = Token::new(TokenKind::Open(BracketKind::Paren), 0, 1);
568 assert_eq!(t.mate(), None);
569 t.set_mate(Some(9));
570 assert_eq!(t.mate(), Some(9));
571 // Index zero is a mate like any other; the stored value is what
572 // carries the plus one.
573 t.set_mate(Some(0));
574 assert_eq!(t.mate(), Some(0));
575 assert_eq!(t.mate_plus_one.map(NonZeroU32::get), Some(1));
576 t.set_mate(None);
577 assert_eq!(t.mate(), None);
578 }
579
580 #[test]
581 fn a_token_is_no_wider_than_the_layout_it_was_shrunk_to() {
582 // Two offsets and a mate at four bytes each, a kind at two: fourteen
583 // of content at an alignment of four. The option costs the zero
584 // niche rather than a word, which is what the plus-one encoding buys.
585 assert_eq!(size_of::<Option<NonZeroU32>>(), 4);
586 assert_eq!(size_of::<Token>(), 16);
587 }
588
589 /// Every bracket kind survives the trip out to a code and back, and no
590 /// other kind answers as a bracket - which is what lets a reader holding
591 /// codes match a close against its open.
592 #[test]
593 fn a_bracket_kind_code_reads_back_as_the_bracket_it_came_from() {
594 for bk in [BracketKind::Paren, BracketKind::Square, BracketKind::Brace] {
595 assert_eq!(
596 TokenKind::bracket_of_code(TokenKind::Open(bk).code()),
597 Some((true, bk)),
598 "an open {bk:?} reads back"
599 );
600 assert_eq!(
601 TokenKind::bracket_of_code(TokenKind::Close(bk).code()),
602 Some((false, bk)),
603 "a close {bk:?} reads back"
604 );
605 }
606 for kind in [
607 TokenKind::Number,
608 TokenKind::Word,
609 TokenKind::Quoted,
610 TokenKind::Whitespace,
611 TokenKind::Punct,
612 TokenKind::Other,
613 TokenKind::Timestamp,
614 ] {
615 assert_eq!(
616 TokenKind::bracket_of_code(kind.code()),
617 None,
618 "{kind:?} is no bracket"
619 );
620 }
621 }
622}