use crate::error::SwiftletError;
use fancy_regex::{Regex, RegexBuilder};
use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use re_parser;
#[derive(Clone, Hash, Eq, PartialEq)]
pub enum Symbol {
Terminal(String),
NonTerminal(String),
}
impl Symbol {
pub fn as_str(&self) -> &str {
match self {
Symbol::Terminal(value) => value.as_str(),
Symbol::NonTerminal(value) => value.as_str(),
}
}
#[inline]
pub fn get_value(&self) -> String {
self.as_str().to_string()
}
#[inline(always)]
pub fn is_terminal(&self) -> bool {
matches!(self, Symbol::Terminal(_))
}
pub fn starts_with(&self, prefix: &str) -> bool {
match self {
Symbol::Terminal(value) => value.starts_with(prefix),
Symbol::NonTerminal(value) => value.starts_with(prefix),
}
}
}
impl Display for Symbol {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Debug for Symbol {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Symbol::Terminal(value) => write!(f, "Terminal({})", value),
Symbol::NonTerminal(value) => write!(f, "NonTerminal({})", value),
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum Pattern {
PatternStr(String),
PatternRegex(Regex),
}
impl Pattern {
pub(crate) fn capture(&self, text: &str) -> Option<(usize, usize)> {
match self {
Pattern::PatternStr(name) => {
if text.starts_with(name) {
return Some((name.len(), name.len()));
}
None
}
Pattern::PatternRegex(regex) => {
if let Ok(Some(caps)) = regex.captures(text)
&& caps.len() > 0
&& let Some(mt) = caps.get(0)
{
return Some((mt.end(), mt.as_str().len()));
}
None
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct RegexFlag {
pub(crate) i: bool, pub(crate) m: bool, pub(crate) s: bool, pub(crate) u: bool, pub(crate) x: bool, }
impl Default for RegexFlag {
fn default() -> Self {
Self {
i: false,
m: false,
s: false,
u: true,
x: false,
}
}
}
#[derive(Debug, Clone)]
pub struct TerminalDef {
pub(crate) name: Arc<Symbol>,
#[allow(dead_code)]
pub(crate) value: String,
pub(crate) pattern: Pattern,
pub(crate) min_width: usize,
pub(crate) max_width: usize,
pub(crate) priority: usize,
}
impl TerminalDef {
pub(crate) fn with_string(name: &str, value: &str, priority: usize) -> Self {
let name = Arc::new(Symbol::Terminal(name.to_string()));
Self {
name,
value: value.to_string(),
pattern: Pattern::PatternStr(value.to_string()),
min_width: value.len(),
max_width: value.len(),
priority,
}
}
pub(crate) fn with_regex(
name: &str,
value: &str,
regex_flag: RegexFlag,
priority: usize,
) -> Self {
let name = Arc::new(Symbol::Terminal(name.to_string()));
let (pattern, min_width, max_width) = {
let rb = RegexBuilder::new((r"^".to_string() + value).as_str())
.case_insensitive(regex_flag.i)
.multi_line(regex_flag.m)
.dot_matches_new_line(regex_flag.s)
.unicode_mode(regex_flag.u)
.verbose_mode(regex_flag.x)
.build()
.unwrap();
let pr = Pattern::PatternRegex(rb);
let (min, max) = match re_parser::parse(value) {
Ok(re_ast) => {
(re_ast.min_width(), re_ast.max_width().unwrap_or_else(|| usize::MAX))
},
Err(_) => {
if value.contains("+") || value.contains("*") {
(value.len(), usize::MAX)
} else {
(value.len(), value.len())
}
}
};
(pr, min, max)
};
Self {
name,
value: value.to_string(),
pattern,
min_width,
max_width,
priority,
}
}
pub fn get_name(&self) -> &Arc<Symbol> {
&self.name
}
fn capture(&self, text: &str) -> Option<(usize, usize)> {
self.pattern.capture(text)
}
}
impl PartialEq for TerminalDef {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.value == other.value
}
}
#[derive(Clone, Debug)]
pub struct Token {
source: Arc<str>,
start: usize,
end: usize,
line: usize,
pub terminal: Arc<Symbol>,
pub terminal_is_hidden: bool,
}
impl Token {
pub fn new(
source: impl Into<Arc<str>>,
start: usize,
end: usize,
line: usize,
terminal: Arc<Symbol>,
) -> Self {
let name = terminal.as_str();
let terminal_is_hidden = name.starts_with('_') && !name.starts_with("__");
Self {
source: source.into(),
start,
end,
line,
terminal,
terminal_is_hidden,
}
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn line(&self) -> usize {
self.line
}
pub fn terminal(&self) -> &str {
self.terminal.as_str()
}
pub fn word(&self) -> &str {
if self.start <= self.end && self.end <= self.source.len() {
&self.source[self.start..self.end]
} else {
&self.source
}
}
}
impl Display for Token {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let text = format!(
"Token {{ word: {}, start: {}, end: {}, line: {}, terminal: {:?} }}",
self.word(),
self.start,
self.end,
self.line,
self.terminal
);
f.write_str(&text)
}
}
impl PartialEq for Token {
fn eq(&self, other: &Self) -> bool {
self.word() == other.word() && self.line == other.line && self.terminal == other.terminal
}
}
impl Eq for Token {}
impl Hash for Token {
fn hash<H: Hasher>(&self, state: &mut H) {
self.word().hash(state);
self.line().hash(state);
self.terminal().hash(state);
}
}
#[derive(Clone, Debug)]
pub(crate) struct TokenMatch {
pub(crate) token: Arc<Token>,
pub(crate) next_start: usize,
pub(crate) next_line: usize,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct TokenProbe {
pub(crate) start: usize,
pub(crate) line: usize,
pub(crate) next_start: usize,
pub(crate) next_line: usize,
pub(crate) priority: usize,
}
#[derive(Debug, Clone)]
pub struct Tokenizer {
text: Arc<str>,
start: usize,
line: usize,
len: usize,
sym_terminal_def: Arc<FxHashMap<Arc<Symbol>, Arc<TerminalDef>>>,
ignore_terminals: Arc<[Arc<TerminalDef>]>,
skip_cache: RefCell<Option<Vec<(usize, usize)>>>,
}
impl Tokenizer {
pub(crate) fn new(
text: Arc<str>,
sym_terminal_def: Arc<FxHashMap<Arc<Symbol>, Arc<TerminalDef>>>,
ignore_terminals: Arc<[Arc<TerminalDef>]>,
) -> Self {
let len = text.len();
Self {
text,
start: 0usize,
line: 0usize,
len,
sym_terminal_def,
ignore_terminals,
skip_cache: RefCell::new(None),
}
}
pub(crate) fn get_start(&self) -> usize {
self.start
}
pub(crate) fn get_line_column(&self) -> (usize, usize) {
self.line_column(self.start)
}
pub(crate) fn get_terminal_def(&self, name: &Arc<Symbol>) -> Option<&Arc<TerminalDef>> {
self.sym_terminal_def.get(name)
}
pub(crate) fn get_text(&self) -> &str {
&self.text
}
pub(crate) fn commit_token_match(&mut self, token_match: &TokenMatch) {
self.commit_position(token_match.next_start, token_match.next_line);
}
pub(crate) fn commit_position(&mut self, next_start: usize, next_end: usize) {
self.start = next_start;
self.line = next_end;
*self.skip_cache.get_mut() = None;
}
pub(crate) fn build_token(
&self,
start: usize,
end: usize,
line: usize,
terminal: &Arc<Symbol>,
) -> Arc<Token> {
Arc::new(Token::new(
self.text.clone(),
start,
end,
line,
terminal.clone(),
))
}
fn skip_trajectory(&self) -> Vec<(usize, usize)> {
let mut traj = vec![];
let mut start = self.start;
let mut line = self.line;
while start < self.len {
traj.push((start, line));
let slice_text = &self.text[start..self.len];
let mut advanced = false;
for term_def in self.ignore_terminals.iter() {
if let Some((mt_end, _)) = term_def.pattern.capture(slice_text) {
if term_def.name.as_ref().as_str() == "_NL" {
line += 1;
}
start += mt_end;
advanced = true;
break;
}
}
if !advanced {
break;
}
}
traj
}
fn line_column(&self, location: usize) -> (usize, usize) {
let prefix = &self.text[..location];
let line = prefix.chars().filter(|ch| *ch == '\n').count() + 1;
let column = prefix
.rsplit('\n')
.next()
.map(|segment| segment.chars().count() + 1)
.unwrap_or(1);
(line, column)
}
pub(crate) fn peek_probe(
&self,
next_symbols: &Arc<Symbol>,
) -> Option<TokenProbe> {
let terminal = self.sym_terminal_def.get(next_symbols)?;
if self.skip_cache.borrow().is_none() {
let traj = self.skip_trajectory();
*self.skip_cache.borrow_mut() = Some(traj);
}
let cache = self.skip_cache.borrow();
let trajectory = cache.as_ref().expect("skip trajectory just populated");
for &(start, line) in trajectory.iter() {
if let Some((mt_end, _)) = terminal.capture(&self.text[start..]) {
let next_start = start + mt_end;
let next_line = if terminal.name.as_ref().as_str() == "_NL" {
line + 1
} else {
line
};
return Some(TokenProbe {
start,
line,
next_start,
next_line,
priority: terminal.priority
});
}
}
None
}
pub(crate) fn peek_token_with_next_symbol(
&self,
next_symbols: &Arc<Symbol>) -> Result<Option<TokenMatch>, SwiftletError> {
Ok(
self.peek_probe(next_symbols).map(|probe| TokenMatch {
token: self.build_token(probe.start, probe.next_start, probe.line, next_symbols),
next_start: probe.next_start,
next_line: probe.next_line
})
)
}
}
#[derive(Debug)]
pub(crate) struct LexerConf {
sym_terminal_def: Arc<FxHashMap<Arc<Symbol>, Arc<TerminalDef>>>,
}
impl LexerConf {
pub fn new(terminals: Vec<Arc<TerminalDef>>) -> Self {
let it = terminals
.iter()
.map(|terminal_def| (terminal_def.name.clone(), terminal_def.clone()));
let sym_terminal_def = FxHashMap::from_iter(it);
Self {
sym_terminal_def: Arc::new(sym_terminal_def),
}
}
pub(crate) fn get_terminal_def(&self, name: &Arc<Symbol>) -> Option<&Arc<TerminalDef>> {
self.sym_terminal_def.get(name)
}
pub fn tokenize(&self, text: &str, ignore_terminals: Arc<[Arc<TerminalDef>]>) -> Tokenizer {
Tokenizer::new(
Arc::<str>::from(text),
self.sym_terminal_def.clone(),
ignore_terminals,
)
}
}
pub fn get_symbol(word: &str) -> Arc<Symbol> {
if word.chars().any(|x| x.is_ascii_lowercase()) {
return Arc::new(Symbol::NonTerminal(word.to_string()));
}
Arc::new(Symbol::Terminal(word.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol_helpers_work() {
let t = Symbol::Terminal("INT".to_string());
let nt = Symbol::NonTerminal("expr".to_string());
assert_eq!(t.as_str(), "INT");
assert_eq!(nt.get_value(), "expr".to_string());
assert!(t.is_terminal());
assert!(!nt.is_terminal());
assert!(nt.starts_with("ex"));
}
#[test]
fn regex_flag_default_is_expected() {
let f = RegexFlag::default();
assert!(!f.i);
assert!(!f.m);
assert!(!f.s);
assert!(f.u);
assert!(!f.x);
}
#[test]
fn terminal_def_with_string_and_regex_capture() {
let st = TerminalDef::with_string("PLUS", "+", 0);
let rg = TerminalDef::with_regex("INT", r"\d+", RegexFlag::default(), 0);
assert_eq!(st.get_name().as_ref().as_str(), "PLUS");
assert_eq!(st.capture("+1").unwrap().0, 1);
assert_eq!(rg.capture("123abc").unwrap().0, 3);
}
#[test]
fn token_new_sets_fields() {
let tk = Token::new(
Arc::<str>::from("xabc"),
1,
4,
2,
Arc::new(Symbol::Terminal("ID".to_string())),
);
assert_eq!(tk.word(), "abc");
assert_eq!(tk.start, 1);
assert_eq!(tk.end, 4);
assert_eq!(tk.line, 2);
}
#[test]
fn token_accessors_and_terminal_name_work() {
let tk = Token::new(
Arc::<str>::from("alpha beta"),
6,
10,
3,
Arc::new(Symbol::Terminal("WORD".to_string())),
);
assert_eq!(tk.start(), 6);
assert_eq!(tk.end(), 10);
assert_eq!(tk.line(), 3);
assert_eq!(tk.terminal(), "WORD".to_string());
assert_eq!(tk.word(), "beta");
}
#[test]
fn token_word_falls_back_to_source_for_invalid_bounds() {
let reversed = Token::new(
Arc::<str>::from("hello"),
4,
2,
0,
Arc::new(Symbol::Terminal("TEXT".to_string())),
);
let beyond_end = Token::new(
Arc::<str>::from("hello"),
0,
10,
0,
Arc::new(Symbol::Terminal("TEXT".to_string())),
);
assert_eq!(reversed.word(), "hello");
assert_eq!(beyond_end.word(), "hello");
}
#[test]
fn token_display_contains_core_metadata() {
let tk = Token::new(
Arc::<str>::from("sum"),
0,
3,
1,
Arc::new(Symbol::Terminal("IDENT".to_string())),
);
assert_eq!(
tk.to_string(),
"Token { word: sum, start: 0, end: 3, line: 1, terminal: Terminal(IDENT) }"
);
}
#[test]
fn token_equality_depends_on_word_line_and_terminal() {
let lhs = Token::new(
Arc::<str>::from("abc def"),
0,
3,
2,
Arc::new(Symbol::Terminal("IDENT".to_string())),
);
let same = Token::new(
Arc::<str>::from("abc xyz"),
0,
3,
2,
Arc::new(Symbol::Terminal("IDENT".to_string())),
);
let different_line = Token::new(
Arc::<str>::from("abc def"),
0,
3,
4,
Arc::new(Symbol::Terminal("IDENT".to_string())),
);
let different_terminal = Token::new(
Arc::<str>::from("abc def"),
0,
3,
2,
Arc::new(Symbol::Terminal("NUMBER".to_string())),
);
assert_eq!(lhs, same);
assert_ne!(lhs, different_line);
assert_ne!(lhs, different_terminal);
}
#[test]
fn get_symbol_classifies_terminal_vs_non_terminal() {
let nt = get_symbol("expr");
let t = get_symbol("INT");
assert_eq!(nt.as_ref().as_str(), "expr");
assert_eq!(t.as_ref().as_str(), "INT");
assert!(!nt.is_terminal());
assert!(t.is_terminal());
}
#[test]
fn symbol_terminal_starts_with_works() {
let t = Symbol::Terminal("INT".to_string());
assert!(t.starts_with("IN"));
assert!(!t.starts_with("int"));
}
#[test]
fn symbol_display_works_for_both_variants() {
let term = Symbol::Terminal("INT".to_string());
let non_term = Symbol::NonTerminal("expr".to_string());
assert_eq!(format!("{}", term), "INT");
assert_eq!(format!("{}", non_term), "expr");
}
#[test]
fn symbol_debug_formats_non_terminal_correctly() {
let non_term = Symbol::NonTerminal("expr".to_string());
assert_eq!(format!("{:?}", non_term), "NonTerminal(expr)");
}
#[test]
fn terminal_def_partial_eq_compares_name_and_value() {
let a = TerminalDef::with_string("PLUS", "+", 0);
let b = TerminalDef::with_string("PLUS", "+", 0);
let different_name = TerminalDef::with_string("MINUS", "+", 0);
let different_value = TerminalDef::with_string("PLUS", "-", 0);
assert_eq!(a, b);
assert_ne!(a, different_name);
assert_ne!(a, different_value);
}
#[test]
fn token_can_be_used_as_hash_map_key() {
use std::collections::HashSet;
let source = Arc::<str>::from("hello world");
let sym = Arc::new(Symbol::Terminal("WORD".to_string()));
let tok1 = Token::new(source.clone(), 0, 5, 0, sym.clone());
let tok2 = Token::new(source.clone(), 0, 5, 0, sym.clone());
let tok3 = Token::new(source.clone(), 6, 11, 0, sym.clone());
let mut set = HashSet::new();
set.insert(tok1);
set.insert(tok2); set.insert(tok3);
assert_eq!(set.len(), 2);
}
}