Skip to main content

tiger_lib/
token.rs

1//! Contains the core [`Token`] and [`Loc`] types, which represent pieces of game script and where
2//! in the game files they came from.
3
4use std::borrow::{Borrow, Cow};
5use std::cmp::Ordering;
6use std::ffi::OsStr;
7use std::fmt::{Debug, Display, Error, Formatter};
8use std::hash::Hash;
9use std::mem::ManuallyDrop;
10use std::ops::{Bound, Range, RangeBounds};
11use std::path::{Path, PathBuf};
12use std::slice::SliceIndex;
13
14use bumpalo::Bump;
15
16use crate::date::Date;
17use crate::fileset::{FileEntry, FileKind};
18use crate::macros::{MACRO_MAP, MacroMapIndex};
19use crate::pathtable::{PathTable, PathTableIndex};
20use crate::report::{ErrorKey, err, untidy};
21
22#[derive(Clone, Copy, Eq, PartialEq, Hash)]
23pub struct Loc {
24    pub(crate) idx: PathTableIndex,
25    pub kind: FileKind,
26    /// line 0 means the loc applies to the file as a whole.
27    pub line: u32,
28    pub column: u32,
29    /// Used in macro expansions to point to the macro invocation
30    /// in the macro table
31    pub link_idx: Option<MacroMapIndex>,
32}
33
34impl Loc {
35    #[must_use]
36    pub(crate) fn for_file(pathname: PathBuf, kind: FileKind, fullpath: PathBuf) -> Self {
37        let idx = PathTable::store(pathname, fullpath);
38        Loc { idx, kind, line: 0, column: 0, link_idx: None }
39    }
40
41    pub fn filename(self) -> Cow<'static, str> {
42        PathTable::lookup_path(self.idx)
43            .file_name()
44            .unwrap_or_else(|| OsStr::new(""))
45            .to_string_lossy()
46    }
47
48    pub fn pathname(self) -> &'static Path {
49        PathTable::lookup_path(self.idx)
50    }
51
52    pub fn fullpath(self) -> &'static Path {
53        PathTable::lookup_fullpath(self.idx)
54    }
55
56    #[inline]
57    pub fn same_file(self, other: Loc) -> bool {
58        self.idx == other.idx
59    }
60}
61
62impl PartialOrd for Loc {
63    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
64        Some(self.cmp(other))
65    }
66}
67
68impl Ord for Loc {
69    fn cmp(&self, other: &Self) -> Ordering {
70        self.idx
71            .cmp(&other.idx)
72            .then(self.line.cmp(&other.line))
73            .then(self.column.cmp(&other.column))
74            .then(
75                self.link_idx
76                    .map(|link| MACRO_MAP.get_loc(link))
77                    .cmp(&other.link_idx.map(|link| MACRO_MAP.get_loc(link))),
78            )
79    }
80}
81
82impl From<&FileEntry> for Loc {
83    fn from(entry: &FileEntry) -> Self {
84        if let Some(idx) = entry.path_idx() {
85            Loc { idx, kind: entry.kind(), line: 0, column: 0, link_idx: None }
86        } else {
87            Self::for_file(entry.path().to_path_buf(), entry.kind(), entry.fullpath().to_path_buf())
88        }
89    }
90}
91
92impl From<&mut FileEntry> for Loc {
93    fn from(entry: &mut FileEntry) -> Self {
94        (&*entry).into()
95    }
96}
97
98impl From<FileEntry> for Loc {
99    fn from(entry: FileEntry) -> Self {
100        (&entry).into()
101    }
102}
103
104impl Debug for Loc {
105    /// Roll our own `Debug` implementation to handle the path field
106    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
107        f.debug_struct("Loc")
108            .field("pathindex", &self.idx)
109            .field("pathname", &self.pathname())
110            .field("fullpath", &self.fullpath())
111            .field("kind", &self.kind)
112            .field("line", &self.line)
113            .field("column", &self.column)
114            .field("linkindex", &self.link_idx)
115            .finish()
116    }
117}
118
119thread_local!(static STR_BUMP: ManuallyDrop<Bump> = ManuallyDrop::new(Bump::new()));
120
121/// Allocate the string on heap with a bump allocator.
122///
123/// SAFETY: This is safe as long as no `Bump::reset` is called to deallocate memory
124/// and `STR_BUMP` is not dropped when thread exits.
125pub(crate) fn bump(s: &str) -> &'static str {
126    STR_BUMP.with(|bump| {
127        let s = bump.alloc_str(s);
128        unsafe {
129            let s_ptr: *const str = s;
130            &*s_ptr
131        }
132    })
133}
134
135/// A Token consists of a string and its location in the parsed files.
136#[allow(missing_copy_implementations)]
137#[derive(Clone, Debug)]
138pub struct Token {
139    s: &'static str,
140    pub loc: Loc,
141}
142
143impl Token {
144    #[must_use]
145    pub fn new(s: &str, loc: Loc) -> Self {
146        Token { s: bump(s), loc }
147    }
148
149    #[must_use]
150    pub fn from_static_str(s: &'static str, loc: Loc) -> Self {
151        Token { s, loc }
152    }
153
154    /// Create a `Token` from a substring of the given `Token`.
155    #[must_use]
156    pub fn subtoken<R>(&self, range: R, loc: Loc) -> Token
157    where
158        R: RangeBounds<usize> + SliceIndex<str, Output = str>,
159    {
160        Token { s: &self.s[range], loc }
161    }
162
163    /// Create a `Token` from a subtring of the given `Token`,
164    /// stripping any whitespace from the created token.
165    #[must_use]
166    pub fn subtoken_stripped(&self, mut range: Range<usize>, mut loc: Loc) -> Token {
167        let mut start = match range.start_bound() {
168            Bound::Included(&i) => i,
169            Bound::Excluded(&i) => i + 1,
170            Bound::Unbounded => 0,
171        };
172        let mut end = match range.end_bound() {
173            Bound::Included(&i) => i + 1,
174            Bound::Excluded(&i) => i,
175            Bound::Unbounded => self.s.len(),
176        };
177        for (i, c) in self.s[range.clone()].char_indices() {
178            if !c.is_whitespace() {
179                start += i;
180                range = start..end;
181                break;
182            }
183            loc.column += 1;
184        }
185        for (i, c) in self.s[range.clone()].char_indices().rev() {
186            if !c.is_whitespace() {
187                end = start + i + c.len_utf8();
188                range = start..end;
189                break;
190            }
191        }
192        Token { s: &self.s[range], loc }
193    }
194
195    pub fn as_str(&self) -> &'static str {
196        self.s
197    }
198
199    pub fn is(&self, s: &str) -> bool {
200        self.s == s
201    }
202
203    pub fn lowercase_is(&self, s: &str) -> bool {
204        self.s.to_ascii_lowercase() == s
205    }
206
207    pub fn starts_with(&self, s: &str) -> bool {
208        self.s.starts_with(s)
209    }
210
211    #[must_use]
212    /// Split the token into one or more subtokens, with `ch` as the delimiter.
213    /// Updates the locs for the created subtokens.
214    /// This is not meant for multiline tokens.
215    /// # Panics
216    /// May panic if the token's column location exceeds 4,294,967,296.
217    pub fn split(&self, ch: char) -> Vec<Token> {
218        let mut pos = 0;
219        let mut vec = Vec::new();
220        let mut loc = self.loc;
221        let mut lines: u32 = 0;
222        for (cols, (i, c)) in self.s.char_indices().enumerate() {
223            let cols = u32::try_from(cols).expect("internal error: 2^32 columns");
224            if c == ch {
225                vec.push(self.subtoken(pos..i, loc));
226                pos = i + 1;
227                loc.column = self.loc.column + cols + 1;
228                loc.line = self.loc.line + lines;
229            }
230            if c == '\n' {
231                lines += 1;
232            }
233        }
234        vec.push(self.subtoken(pos.., loc));
235        vec
236    }
237
238    #[must_use]
239    pub fn strip_suffix(&self, sfx: &str) -> Option<Token> {
240        self.s.strip_suffix(sfx).map(|pfx| Token::from_static_str(pfx, self.loc))
241    }
242
243    #[must_use]
244    pub fn strip_prefix(&self, pfx: &str) -> Option<Token> {
245        #[allow(clippy::cast_possible_truncation)]
246        self.s.strip_prefix(pfx).map(|sfx| {
247            let mut loc = self.loc;
248            loc.column += pfx.chars().count() as u32;
249            Token::from_static_str(sfx, loc)
250        })
251    }
252
253    #[must_use]
254    /// Split the token into two subtokens, with the split at the first occurrence of `ch`.
255    /// Updates the locs for the created subtokens.
256    /// This is not meant for multiline tokens.
257    /// Returns `None` if `ch` was not found in the token.
258    /// # Panics
259    /// May panic if the token's column location exceeds 4,294,967,296.
260    pub fn split_once(&self, ch: char) -> Option<(Token, Token)> {
261        for (cols, (i, c)) in self.s.char_indices().enumerate() {
262            let cols = u32::try_from(cols).expect("internal error: 2^32 columns");
263            if c == ch {
264                let token1 = self.subtoken(..i, self.loc);
265                let mut loc = self.loc;
266                loc.column += cols + 1;
267                let token2 = self.subtoken(i + 1.., loc);
268                return Some((token1, token2));
269            }
270        }
271        None
272    }
273
274    /// Split the token into two subtokens, with the split at the first instance of `ch`, such that `ch` is part of the first returned token.
275    /// Updates the locs for the created subtokens.
276    /// This is not meant for multiline tokens.
277    /// Returns `None` if `ch` was not found in the token.
278    /// # Panics
279    /// May panic if the token's column location exceeds 4,294,967,296.
280    #[must_use]
281    pub fn split_after(&self, ch: char) -> Option<(Token, Token)> {
282        for (cols, (i, c)) in self.s.char_indices().enumerate() {
283            let cols = u32::try_from(cols).expect("internal error: 2^32 columns");
284            #[allow(clippy::cast_possible_truncation)] // chlen can't be more than 6
285            if c == ch {
286                let chlen = ch.len_utf8();
287                let token1 = self.subtoken(..i + chlen, self.loc);
288                let mut loc = self.loc;
289                loc.column += cols + chlen as u32;
290                let token2 = self.subtoken(i + chlen.., loc);
291                return Some((token1, token2));
292            }
293        }
294        None
295    }
296
297    /// Create a new token that is a concatenation of this token and `other`, with `c` between them.
298    pub fn combine(&mut self, other: &Token, c: char) {
299        let mut s = self.s.to_string();
300        s.push(c);
301        s.push_str(other.s);
302        self.s = bump(&s);
303    }
304
305    #[must_use]
306    /// Return a subtoken of this token, such that all whitespace is removed from the start and end.
307    /// Will update the loc of the subtoken.
308    /// This is not meant for multiline tokens.
309    /// # Panics
310    /// May panic if the token's column location exceeds 4,294,967,296.
311    pub fn trim(&self) -> Token {
312        let mut real_start = None;
313        let mut real_end = self.s.len();
314        for (cols, (i, c)) in self.s.char_indices().enumerate() {
315            let cols = u32::try_from(cols).expect("internal error: 2^32 columns");
316            if c != ' ' {
317                real_start = Some((cols, i));
318                break;
319            }
320        }
321        // looping over the indices is safe here because we're only skipping spaces
322        while real_end > 0 && &self.s[real_end - 1..real_end] == " " {
323            real_end -= 1;
324        }
325        if let Some((cols, i)) = real_start {
326            let mut loc = self.loc;
327            loc.column += cols;
328            self.subtoken(i..real_end, loc)
329        } else {
330            // all spaces
331            Token::from_static_str("", self.loc)
332        }
333    }
334
335    pub fn expect_number(&self) -> Option<f64> {
336        self.check_number();
337        // Trim "f" from the end of numbers
338        let s = self.s.trim_end_matches('f');
339        if let Ok(v) = s.parse::<f64>() {
340            Some(v)
341        } else {
342            err(ErrorKey::Validation).msg("expected number").loc(self).push();
343            None
344        }
345    }
346
347    /// Gets the field as a fixed-width decimal, specifically the value multiplied by 100,000
348    pub fn get_fixed_number(&self) -> Option<i64> {
349        if !self.s.contains('.') {
350            return Some(self.s.parse::<i64>().ok()? * 100_000);
351        }
352
353        let r = self.s.find('.')?;
354        let whole = &self.s[..r];
355        let fraction = &self.s[r + 1..];
356
357        if fraction.len() > 5 {
358            return None;
359        }
360        format!("{whole}{fraction:0<5}").parse::<i64>().ok()
361    }
362
363    pub fn get_number(&self) -> Option<f64> {
364        self.s.parse::<f64>().ok()
365    }
366
367    pub fn is_number(&self) -> bool {
368        self.s.parse::<f64>().is_ok()
369    }
370
371    pub fn check_number(&self) {
372        if let Some(idx) = self.s.find('.') {
373            if self.s.len() - idx > 6 {
374                let msg = "only 5 decimals are supported";
375                let info =
376                    "if you give more decimals, you get an error and the number is read as 0";
377                err(ErrorKey::Validation).msg(msg).info(info).loc(self).push();
378            }
379        }
380    }
381
382    /// Some files seem not to have the 5-decimal limitation
383    pub fn expect_precise_number(&self) -> Option<f64> {
384        // Trim "f" from the end of precise numbers
385        let s = if self.s.ends_with("inf") { self.s } else { self.s.trim_end_matches('f') };
386        if let Ok(v) = s.parse::<f64>() {
387            Some(v)
388        } else {
389            err(ErrorKey::Validation).msg("expected number").loc(self).push();
390            None
391        }
392    }
393
394    pub fn expect_integer(&self) -> Option<i64> {
395        if let Ok(v) = self.s.parse::<i64>() {
396            Some(v)
397        } else {
398            err(ErrorKey::Validation).msg("expected integer").loc(self).push();
399            None
400        }
401    }
402
403    pub fn get_integer(&self) -> Option<i64> {
404        self.s.parse::<i64>().ok()
405    }
406
407    pub fn is_integer(&self) -> bool {
408        self.s.parse::<i64>().is_ok()
409    }
410
411    pub fn expect_date(&self) -> Option<Date> {
412        if let Ok(v) = self.s.parse::<Date>() {
413            if self.s.ends_with('.') {
414                untidy(ErrorKey::Validation).msg("trailing dot on date").loc(self).push();
415            }
416            Some(v)
417        } else {
418            err(ErrorKey::Validation).msg("expected date").loc(self).push();
419            None
420        }
421    }
422
423    pub fn get_date(&self) -> Option<Date> {
424        self.s.parse::<Date>().ok()
425    }
426
427    pub fn is_date(&self) -> bool {
428        self.s.parse::<Date>().is_ok()
429    }
430
431    /// Tests if the taken is lowercase
432    pub fn is_lowercase(&self) -> bool {
433        !self.s.chars().any(char::is_uppercase)
434    }
435
436    #[must_use]
437    pub fn linked(mut self, link_idx: Option<MacroMapIndex>) -> Self {
438        self.loc.link_idx = link_idx;
439        self
440    }
441}
442
443impl From<&Token> for Token {
444    fn from(token: &Token) -> Token {
445        token.clone()
446    }
447}
448
449/// Tokens are compared for equality regardless of their loc.
450impl PartialEq for Token {
451    fn eq(&self, other: &Self) -> bool {
452        self.s == other.s
453    }
454}
455
456impl Eq for Token {}
457
458impl Hash for Token {
459    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
460        self.s.hash(state);
461    }
462}
463
464impl Borrow<str> for Token {
465    fn borrow(&self) -> &str {
466        self.s
467    }
468}
469
470impl Borrow<str> for &Token {
471    fn borrow(&self) -> &str {
472        self.s
473    }
474}
475
476impl From<Loc> for Token {
477    fn from(loc: Loc) -> Self {
478        Token { s: "", loc }
479    }
480}
481
482impl Display for Token {
483    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
484        write!(f, "{}", self.s)
485    }
486}