Compare commits

...

23 commits

Author SHA1 Message Date
Will Lillis 2a8a17cf03 revert raw pointer hoisting to get_unchecked 2026-03-01 17:31:32 -05:00
Will Lillis 72718ca566 use bitset for coincident tokens 2026-03-01 15:41:46 -05:00
Will Lillis fd90c6e974 use simple bitvecs in TokenConflictMap, yields ~6% gain in tree-sitter-bash 2026-03-01 15:41:24 -05:00
Will Lillis 8982547112 use simple bitset for coincident token map, ~8% gain for tree-sitter bash 2026-03-01 15:39:08 -05:00
Will Lillis 7b3b9da95b more nfa stuff 2026-03-01 15:39:08 -05:00
Will Lillis 8f1ee0f13f optimize allocations around NfaStates 2026-03-01 15:39:08 -05:00
Will Lillis cf7d9e4d68 hashing trick, eliminate the expensive clone. Some pre-allocs as well 2026-03-01 15:39:08 -05:00
Will Lillis 3f353a21f9 more precompute, small gains 2026-03-01 15:39:08 -05:00
Will Lillis f50c7d9e64 perf: Use FxHasher over default hash implementation 2026-03-01 15:39:08 -05:00
Will Lillis b9f40095d7 cleanup symbol key using newtype method 2026-03-01 15:39:08 -05:00
Will Lillis bbd57f95d4 perf(generate): optimize variable_index_for_nfa_state 2026-03-01 15:39:08 -05:00
Will Lillis 45ad716e86 states conflict smart matrix accesses, small perf win 2026-03-01 15:39:08 -05:00
Will Lillis 4fd554f6af prealloc followup 2026-03-01 15:39:08 -05:00
Will Lillis 58d153efdb use bitflags for tokenconflictstatus 2026-03-01 15:39:08 -05:00
Will Lillis e5b6b40bb9 unchecked array accesses 2026-03-01 15:39:08 -05:00
Will Lillis d61d9e3715 pointers to map, small perf gain, scales w grammar 2026-03-01 15:39:08 -05:00
Will Lillis 58c7177277 Use symbol key for symbol, big boost for larger grammars 2026-03-01 15:39:08 -05:00
Will Lillis 62eb93fb0f nonterminal index, basically no change 2026-03-01 15:39:08 -05:00
Will Lillis afcc08c75e yet another precompute, also small/zero gains 2026-03-01 15:39:08 -05:00
Will Lillis e661efecee another precompute, much smaller/zero gains 2026-03-01 15:39:08 -05:00
Will Lillis f5df7c1a6f precompute some stuff, looks promising 2026-03-01 15:39:08 -05:00
Will Lillis 27035f6608
WIP: use u16 2026-03-01 15:38:18 -05:00
Will Lillis 0b269f537b
WIP: use linear Vec 2026-03-01 15:38:09 -05:00
22 changed files with 640 additions and 285 deletions

View file

@ -234,6 +234,7 @@ impl BitVec {
}
/// Word-level OR: self |= other. Returns true if any new bits were set.
#[inline]
pub fn insert_all(&mut self, other: &Self) -> bool {
let other_words = other.words_in_use();
if other_words == 0 {

View file

@ -6,12 +6,13 @@ mod item_set_builder;
mod minimize_parse_table;
mod token_conflicts;
use std::collections::{BTreeSet, HashMap};
use std::collections::BTreeSet;
pub use build_lex_table::LARGE_CHARACTER_RANGE_COUNT;
use build_parse_table::BuildTableResult;
pub use build_parse_table::ParseTableBuilderError;
use log::{debug, info};
use rustc_hash::FxHashMap;
use self::{
build_lex_table::build_lex_table,
@ -91,7 +92,7 @@ pub fn build_tables(
&token_conflict_map,
);
populate_external_lex_states(&mut parse_table, syntax_grammar);
mark_fragile_tokens(&mut parse_table, lexical_grammar, &token_conflict_map);
mark_fragile_tokens(&mut parse_table, &token_conflict_map);
if let Some(report_symbol_name) = report_symbol_name {
report_state_info(
@ -284,7 +285,7 @@ fn populate_used_symbols(
}
fn populate_external_lex_states(parse_table: &mut ParseTable, syntax_grammar: &SyntaxGrammar) {
let mut external_tokens_by_corresponding_internal_token = HashMap::new();
let mut external_tokens_by_corresponding_internal_token = FxHashMap::default();
for (i, external_token) in syntax_grammar.external_tokens.iter().enumerate() {
if let Some(symbol) = external_token.corresponding_internal_token {
external_tokens_by_corresponding_internal_token.insert(symbol.index, i);
@ -390,13 +391,12 @@ fn identify_keywords(
// If the word token was already valid in every state containing
// this keyword candidate, then substituting the word token won't
// introduce any new lexical conflicts.
if coincident_token_index
.states_with(*token, Symbol::terminal(other_index))
.iter()
.all(|state_id| {
parse_table.states[*state_id]
.terminal_entries
.contains_key(&word_token)
let other = Symbol::terminal(other_index);
if !coincident_token_index.contains(*token, other)
|| parse_table.states.iter().all(|state| {
!state.terminal_entries.contains_key(token)
|| !state.terminal_entries.contains_key(&other)
|| state.terminal_entries.contains_key(&word_token)
})
{
continue;
@ -425,25 +425,19 @@ fn identify_keywords(
.collect()
}
fn mark_fragile_tokens(
parse_table: &mut ParseTable,
lexical_grammar: &LexicalGrammar,
token_conflict_map: &TokenConflictMap,
) {
let n = lexical_grammar.variables.len();
let mut valid_tokens_mask = Vec::with_capacity(n);
fn mark_fragile_tokens(parse_table: &mut ParseTable, token_conflict_map: &TokenConflictMap) {
let mut valid_terminal_indices = Vec::new();
for state in &mut parse_table.states {
valid_tokens_mask.clear();
valid_tokens_mask.resize(n, false);
valid_terminal_indices.clear();
for token in state.terminal_entries.keys() {
if token.is_terminal() {
valid_tokens_mask[token.index] = true;
valid_terminal_indices.push(token.index);
}
}
for (token, entry) in &mut state.terminal_entries {
if token.is_terminal() {
for (i, is_valid) in valid_tokens_mask.iter().enumerate() {
if *is_valid && token_conflict_map.does_overlap(i, token.index) {
for &i in &valid_terminal_indices {
if token_conflict_map.does_overlap(i, token.index) {
entry.reusable = false;
break;
}

View file

@ -1,8 +1,10 @@
use std::{
collections::{HashMap, VecDeque, hash_map::Entry},
collections::{VecDeque, hash_map::Entry},
mem,
};
use rustc_hash::FxHashMap;
use log::debug;
use super::{coincident_tokens::CoincidentTokenIndex, token_conflicts::TokenConflictMap};
@ -139,7 +141,7 @@ struct LexTableBuilder<'a> {
cursor: NfaCursor<'a>,
table: LexTable,
state_queue: VecDeque<QueueEntry>,
state_ids_by_nfa_state_set: HashMap<(Vec<u32>, bool), usize>,
state_ids_by_nfa_state_set: FxHashMap<(Vec<u32>, bool), usize>,
}
impl<'a> LexTableBuilder<'a> {
@ -149,7 +151,7 @@ impl<'a> LexTableBuilder<'a> {
cursor: NfaCursor::new(&lexical_grammar.nfa, vec![]),
table: LexTable::default(),
state_queue: VecDeque::new(),
state_ids_by_nfa_state_set: HashMap::new(),
state_ids_by_nfa_state_set: FxHashMap::default(),
}
}
@ -197,9 +199,13 @@ impl<'a> LexTableBuilder<'a> {
fn add_state(&mut self, nfa_states: Vec<u32>, eof_valid: bool) -> (usize, bool) {
self.cursor.reset(nfa_states);
// Move cursor.state_ids out as the key instead of cloning it.
// The cursor is always rebuilt via reset() before its state is needed
// again, so leaving it empty here is safe.
let key_states = mem::take(&mut self.cursor.state_ids);
match self
.state_ids_by_nfa_state_set
.entry((self.cursor.state_ids.clone(), eof_valid))
.entry((key_states, eof_valid))
{
Entry::Occupied(o) => (*o.get(), false),
Entry::Vacant(v) => {
@ -234,8 +240,7 @@ impl<'a> LexTableBuilder<'a> {
completion = Some((id, prec));
}
let transitions = self.cursor.transitions();
let has_sep = self.cursor.transition_chars().any(|(_, sep)| sep);
let (transitions, has_sep) = self.cursor.transitions_and_any_sep();
// If EOF is a valid lookahead token, add a transition predicated on the null
// character that leads to the empty set of NFA states.
@ -285,20 +290,27 @@ fn check_token_conflicts(
token_conflict_map: &TokenConflictMap,
coincident_token_index: &CoincidentTokenIndex,
) -> bool {
let symbol = Symbol::terminal(i);
for existing_token in set_without_terminal.terminals() {
if token_conflict_map.does_conflict(i, existing_token.index)
|| token_conflict_map.does_match_prefix(i, existing_token.index)
{
return true;
}
if !coincident_token_index.contains(symbol, existing_token)
&& (token_conflict_map.does_overlap(existing_token.index, i)
|| token_conflict_map.does_overlap(i, existing_token.index))
{
let wpr = token_conflict_map.row_words;
let row_start = i * wpr;
let set_bits = set_without_terminal.terminal_bits_words();
// Does terminal i conflict with or match-prefix any terminal in the set?
let conflict_row = &token_conflict_map.conflict_or_prefix_bits[row_start..row_start + wpr];
for (&c, &s) in conflict_row.iter().zip(set_bits) {
if c & s != 0 {
return true;
}
}
// Does terminal i overlap (in either direction) with any non-coincident terminal in the set?
let overlap_row = &token_conflict_map.overlap_either_bits[row_start..row_start + wpr];
let coincident_row = &coincident_token_index.row_bits[row_start..row_start + wpr];
for ((&o, &s), &c) in overlap_row.iter().zip(set_bits).zip(coincident_row) {
if o & s & !c != 0 {
return true;
}
}
false
}
@ -345,7 +357,7 @@ fn merge_token_set(
fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
// Initially group the states by their accept action and their
// valid lookahead characters.
let mut state_ids_by_signature = HashMap::new();
let mut state_ids_by_signature = FxHashMap::default();
for (i, state) in table.states.iter().enumerate() {
let signature = (
i == 0,

View file

@ -1,12 +1,12 @@
use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
collections::{BTreeMap, BTreeSet, VecDeque},
hash::BuildHasherDefault,
};
use indexmap::{IndexMap, map::Entry};
use log::warn;
use rustc_hash::FxHasher;
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use serde::Serialize;
use thiserror::Error;
@ -56,12 +56,12 @@ struct ParseTableBuilder<'a> {
syntax_grammar: &'a SyntaxGrammar,
lexical_grammar: &'a LexicalGrammar,
variable_info: &'a [VariableInfo],
core_ids_by_core: HashMap<ParseItemSetCore<'a>, usize>,
core_ids_by_core: FxHashMap<ParseItemSetCore<'a>, usize>,
state_ids_by_item_set: IndexMap<ParseItemSet<'a>, ParseStateId, BuildHasherDefault<FxHasher>>,
parse_state_info_by_id: Vec<ParseStateInfo<'a>>,
parse_state_queue: VecDeque<ParseStateQueueEntry>,
non_terminal_extra_states: Vec<(Symbol, usize)>,
actual_conflicts: HashSet<Vec<Symbol>>,
actual_conflicts: FxHashSet<Vec<Symbol>>,
parse_table: ParseTable,
}
@ -252,7 +252,7 @@ impl<'a> ParseTableBuilder<'a> {
variable_info,
non_terminal_extra_states: Vec::new(),
state_ids_by_item_set: IndexMap::default(),
core_ids_by_core: HashMap::new(),
core_ids_by_core: FxHashMap::default(),
parse_state_info_by_id: Vec::new(),
parse_state_queue: VecDeque::new(),
actual_conflicts: syntax_grammar.expected_conflicts.iter().cloned().collect(),
@ -417,7 +417,7 @@ impl<'a> ParseTableBuilder<'a> {
let mut terminal_successors = BTreeMap::new();
let mut non_terminal_successors = BTreeMap::new();
let mut lookaheads_with_conflicts = TokenSet::new();
let mut reduction_infos = HashMap::<Symbol, ReductionInfo>::new();
let mut reduction_infos = FxHashMap::<Symbol, ReductionInfo>::default();
// Each item in the item set contributes to either or a Shift action or a Reduce
// action in this state.
@ -481,9 +481,9 @@ impl<'a> ParseTableBuilder<'a> {
} else {
ParseAction::Reduce {
symbol,
child_count: item.step_index as usize,
child_count: item.step_index as u16,
dynamic_precedence: item.production.dynamic_precedence,
production_id: self.get_production_id(item),
production_id: self.get_production_id(item) as u16,
}
};
@ -616,7 +616,7 @@ impl<'a> ParseTableBuilder<'a> {
None
}
})
.collect::<HashSet<_>>();
.collect::<FxHashSet<_>>();
let parent_symbol_names = parent_symbols
.iter()
.map(|&variable_index| {

View file

@ -3,11 +3,17 @@ use std::fmt;
use crate::{
grammars::LexicalGrammar,
rules::Symbol,
tables::{ParseStateId, ParseTable},
tables::ParseTable,
};
pub struct CoincidentTokenIndex<'a> {
entries: Vec<Vec<ParseStateId>>,
/// Flat bitset for fast `contains()` checks. Indexed as `a * n + b`
/// (both `(a,b)` and `(b,a)` bits are set, so no min/max normalization needed).
contains_bits: Vec<u64>,
/// Word-aligned per-row bitsets for vectorized intersection checks.
/// Row `a` spans `[a * row_words .. (a+1) * row_words]`.
/// Bit `b` is set iff tokens `a` and `b` are coincident in some parse state.
pub row_bits: Vec<u64>,
grammar: &'a LexicalGrammar,
n: usize,
}
@ -15,15 +21,17 @@ pub struct CoincidentTokenIndex<'a> {
impl<'a> CoincidentTokenIndex<'a> {
pub fn new(table: &ParseTable, lexical_grammar: &'a LexicalGrammar) -> Self {
let n = lexical_grammar.variables.len();
let row_words = n.div_ceil(64);
let mut result = Self {
n,
grammar: lexical_grammar,
entries: vec![Vec::new(); n * n],
contains_bits: vec![0u64; (n * n).div_ceil(64)],
row_bits: vec![0u64; n * row_words],
};
// Pre-collect terminal indices up front rather than continuously recomputing within the
// loop below.
let mut terminal_indices = Vec::new();
for (i, state) in table.states.iter().enumerate() {
for state in table.states.iter() {
terminal_indices.clear();
terminal_indices.extend(
state
@ -34,52 +42,45 @@ impl<'a> CoincidentTokenIndex<'a> {
);
for (j, &a) in terminal_indices.iter().enumerate() {
for &b in &terminal_indices[j..] {
let index = result.index(a, b);
if result.entries[index].last().copied() != Some(i) {
result.entries[index].push(i);
}
// Set both (a,b) and (b,a) bits so `contains()` needs
// no min/max normalization.
let ab = a * n + b;
result.contains_bits[ab / 64] |= 1u64 << (ab % 64);
let ba = b * n + a;
result.contains_bits[ba / 64] |= 1u64 << (ba % 64);
// Also populate the word-aligned row bitsets.
result.row_bits[a * row_words + b / 64] |= 1u64 << (b % 64);
result.row_bits[b * row_words + a / 64] |= 1u64 << (a % 64);
}
}
}
result
}
pub fn states_with(&self, a: Symbol, b: Symbol) -> &[ParseStateId] {
&self.entries[self.index(a.index, b.index)]
}
pub fn contains(&self, a: Symbol, b: Symbol) -> bool {
!self.entries[self.index(a.index, b.index)].is_empty()
}
#[must_use]
const fn index(&self, a: usize, b: usize) -> usize {
if a < b {
a * self.n + b
} else {
b * self.n + a
}
let bit_index = a.index * self.n + b.index;
self.contains_bits[bit_index / 64] & (1u64 << (bit_index % 64)) != 0
}
}
impl fmt::Debug for CoincidentTokenIndex<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "CoincidentTokenIndex {{")?;
writeln!(f, " entries: {{")?;
for i in 0..self.n {
writeln!(f, " {}: {{", self.grammar.variables[i].name)?;
let mut coincident = Vec::new();
for j in 0..self.n {
if self.contains(Symbol::terminal(i), Symbol::terminal(j)) {
coincident.push(&self.grammar.variables[j].name);
}
}
if !coincident.is_empty() {
writeln!(
f,
" {}: {:?},",
self.grammar.variables[j].name,
self.entries[self.index(i, j)].len()
" {}: {:?},",
self.grammar.variables[i].name, coincident
)?;
}
writeln!(f, " }},")?;
}
write!(f, " }},")?;
write!(f, "}}")?;
Ok(())
}

View file

@ -1,7 +1,6 @@
use std::{
collections::{HashMap, HashSet},
fmt,
};
use std::fmt;
use rustc_hash::{FxHashMap, FxHashSet};
use super::item::{ParseItem, ParseItemDisplay, ParseItemSet, ParseItemSetEntry, TokenSetDisplay};
use crate::{
@ -25,9 +24,9 @@ struct FollowSetInfo {
pub struct ParseItemSetBuilder<'a> {
syntax_grammar: &'a SyntaxGrammar,
lexical_grammar: &'a LexicalGrammar,
first_sets: HashMap<Symbol, TokenSet>,
reserved_first_sets: HashMap<Symbol, ReservedWordSetId>,
last_sets: HashMap<Symbol, TokenSet>,
first_sets: FxHashMap<Symbol, TokenSet>,
reserved_first_sets: FxHashMap<Symbol, ReservedWordSetId>,
last_sets: FxHashMap<Symbol, TokenSet>,
inlines: &'a InlinedProductionMap,
transitive_closure_additions: Vec<Vec<TransitiveClosureAddition<'a>>>,
}
@ -47,9 +46,9 @@ impl<'a> ParseItemSetBuilder<'a> {
let mut result = Self {
syntax_grammar,
lexical_grammar,
first_sets: HashMap::new(),
reserved_first_sets: HashMap::new(),
last_sets: HashMap::new(),
first_sets: FxHashMap::default(),
reserved_first_sets: FxHashMap::default(),
last_sets: FxHashMap::default(),
inlines,
transitive_closure_additions: vec![Vec::new(); syntax_grammar.variables.len()],
};
@ -92,7 +91,7 @@ impl<'a> ParseItemSetBuilder<'a> {
// Rather than computing these sets using recursion, we use an explicit stack
// called `symbols_to_process`.
let mut symbols_to_process = Vec::new();
let mut processed_non_terminals = HashSet::new();
let mut processed_non_terminals = FxHashSet::default();
for i in 0..syntax_grammar.variables.len() {
let symbol = Symbol::non_terminal(i);
let first_set = result.first_sets.entry(symbol).or_default();
@ -162,7 +161,7 @@ impl<'a> ParseItemSetBuilder<'a> {
// Rather than computing these additions recursively, we use an explicit stack.
let empty_lookaheads = TokenSet::new();
let mut stack = Vec::new();
let mut follow_set_info_by_non_terminal = HashMap::<usize, FollowSetInfo>::new();
let mut follow_set_info_by_non_terminal = FxHashMap::<usize, FollowSetInfo>::default();
for i in 0..syntax_grammar.variables.len() {
// First, build up a map whose keys are all of the non-terminals that can
// appear at the beginning of non-terminal `i`, and whose values store

View file

@ -1,7 +1,6 @@
use std::{
collections::{HashMap, HashSet},
mem,
};
use std::{cmp::Ordering, mem};
use rustc_hash::{FxHashMap, FxHashSet};
use log::debug;
@ -10,10 +9,70 @@ use crate::{
OptLevel,
dedup::split_state_id_groups,
grammars::{LexicalGrammar, SyntaxGrammar, VariableType},
rules::{AliasMap, Symbol, TokenSet},
rules::{AliasMap, Symbol, SymbolType, TokenSet},
tables::{GotoAction, ParseAction, ParseState, ParseStateId, ParseTable, ParseTableEntry},
};
/// Index into `SyntaxGrammar::variables`. All nonterminal `Symbol`s share
/// the same `kind`, so storing the index alone is sufficient for ordering.
type NonterminalIndex = usize;
/// A `Symbol` packed into a `u64` for O(1) sort-key comparison.
///
/// Layout: high 3 bits = `kind` discriminant (5 variants fit in 3 bits), low 61 bits = `index`.
/// This preserves `Symbol`'s derived `Ord` ordering (kind first, then index) as a single
/// integer comparison, and halves each entry's size vs storing a full `(Symbol, _)` tuple.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct SymbolKey(u64);
const KEY_TAG_SHIFT: u32 = 61;
const KEY_INDEX_MASK: u64 = (1u64 << KEY_TAG_SHIFT) - 1;
impl SymbolKey {
#[inline(always)]
fn new(sym: Symbol) -> Self {
debug_assert!(sym.index as u64 <= KEY_INDEX_MASK, "symbol index too large");
Self((sym.kind as u64) << KEY_TAG_SHIFT | sym.index as u64)
}
#[inline(always)]
fn symbol(self) -> Symbol {
let kind = match self.0 >> KEY_TAG_SHIFT {
0 => SymbolType::External,
1 => SymbolType::End,
2 => SymbolType::EndOfNonTerminalExtra,
3 => SymbolType::Terminal,
_ => SymbolType::NonTerminal,
};
Symbol {
kind,
index: self.index(),
}
}
#[inline(always)]
fn index(self) -> usize {
(self.0 & KEY_INDEX_MASK) as usize
}
#[inline(always)]
fn is_terminal(self) -> bool {
(self.0 >> KEY_TAG_SHIFT) == SymbolType::Terminal as u64
}
#[allow(dead_code)]
#[inline(always)]
fn is_non_terminal(self) -> bool {
(self.0 >> KEY_TAG_SHIFT) == SymbolType::NonTerminal as u64
}
}
impl From<Symbol> for SymbolKey {
fn from(sym: Symbol) -> Self {
Self::new(sym)
}
}
pub fn minimize_parse_table(
parse_table: &mut ParseTable,
syntax_grammar: &SyntaxGrammar,
@ -50,7 +109,7 @@ struct Minimizer<'a> {
impl Minimizer<'_> {
fn remove_unit_reductions(&mut self) {
let mut aliased_symbols = HashSet::new();
let mut aliased_symbols = FxHashSet::default();
for variable in &self.syntax_grammar.variables {
for production in &variable.productions {
for step in &production.steps {
@ -61,7 +120,7 @@ impl Minimizer<'_> {
}
}
let mut unit_reduction_symbols_by_state = HashMap::new();
let mut unit_reduction_symbols_by_state = FxHashMap::default();
for (i, state) in self.parse_table.states.iter().enumerate() {
let mut only_unit_reductions = true;
let mut unit_reduction_symbol = None;
@ -135,26 +194,101 @@ impl Minimizer<'_> {
// Initially group the states by their parse item set core.
let mut group_ids_by_state_id = Vec::with_capacity(self.parse_table.states.len());
let mut state_ids_by_group_id = vec![Vec::<ParseStateId>::new(); core_count];
// Pre-allocate for the maximum possible number of groups (one per state) to
// avoid reallocs as split_state_id_groups pushes new groups.
let mut state_ids_by_group_id = Vec::with_capacity(self.parse_table.states.len());
state_ids_by_group_id.resize(core_count, Vec::new());
for (i, state) in self.parse_table.states.iter().enumerate() {
state_ids_by_group_id[state.core_id].push(i);
group_ids_by_state_id.push(state.core_id);
}
// Precompute sorted terminal entry pointers for merge-join in states_conflict.
// entry_maps[state_id][i] = (symbol_key, *const ParseTableEntry)
// Keys are packed u64s (symbol_key) for single-instruction comparison.
// Storing a raw pointer avoids the IndexMap::get_index call in states_conflict.
//
// Safety invariant: parse_table.states are not mutated during the grouping phase,
// so all pointers remain valid for the lifetime of entry_maps.
let entry_maps: Vec<Vec<(SymbolKey, *const ParseTableEntry)>> = self
.parse_table
.states
.iter()
.map(|state| {
let mut entries: Vec<(SymbolKey, *const ParseTableEntry)> = state
.terminal_entries
.iter()
.map(|(sym, entry)| (SymbolKey::new(*sym), entry as *const ParseTableEntry))
.collect();
entries.sort_unstable_by_key(|&(key, _)| key);
entries
})
.collect();
split_state_id_groups(
&self.parse_table.states,
&mut state_ids_by_group_id,
&mut group_ids_by_state_id,
0,
|left, right, groups| self.states_conflict(left, right, groups),
|left, right, groups| self.states_conflict(left, right, groups, &entry_maps),
);
// Precompute per-state sorted shift actions and nonterminal goto actions.
// State actions are stable across loop iterations; only group assignments change.
// Keys are packed u64s (symbol_key) for single-instruction comparison.
let shift_maps: Vec<Vec<(SymbolKey, ParseStateId)>> = self
.parse_table
.states
.iter()
.map(|state| {
let mut shifts: Vec<(SymbolKey, ParseStateId)> = state
.terminal_entries
.iter()
.filter_map(|(sym, entry)| {
let action = entry.actions.last()?;
if let ParseAction::Shift { state: s, .. } = action {
Some((SymbolKey::new(*sym), *s))
} else {
None
}
})
.collect();
shifts.sort_unstable_by_key(|&(key, _)| key);
shifts
})
.collect();
// Store only the symbol index: all nonterminal entries share the same kind,
// so index alone is sufficient for sorting and comparison.
let nonterminal_maps: Vec<Vec<(NonterminalIndex, GotoAction)>> = self
.parse_table
.states
.iter()
.map(|state| {
let mut entries: Vec<(NonterminalIndex, GotoAction)> = state
.nonterminal_entries
.iter()
.map(|(sym, action)| (sym.index, *action))
.collect();
entries.sort_unstable_by_key(|&(idx, _)| idx);
entries
})
.collect();
while split_state_id_groups(
&self.parse_table.states,
&mut state_ids_by_group_id,
&mut group_ids_by_state_id,
0,
|left, right, groups| self.state_successors_differ(left, right, groups),
|left, right, groups| {
self.state_successors_differ(
left,
right,
groups,
&shift_maps,
&nonterminal_maps,
)
},
) {}
let error_group_index = state_ids_by_group_id
@ -203,35 +337,72 @@ impl Minimizer<'_> {
fn states_conflict(
&self,
left_state: &ParseState,
right_state: &ParseState,
state1: &ParseState,
state2: &ParseState,
group_ids_by_state_id: &[ParseStateId],
entry_maps: &[Vec<(SymbolKey, *const ParseTableEntry)>],
) -> bool {
for (token, left_entry) in &left_state.terminal_entries {
if let Some(right_entry) = right_state.terminal_entries.get(token) {
if self.entries_conflict(
left_state.id,
right_state.id,
token,
left_entry,
right_entry,
group_ids_by_state_id,
) {
return true;
let entries1 = &entry_maps[state1.id];
let entries2 = &entry_maps[state2.id];
let len1 = entries1.len();
let len2 = entries2.len();
let mut i = 0;
let mut j = 0;
while i < len1 || j < len2 {
// SAFETY: each branch only accesses entries1[i] when i < len1
// and entries2[j] when j < len2, both of which hold by construction.
let ord = if i < len1 && j < len2 {
unsafe { entries1.get_unchecked(i) }
.0
.cmp(&unsafe { entries2.get_unchecked(j) }.0)
} else if i < len1 {
Ordering::Less
} else {
Ordering::Greater
};
match ord {
Ordering::Equal => {
// SAFETY: Equal is only reachable when i < len1 && j < len2.
let e1 = unsafe { entries1.get_unchecked(i) };
let e2 = unsafe { entries2.get_unchecked(j) };
let token = e1.0.symbol();
// Safety: pointers were taken from the same parse_table.states that
// is not mutated during the grouping phase (see entry_maps comment).
let left_entry = unsafe { &*e1.1 };
let right_entry = unsafe { &*e2.1 };
if self.entries_conflict(
state1.id,
state2.id,
&token,
left_entry,
right_entry,
group_ids_by_state_id,
) {
return true;
}
i += 1;
j += 1;
}
Ordering::Less => {
// SAFETY: Less is only reachable when i < len1.
let e1 = unsafe { entries1.get_unchecked(i) };
let token = e1.0.symbol();
if self.token_conflicts(state1.id, state2.id, state2, entries2, token) {
return true;
}
i += 1;
}
Ordering::Greater => {
// SAFETY: Greater is only reachable when j < len2.
let e2 = unsafe { entries2.get_unchecked(j) };
let token = e2.0.symbol();
if self.token_conflicts(state1.id, state2.id, state1, entries1, token) {
return true;
}
j += 1;
}
} else if self.token_conflicts(left_state.id, right_state.id, right_state, *token) {
return true;
}
}
for token in right_state.terminal_entries.keys() {
if !left_state.terminal_entries.contains_key(token)
&& self.token_conflicts(left_state.id, right_state.id, left_state, *token)
{
return true;
}
}
false
}
@ -240,44 +411,69 @@ impl Minimizer<'_> {
state1: &ParseState,
state2: &ParseState,
group_ids_by_state_id: &[ParseStateId],
shift_maps: &[Vec<(SymbolKey, ParseStateId)>],
nonterminal_maps: &[Vec<(usize, GotoAction)>],
) -> bool {
for (token, entry1) in &state1.terminal_entries {
if let ParseAction::Shift { state: s1, .. } = entry1.actions.last().unwrap()
&& let Some(entry2) = state2.terminal_entries.get(token)
&& let ParseAction::Shift { state: s2, .. } = entry2.actions.last().unwrap()
{
let group1 = group_ids_by_state_id[*s1];
let group2 = group_ids_by_state_id[*s2];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.symbol_name(token),
);
return true;
let shifts1 = &shift_maps[state1.id];
let shifts2 = &shift_maps[state2.id];
let mut i = 0;
let mut j = 0;
while i < shifts1.len() && j < shifts2.len() {
// SAFETY: loop condition ensures i < shifts1.len() and j < shifts2.len().
let (k1, s1) = *unsafe { shifts1.get_unchecked(i) };
let (k2, s2) = *unsafe { shifts2.get_unchecked(j) };
match k1.cmp(&k2) {
Ordering::Less => i += 1,
Ordering::Greater => j += 1,
Ordering::Equal => {
let group1 = group_ids_by_state_id[s1];
let group2 = group_ids_by_state_id[s2];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.symbol_name(&k1.symbol()),
);
return true;
}
i += 1;
j += 1;
}
}
}
for (symbol, s1) in &state1.nonterminal_entries {
if let Some(s2) = state2.nonterminal_entries.get(symbol) {
match (s1, s2) {
(GotoAction::ShiftExtra, GotoAction::ShiftExtra) => {}
(GotoAction::Goto(s1), GotoAction::Goto(s2)) => {
let group1 = group_ids_by_state_id[*s1];
let group2 = group_ids_by_state_id[*s2];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.symbol_name(symbol),
);
return true;
let nonterms1 = &nonterminal_maps[state1.id];
let nonterms2 = &nonterminal_maps[state2.id];
let mut i = 0;
let mut j = 0;
while i < nonterms1.len() && j < nonterms2.len() {
// SAFETY: loop condition ensures i < nonterms1.len() and j < nonterms2.len().
let (idx1, s1) = *unsafe { nonterms1.get_unchecked(i) };
let (idx2, s2) = *unsafe { nonterms2.get_unchecked(j) };
match idx1.cmp(&idx2) {
Ordering::Less => i += 1,
Ordering::Greater => j += 1,
Ordering::Equal => {
match (s1, s2) {
(GotoAction::ShiftExtra, GotoAction::ShiftExtra) => {}
(GotoAction::Goto(s1), GotoAction::Goto(s2)) => {
let group1 = group_ids_by_state_id[s1];
let group2 = group_ids_by_state_id[s2];
if group1 != group2 {
debug!(
"split states {} {} - successors for {} are split: {s1} {s2}",
state1.id,
state2.id,
self.syntax_grammar.variables[idx1].name,
);
return true;
}
}
_ => return true,
}
_ => return true,
i += 1;
j += 1;
}
}
}
@ -346,6 +542,7 @@ impl Minimizer<'_> {
left_id: ParseStateId,
right_id: ParseStateId,
right_state: &ParseState,
right_entry_map: &[(SymbolKey, *const ParseTableEntry)],
new_token: Symbol,
) -> bool {
if new_token == Symbol::end_of_nonterminal_extra() {
@ -382,28 +579,36 @@ impl Minimizer<'_> {
return true;
}
// Hoist loop-invariant word-token comparisons.
let word_token = self.syntax_grammar.word_token;
let word_key = word_token.map(SymbolKey::new);
let new_token_key = SymbolKey::new(new_token);
let new_token_is_word = word_key == Some(new_token_key);
let new_token_is_keyword = word_token.is_some() && self.keywords.contains(&new_token);
// Do not add a token if it conflicts with an existing token.
for token in right_state.terminal_entries.keys().copied() {
if !token.is_terminal() {
// Iterate the pre-sorted Vec instead of IndexMap keys for cache-friendlier access.
for &(key, _) in right_entry_map {
if !key.is_terminal() {
continue;
}
if self.syntax_grammar.word_token == Some(token) && self.keywords.contains(&new_token) {
if new_token_is_keyword && word_key == Some(key) {
continue;
}
if self.syntax_grammar.word_token == Some(new_token) && self.keywords.contains(&token) {
if new_token_is_word && self.keywords.contains(&key.symbol()) {
continue;
}
if self
.token_conflict_map
.does_conflict(new_token.index, token.index)
.does_conflict(new_token.index, key.index())
{
debug!(
"split states {} {} - token {} conflicts with {}",
left_id,
right_id,
self.symbol_name(&new_token),
self.symbol_name(&token),
self.symbol_name(&key.symbol()),
);
return true;
}

View file

@ -1,4 +1,8 @@
use std::{cmp::Ordering, collections::HashSet, fmt};
use std::{cmp::Ordering, fmt};
use rustc_hash::FxHashSet;
use bitflags::bitflags;
use crate::{
build_tables::item::TokenSetDisplay,
@ -7,14 +11,22 @@ use crate::{
rules::TokenSet,
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct TokenConflictStatus {
matches_prefix: bool,
does_match_continuation: bool,
does_match_valid_continuation: bool,
does_match_separators: bool,
matches_same_string: bool,
matches_different_string: bool,
bitflags! {
/// Per-(i,j) token conflict status, packed into a single byte.
///
/// Using a `u8` bitfield instead of a struct of six `bool`s reduces the
/// status matrix from `n²·6` bytes to `n²` bytes (6× smaller). For a
/// grammar with 200 tokens that's 240 KB → 40 KB, so a full row fits
/// comfortably in L1 cache while scanning in `token_conflicts`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct TokenConflictStatus: u8 {
const MATCHES_PREFIX = 1 << 0;
const DOES_MATCH_CONTINUATION = 1 << 1;
const DOES_MATCH_VALID_CONT = 1 << 2;
const DOES_MATCH_SEPARATORS = 1 << 3;
const MATCHES_SAME_STRING = 1 << 4;
const MATCHES_DIFFERENT_STRING = 1 << 5;
}
}
pub struct TokenConflictMap<'a> {
@ -24,6 +36,13 @@ pub struct TokenConflictMap<'a> {
starting_chars_by_index: Vec<CharacterSet>,
following_chars_by_index: Vec<CharacterSet>,
grammar: &'a LexicalGrammar,
/// Per-row bitsets for fast batch conflict checks.
/// Row `i` spans `[i * row_words .. (i+1) * row_words]`.
/// Bit `j` is set iff `does_conflict(i, j) || does_match_prefix(i, j)`.
pub conflict_or_prefix_bits: Vec<u64>,
/// Bit `j` is set iff `does_overlap(i, j) || does_overlap(j, i)`.
pub overlap_either_bits: Vec<u64>,
pub row_words: usize,
}
impl<'a> TokenConflictMap<'a> {
@ -38,16 +57,50 @@ impl<'a> TokenConflictMap<'a> {
let starting_chars = get_starting_chars(&mut cursor, grammar);
let following_chars = get_following_chars(&starting_chars, &following_tokens);
// Pre-compute O(1) lookup: NFA state ID → owning variable index.
// Replaces repeated O(log n) binary searches in compute_conflict_status.
let nfa_state_to_var: Vec<usize> = (0..grammar.nfa.states.len())
.map(|id| grammar.variable_index_for_nfa_state(id as u32))
.collect();
let n = grammar.variables.len();
let mut status_matrix = vec![TokenConflictStatus::default(); n * n];
let mut status_matrix = vec![TokenConflictStatus::empty(); n * n];
for i in 0..grammar.variables.len() {
for j in 0..i {
let status = compute_conflict_status(&mut cursor, grammar, &following_chars, i, j);
let status =
compute_conflict_status(&mut cursor, grammar, &following_chars, &nfa_state_to_var, i, j);
status_matrix[matrix_index(n, i, j)] = status.0;
status_matrix[matrix_index(n, j, i)] = status.1;
}
}
// Precompute per-row bitsets for vectorized check_token_conflicts.
let row_words = n.div_ceil(64);
let conflict_mask = TokenConflictStatus::DOES_MATCH_VALID_CONT
| TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_SAME_STRING
| TokenConflictStatus::MATCHES_PREFIX;
let overlap_mask = TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_PREFIX
| TokenConflictStatus::MATCHES_SAME_STRING
| TokenConflictStatus::DOES_MATCH_CONTINUATION;
let mut conflict_or_prefix_bits = vec![0u64; n * row_words];
let mut overlap_either_bits = vec![0u64; n * row_words];
for i in 0..n {
let row_base = i * row_words;
for j in 0..n {
let entry_ij = status_matrix[matrix_index(n, i, j)];
if entry_ij.intersects(conflict_mask) {
conflict_or_prefix_bits[row_base + j / 64] |= 1u64 << (j % 64);
}
let entry_ji = status_matrix[matrix_index(n, j, i)];
if entry_ij.intersects(overlap_mask) || entry_ji.intersects(overlap_mask) {
overlap_either_bits[row_base + j / 64] |= 1u64 << (j % 64);
}
}
}
TokenConflictMap {
n,
status_matrix,
@ -55,6 +108,9 @@ impl<'a> TokenConflictMap<'a> {
starting_chars_by_index: starting_chars,
following_chars_by_index: following_chars,
grammar,
conflict_or_prefix_bits,
overlap_either_bits,
row_words,
}
}
@ -68,44 +124,54 @@ impl<'a> TokenConflictMap<'a> {
/// Does token `i` match any strings that token `j` does *not* match?
pub fn does_match_different_string(&self, i: usize, j: usize) -> bool {
self.status_matrix[matrix_index(self.n, i, j)].matches_different_string
self.status_matrix[matrix_index(self.n, i, j)]
.contains(TokenConflictStatus::MATCHES_DIFFERENT_STRING)
}
/// Does token `i` match any strings that token `j` also matches, where
/// token `i` is preferred over token `j`?
#[inline]
pub fn does_match_same_string(&self, i: usize, j: usize) -> bool {
self.status_matrix[matrix_index(self.n, i, j)].matches_same_string
self.status_matrix[matrix_index(self.n, i, j)]
.contains(TokenConflictStatus::MATCHES_SAME_STRING)
}
#[inline]
pub fn does_conflict(&self, i: usize, j: usize) -> bool {
let entry = &self.status_matrix[matrix_index(self.n, i, j)];
entry.does_match_valid_continuation
|| entry.does_match_separators
|| entry.matches_same_string
debug_assert!(i < self.n && j < self.n, "token indices out of bounds");
// Safety: i < n and j < n imply n*i+j < n*n == status_matrix.len().
let entry = unsafe { *self.status_matrix.get_unchecked(matrix_index(self.n, i, j)) };
entry.intersects(
TokenConflictStatus::DOES_MATCH_VALID_CONT
| TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_SAME_STRING,
)
}
/// Does token `i` match any strings that are *prefixes* of strings matched by `j`?
#[allow(dead_code)]
#[inline]
pub fn does_match_prefix(&self, i: usize, j: usize) -> bool {
self.status_matrix[matrix_index(self.n, i, j)].matches_prefix
self.status_matrix[matrix_index(self.n, i, j)]
.contains(TokenConflictStatus::MATCHES_PREFIX)
}
pub fn does_match_shorter_or_longer(&self, i: usize, j: usize) -> bool {
let entry = &self.status_matrix[matrix_index(self.n, i, j)];
let reverse_entry = &self.status_matrix[matrix_index(self.n, j, i)];
(entry.does_match_valid_continuation || entry.does_match_separators)
&& !reverse_entry.does_match_separators
let entry = self.status_matrix[matrix_index(self.n, i, j)];
let reverse_entry = self.status_matrix[matrix_index(self.n, j, i)];
entry.intersects(
TokenConflictStatus::DOES_MATCH_VALID_CONT | TokenConflictStatus::DOES_MATCH_SEPARATORS,
) && !reverse_entry.contains(TokenConflictStatus::DOES_MATCH_SEPARATORS)
}
#[inline]
pub fn does_overlap(&self, i: usize, j: usize) -> bool {
let status = &self.status_matrix[matrix_index(self.n, i, j)];
status.does_match_separators
|| status.matches_prefix
|| status.matches_same_string
|| status.does_match_continuation
self.status_matrix[matrix_index(self.n, i, j)].intersects(
TokenConflictStatus::DOES_MATCH_SEPARATORS
| TokenConflictStatus::MATCHES_PREFIX
| TokenConflictStatus::MATCHES_SAME_STRING
| TokenConflictStatus::DOES_MATCH_CONTINUATION,
)
}
pub fn prefer_token(grammar: &LexicalGrammar, left: (i32, usize), right: (i32, usize)) -> bool {
@ -241,51 +307,70 @@ fn get_following_chars(
.collect()
}
/// Hash a sorted slice of NFA state IDs to a single `u64` for use as a
/// visited-set key. The collision probability per pair is ~1/2^64, which is
/// negligible for any practical grammar.
#[inline]
fn hash_state_set(states: &[u32]) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = rustc_hash::FxHasher::default();
states.hash(&mut h);
h.finish()
}
fn compute_conflict_status(
cursor: &mut NfaCursor,
grammar: &LexicalGrammar,
following_chars: &[CharacterSet],
nfa_state_to_var: &[usize],
i: usize,
j: usize,
) -> (TokenConflictStatus, TokenConflictStatus) {
let mut visited_state_sets = HashSet::new();
let mut state_set_queue = vec![vec![
let mut visited_state_sets = FxHashSet::<u64>::default();
let mut state_set_queue = Vec::with_capacity(4);
state_set_queue.push(vec![
grammar.variables[i].start_state,
grammar.variables[j].start_state,
]];
]);
let mut result = (
TokenConflictStatus::default(),
TokenConflictStatus::default(),
TokenConflictStatus::empty(),
TokenConflictStatus::empty(),
);
while let Some(state_set) = state_set_queue.pop() {
let mut live_variable_indices = grammar.variable_indices_for_nfa_states(&state_set);
// If only one of the two tokens could possibly match from this state, then
// there is no reason to analyze any of its successors. Just record the fact
// that the token matches a string that the other token does not match.
let first_live_variable_index = live_variable_indices.next().unwrap();
if live_variable_indices.count() == 0 {
let first_live_variable_index = nfa_state_to_var[state_set[0] as usize];
if state_set
.iter()
.all(|&s| nfa_state_to_var[s as usize] == first_live_variable_index)
{
if first_live_variable_index == i {
result.0.matches_different_string = true;
result.0.insert(TokenConflictStatus::MATCHES_DIFFERENT_STRING);
} else {
result.1.matches_different_string = true;
result.1.insert(TokenConflictStatus::MATCHES_DIFFERENT_STRING);
}
continue;
}
// Don't pursue states where there's no potential for conflict.
cursor.reset(state_set);
let within_separator = cursor.transition_chars().any(|(_, sep)| sep);
// Compute lazily: most BFS states have no completions, so
// `within_separator` is never needed in those iterations.
let mut within_separator = None::<bool>;
// Examine each possible completed token in this state.
let mut completion = None;
for (id, precedence) in cursor.completions() {
if within_separator {
let sep = *within_separator
.get_or_insert_with(|| cursor.transition_chars().any(|(_, sep)| sep));
if sep {
if id == i {
result.0.does_match_separators = true;
result.0.insert(TokenConflictStatus::DOES_MATCH_SEPARATORS);
} else {
result.1.does_match_separators = true;
result.1.insert(TokenConflictStatus::DOES_MATCH_SEPARATORS);
}
}
@ -310,9 +395,9 @@ fn compute_conflict_status(
}
if preferred_id == i {
result.0.matches_same_string = true;
result.0.insert(TokenConflictStatus::MATCHES_SAME_STRING);
} else {
result.1.matches_same_string = true;
result.1.insert(TokenConflictStatus::MATCHES_SAME_STRING);
}
} else {
completion = Some((id, precedence));
@ -329,12 +414,18 @@ fn compute_conflict_status(
if let Some((completed_id, completed_precedence)) = completion {
let mut advanced_id = None;
let mut successor_contains_completed_id = false;
for variable_id in grammar.variable_indices_for_nfa_states(&transition.states) {
if variable_id == completed_id {
let mut prev_var = None;
for &state_id in &transition.states {
let var_id = nfa_state_to_var[state_id as usize];
if prev_var == Some(var_id) {
continue;
}
prev_var = Some(var_id);
if var_id == completed_id {
successor_contains_completed_id = true;
break;
}
advanced_id = Some(variable_id);
advanced_id = Some(var_id);
}
// Determine which action is preferred: matching the already complete
@ -345,29 +436,31 @@ fn compute_conflict_status(
&transition,
completed_id,
completed_precedence,
within_separator,
within_separator.unwrap_or(false),
) {
can_advance = true;
if advanced_id == i {
result.0.does_match_continuation = true;
result.0.insert(TokenConflictStatus::DOES_MATCH_CONTINUATION);
if transition.characters.does_intersect(&following_chars[j]) {
result.0.does_match_valid_continuation = true;
result.0.insert(TokenConflictStatus::DOES_MATCH_VALID_CONT);
}
} else {
result.1.does_match_continuation = true;
result.1.insert(TokenConflictStatus::DOES_MATCH_CONTINUATION);
if transition.characters.does_intersect(&following_chars[i]) {
result.1.does_match_valid_continuation = true;
result.1.insert(TokenConflictStatus::DOES_MATCH_VALID_CONT);
}
}
} else if completed_id == i {
result.0.matches_prefix = true;
result.0.insert(TokenConflictStatus::MATCHES_PREFIX);
} else {
result.1.matches_prefix = true;
result.1.insert(TokenConflictStatus::MATCHES_PREFIX);
}
}
}
if can_advance && visited_state_sets.insert(transition.states.clone()) {
if can_advance
&& visited_state_sets.insert(hash_state_set(&transition.states))
{
state_set_queue.push(transition.states);
}
}

View file

@ -8,6 +8,9 @@ pub fn split_state_id_groups<S>(
) -> bool {
let mut result = false;
// Use a flat bitset instead of Vec::contains for O(1) membership tests.
let mut is_split = vec![false; states.len()];
let mut group_id = start_group_id;
while group_id < state_ids_by_group_id.len() {
let state_ids = &state_ids_by_group_id[group_id];
@ -16,7 +19,7 @@ pub fn split_state_id_groups<S>(
let mut i = 0;
while i < state_ids.len() {
let left_state_id = state_ids[i];
if split_state_ids.contains(&left_state_id) {
if is_split[left_state_id] {
i += 1;
continue;
}
@ -28,14 +31,20 @@ pub fn split_state_id_groups<S>(
let mut j = i + 1;
while j < state_ids.len() {
let right_state_id = state_ids[j];
if split_state_ids.contains(&right_state_id) {
if is_split[right_state_id] {
j += 1;
continue;
}
let right_state = &states[right_state_id];
if should_split(left_state, right_state, group_ids_by_state_id) {
// Reserve on first split to cover the rest of the group in one
// allocation, avoiding all subsequent reallocs for this group.
if split_state_ids.is_empty() {
split_state_ids.reserve(state_ids.len());
}
split_state_ids.push(right_state_id);
is_split[right_state_id] = true;
}
j += 1;
@ -47,11 +56,12 @@ pub fn split_state_id_groups<S>(
// If any states were removed from the group, add them all as a new group.
if !split_state_ids.is_empty() {
result = true;
state_ids_by_group_id[group_id].retain(|i| !split_state_ids.contains(i));
state_ids_by_group_id[group_id].retain(|i| !is_split[*i]);
let new_group_id = state_ids_by_group_id.len();
for id in &split_state_ids {
group_ids_by_state_id[*id] = new_group_id;
is_split[*id] = false;
}
state_ids_by_group_id.push(split_state_ids);

View file

@ -229,10 +229,11 @@ impl LexicalGrammar {
}
pub fn variable_index_for_nfa_state(&self, state_id: u32) -> usize {
// `start_state` values are monotonically increasing (each variable's entry
// is the last NFA state allocated for it). Binary search for the first
// variable whose upper-boundary state >= state_id, i.e. the owner variable.
self.variables
.iter()
.position(|v| v.start_state >= state_id)
.unwrap()
.partition_point(|v| v.start_state < state_id)
}
}

View file

@ -482,6 +482,18 @@ impl<'a> NfaCursor<'a> {
Self::group_transitions(self.raw_transitions())
}
/// Like `transitions()` but also returns whether any raw NFA transition
/// is a separator — computed in the same pass, avoiding a second iteration
/// over `state_ids` for callers that need both.
pub fn transitions_and_any_sep(&self) -> (Vec<NfaTransition>, bool) {
let mut any_sep = false;
let result = Self::group_transitions(self.raw_transitions().map(|(chars, is_sep, prec, state)| {
any_sep |= is_sep;
(chars, is_sep, prec, state)
}));
(result, any_sep)
}
fn raw_transitions(&self) -> impl Iterator<Item = (&CharacterSet, bool, i32, u32)> {
self.state_ids.iter().filter_map(move |id| {
if let NfaState::Advance {
@ -502,13 +514,22 @@ impl<'a> NfaCursor<'a> {
iter: impl Iterator<Item = (&'b CharacterSet, bool, i32, u32)>,
) -> Vec<NfaTransition> {
let mut result = Vec::<NfaTransition>::new();
for (chars, is_sep, prec, state) in iter {
let mut chars = chars.clone();
// Reuse a single CharacterSet buffer across iterations to avoid one
// malloc per raw transition. `assign` refills it in-place; `mem::take`
// donates the allocation to a result entry when chars has a remainder.
let mut chars = CharacterSet::empty();
for (input_chars, is_sep, prec, state) in iter {
chars.assign(input_chars);
let mut i = 0;
while i < result.len() && !chars.is_empty() {
let intersection = result[i].characters.remove_intersection(&mut chars);
if !intersection.is_empty() {
let mut intersection_states = result[i].states.clone();
let chars_is_empty = result[i].characters.is_empty();
let mut intersection_states = if chars_is_empty {
mem::take(&mut result[i].states)
} else {
result[i].states.clone()
};
if let Err(j) = intersection_states.binary_search(&state) {
intersection_states.insert(j, state);
}
@ -518,18 +539,23 @@ impl<'a> NfaCursor<'a> {
precedence: max(result[i].precedence, prec),
states: intersection_states,
};
if result[i].characters.is_empty() {
if chars_is_empty {
result[i] = intersection_transition;
} else {
result.insert(i, intersection_transition);
i += 1;
// Push to the tail instead of inserting at i (which
// would be O(n)). After remove_intersection, the new
// `chars` (C') and `intersection` (I = A∩C) are
// disjoint, so when the loop later reaches I at the
// tail, remove_intersection(I, C'') will be a no-op.
// The final sort makes mid-loop ordering irrelevant.
result.push(intersection_transition);
}
}
i += 1;
}
if !chars.is_empty() {
result.push(NfaTransition {
characters: chars,
characters: mem::take(&mut chars),
precedence: prec,
states: vec![state],
is_separator: is_sep,
@ -546,7 +572,7 @@ impl<'a> NfaCursor<'a> {
{
let characters = mem::take(&mut result[j].characters);
result[j].characters = characters.add(&result[i].characters);
result.remove(i);
result.swap_remove(i);
i -= 1;
break;
}

View file

@ -1,4 +1,6 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::collections::{BTreeMap, BTreeSet};
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
use thiserror::Error;
@ -22,7 +24,7 @@ pub struct FieldInfo {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct VariableInfo {
pub fields: HashMap<String, FieldInfo>,
pub fields: FxHashMap<String, FieldInfo>,
pub children: FieldInfo,
pub children_without_fields: FieldInfo,
pub has_multi_step_production: bool,
@ -195,7 +197,7 @@ pub fn get_variable_info(
// immediately combined across all productions, but the child quantities must be
// recorded separately for each production.
for production in &variable.productions {
let mut production_field_quantities = HashMap::new();
let mut production_field_quantities = FxHashMap::default();
let mut production_children_quantity = ChildQuantity::zero();
let mut production_children_without_fields_quantity = ChildQuantity::zero();
let mut production_has_uninitialized_invisible_children = false;
@ -379,8 +381,8 @@ pub fn get_variable_info(
fn get_aliases_by_symbol(
syntax_grammar: &SyntaxGrammar,
default_aliases: &AliasMap,
) -> HashMap<Symbol, BTreeSet<Option<Alias>>> {
let mut aliases_by_symbol = HashMap::new();
) -> FxHashMap<Symbol, BTreeSet<Option<Alias>>> {
let mut aliases_by_symbol = FxHashMap::default();
for (symbol, alias) in default_aliases {
aliases_by_symbol.insert(*symbol, {
let mut aliases = BTreeSet::new();
@ -426,7 +428,7 @@ pub fn get_supertype_symbol_map(
let aliases_by_symbol = get_aliases_by_symbol(syntax_grammar, default_aliases);
let mut supertype_symbol_map = BTreeMap::new();
let mut symbols_by_alias = HashMap::new();
let mut symbols_by_alias = FxHashMap::default();
for (symbol, aliases) in &aliases_by_symbol {
for alias in aliases.iter().flatten() {
symbols_by_alias
@ -555,7 +557,7 @@ pub fn generate_node_types_json(
)
})
})
.collect::<HashSet<_>>();
.collect::<FxHashSet<_>>();
let mut subtype_map = Vec::new();
for (i, info) in variable_info.iter().enumerate() {
@ -1833,7 +1835,7 @@ mod tests {
}
)]
.into_iter()
.collect::<HashMap<_, _>>()
.collect::<FxHashMap<_, _>>()
);
assert_eq!(
@ -1853,7 +1855,7 @@ mod tests {
}
)]
.into_iter()
.collect::<HashMap<_, _>>()
.collect::<FxHashMap<_, _>>()
);
}
@ -1920,7 +1922,7 @@ mod tests {
}
)]
.into_iter()
.collect::<HashMap<_, _>>()
.collect::<FxHashMap<_, _>>()
);
}
@ -1981,7 +1983,7 @@ mod tests {
}
)]
.into_iter()
.collect::<HashMap<_, _>>()
.collect::<FxHashMap<_, _>>()
);
assert_eq!(
@ -2055,7 +2057,7 @@ mod tests {
}
)]
.into_iter()
.collect::<HashMap<_, _>>()
.collect::<FxHashMap<_, _>>()
);
}

View file

@ -1,4 +1,4 @@
use std::collections::HashSet;
use rustc_hash::FxHashSet;
use log::warn;
use regex::Regex;
@ -160,7 +160,7 @@ fn variable_is_used(
extras: &[Rule],
externals: &[Rule],
target_name: &str,
in_progress: &mut HashSet<String>,
in_progress: &mut FxHashSet<String>,
) -> bool {
let root = &grammar_rules.first().unwrap().0;
if target_name == root {
@ -241,7 +241,7 @@ pub(crate) fn parse_grammar(input: &str) -> ParseGrammarResult<InputGrammar> {
.map(|(n, r)| Ok((n, parse_rule(serde_json::from_value(r)?, false)?)))
.collect::<ParseGrammarResult<Vec<_>>>()?;
let mut in_progress = HashSet::new();
let mut in_progress = FxHashSet::default();
for (name, rule) in &rules {
if grammar_json.word.as_ref().is_none_or(|w| w != name)

View file

@ -8,7 +8,7 @@ mod process_inlines;
use std::{
cmp::Ordering,
collections::{BTreeSet, HashMap, HashSet, hash_map},
collections::{BTreeSet, hash_map},
mem,
};
@ -16,6 +16,7 @@ pub use expand_tokens::ExpandTokensError;
pub use extract_tokens::ExtractTokensError;
pub use flatten_grammar::FlattenGrammarError;
use indexmap::IndexMap;
use rustc_hash::{FxHashMap, FxHashSet};
pub use intern_symbols::InternSymbolsError;
pub use process_inlines::ProcessInlinesError;
use serde::Serialize;
@ -256,7 +257,7 @@ fn validate_precedences(grammar: &InputGrammar) -> ValidatePrecedenceResult<()>
fn validate(
rule_name: &str,
rule: &Rule,
names: &HashSet<&String>,
names: &FxHashSet<&String>,
) -> ValidatePrecedenceResult<()> {
match rule {
Rule::Repeat(rule) => validate(rule_name, rule, names),
@ -281,7 +282,7 @@ fn validate_precedences(grammar: &InputGrammar) -> ValidatePrecedenceResult<()>
// For any two precedence names `a` and `b`, if `a` comes before `b`
// in some list, then it cannot come *after* `b` in any list.
let mut pairs = HashMap::new();
let mut pairs = FxHashMap::default();
for list in &grammar.precedence_orderings {
for (i, mut entry1) in list.iter().enumerate() {
for mut entry2 in list.iter().skip(i + 1) {
@ -321,7 +322,7 @@ fn validate_precedences(grammar: &InputGrammar) -> ValidatePrecedenceResult<()>
None
}
})
.collect::<HashSet<&String>>();
.collect::<FxHashSet<&String>>();
for variable in &grammar.variables {
validate(&variable.name, &variable.rule, &precedence_names)?;
}

View file

@ -1,4 +1,6 @@
use std::{collections::HashMap, mem};
use std::mem;
use rustc_hash::FxHashMap;
use super::ExtractedSyntaxGrammar;
use crate::{
@ -11,7 +13,7 @@ struct Expander {
repeat_count_in_variable: usize,
preceding_symbol_count: usize,
auxiliary_variables: Vec<Variable>,
existing_repeats: HashMap<Rule, Symbol>,
existing_repeats: FxHashMap<Rule, Symbol>,
}
impl Expander {
@ -106,7 +108,7 @@ pub(super) fn expand_repeats(mut grammar: ExtractedSyntaxGrammar) -> ExtractedSy
repeat_count_in_variable: 0,
preceding_symbol_count: grammar.variables.len(),
auxiliary_variables: Vec::new(),
existing_repeats: HashMap::new(),
existing_repeats: FxHashMap::default(),
};
for (i, variable) in grammar.variables.iter_mut().enumerate() {

View file

@ -1,4 +1,4 @@
use std::collections::HashMap;
use rustc_hash::FxHashMap;
use serde::Serialize;
use thiserror::Error;
@ -89,7 +89,7 @@ pub(super) fn extract_tokens(
// will need to have their indices decremented.
let mut variables = Vec::with_capacity(grammar.variables.len());
let mut symbol_replacer = SymbolReplacer {
replacements: HashMap::new(),
replacements: FxHashMap::default(),
};
for (i, variable) in grammar.variables.into_iter().enumerate() {
if let Rule::Symbol(Symbol {
@ -268,7 +268,7 @@ struct TokenExtractor {
}
struct SymbolReplacer {
replacements: HashMap<usize, usize>,
replacements: FxHashMap<usize, usize>,
}
impl TokenExtractor {

View file

@ -1,4 +1,4 @@
use std::collections::HashMap;
use rustc_hash::FxHashMap;
use serde::Serialize;
use thiserror::Error;
@ -31,7 +31,7 @@ unless they are used only as the grammar's start rule.
struct RuleFlattener {
production: Production,
reserved_word_set_ids: HashMap<String, ReservedWordSetId>,
reserved_word_set_ids: FxHashMap<String, ReservedWordSetId>,
precedence_stack: Vec<Precedence>,
associativity_stack: Vec<Associativity>,
reserved_word_stack: Vec<ReservedWordSetId>,
@ -40,7 +40,7 @@ struct RuleFlattener {
}
impl RuleFlattener {
const fn new(reserved_word_set_ids: HashMap<String, ReservedWordSetId>) -> Self {
const fn new(reserved_word_set_ids: FxHashMap<String, ReservedWordSetId>) -> Self {
Self {
production: Production {
steps: Vec::new(),
@ -248,7 +248,7 @@ fn symbol_is_used(variables: &[SyntaxVariable], symbol: Symbol) -> bool {
pub(super) fn flatten_grammar(
grammar: ExtractedSyntaxGrammar,
) -> FlattenGrammarResult<SyntaxGrammar> {
let mut reserved_word_set_ids_by_name = HashMap::new();
let mut reserved_word_set_ids_by_name = FxHashMap::default();
for (ix, set) in grammar.reserved_word_sets.iter().enumerate() {
reserved_word_set_ids_by_name.insert(set.name.clone(), ReservedWordSetId(ix));
}
@ -307,7 +307,7 @@ mod tests {
#[test]
fn test_flatten_grammar() {
let mut flattener = RuleFlattener::new(HashMap::default());
let mut flattener = RuleFlattener::new(FxHashMap::default());
let result = flattener
.flatten_variable(Variable {
name: "test".to_string(),
@ -368,7 +368,7 @@ mod tests {
#[test]
fn test_flatten_grammar_with_maximum_dynamic_precedence() {
let mut flattener = RuleFlattener::new(HashMap::default());
let mut flattener = RuleFlattener::new(FxHashMap::default());
let result = flattener
.flatten_variable(Variable {
name: "test".to_string(),
@ -424,7 +424,7 @@ mod tests {
#[test]
fn test_flatten_grammar_with_final_precedence() {
let mut flattener = RuleFlattener::new(HashMap::default());
let mut flattener = RuleFlattener::new(FxHashMap::default());
let result = flattener
.flatten_variable(Variable {
name: "test".to_string(),
@ -474,7 +474,7 @@ mod tests {
#[test]
fn test_flatten_grammar_with_field_names() {
let mut flattener = RuleFlattener::new(HashMap::default());
let mut flattener = RuleFlattener::new(FxHashMap::default());
let result = flattener
.flatten_variable(Variable {
name: "test".to_string(),

View file

@ -1,4 +1,4 @@
use std::collections::HashMap;
use rustc_hash::FxHashMap;
use serde::Serialize;
use thiserror::Error;
@ -19,7 +19,7 @@ struct ProductionStepId {
}
struct InlinedProductionMapBuilder {
production_indices_by_step_id: HashMap<ProductionStepId, Vec<usize>>,
production_indices_by_step_id: FxHashMap<ProductionStepId, Vec<usize>>,
productions: Vec<Production>,
}
@ -227,7 +227,7 @@ pub(super) fn process_inlines(
Ok(InlinedProductionMapBuilder {
productions: Vec::new(),
production_indices_by_step_id: HashMap::new(),
production_indices_by_step_id: FxHashMap::default(),
}
.build(grammar))
}

View file

@ -1,5 +1,4 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{LazyLock, Mutex},
};
@ -9,6 +8,7 @@ use rquickjs::{
Context, Ctx, Function, Module, Object, Runtime, Type, Value,
loader::{FileResolver, ScriptLoader},
};
use rustc_hash::FxHashMap;
use super::{IoError, JSError, JSResult};
@ -49,8 +49,8 @@ fn format_js_exception(v: Value) -> JSError {
}
}
static FILE_CACHE: LazyLock<Mutex<HashMap<String, String>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static FILE_CACHE: LazyLock<Mutex<FxHashMap<String, String>>> =
LazyLock::new(|| Mutex::new(FxHashMap::default()));
#[rquickjs::function]
fn load_file(path: String) -> rquickjs::Result<String> {

View file

@ -1,10 +1,12 @@
use std::{
cmp,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
collections::{BTreeMap, BTreeSet},
fmt::Write,
mem::swap,
};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::LANGUAGE_VERSION;
use indoc::indoc;
use serde::Serialize;
@ -90,11 +92,11 @@ struct Generator {
syntax_grammar: SyntaxGrammar,
lexical_grammar: LexicalGrammar,
default_aliases: AliasMap,
symbol_order: HashMap<Symbol, usize>,
symbol_ids: HashMap<Symbol, String>,
alias_ids: HashMap<Alias, String>,
symbol_order: FxHashMap<Symbol, usize>,
symbol_ids: FxHashMap<Symbol, String>,
alias_ids: FxHashMap<Alias, String>,
unique_aliases: Vec<Alias>,
symbol_map: HashMap<Symbol, Symbol>,
symbol_map: FxHashMap<Symbol, Symbol>,
reserved_word_sets: Vec<TokenSet>,
reserved_word_set_ids_by_parse_state: Vec<usize>,
field_names: Vec<String>,
@ -187,7 +189,7 @@ impl Generator {
}
fn init(&mut self) {
let mut symbol_identifiers = HashSet::new();
let mut symbol_identifiers = FxHashSet::default();
for i in 0..self.parse_table.symbols.len() {
self.assign_symbol_id(self.parse_table.symbols[i], &mut symbol_identifiers);
}
@ -196,7 +198,7 @@ impl Generator {
self.symbol_ids[&Symbol::end()].clone(),
);
self.symbol_map = HashMap::new();
self.symbol_map = FxHashMap::default();
for symbol in &self.parse_table.symbols {
let mut mapping = symbol;
@ -610,7 +612,7 @@ impl Generator {
}
fn add_non_terminal_alias_map(&mut self) {
let mut alias_ids_by_symbol = HashMap::new();
let mut alias_ids_by_symbol = FxHashMap::default();
for variable in &self.syntax_grammar.variables {
for production in &variable.productions {
for step in &production.steps {
@ -666,7 +668,7 @@ impl Generator {
"static const TSStateId ts_primary_state_ids[STATE_COUNT] = {{"
);
indent!(self);
let mut first_state_for_each_core_id = HashMap::new();
let mut first_state_for_each_core_id = FxHashMap::default();
for (idx, state) in self.parse_table.states.iter().enumerate() {
let primary_state = first_state_for_each_core_id
.entry(state.core_id)
@ -1287,7 +1289,7 @@ impl Generator {
}
fn add_parse_table(&mut self) -> RenderResult<()> {
let mut parse_table_entries = HashMap::new();
let mut parse_table_entries = FxHashMap::default();
let mut next_parse_action_list_index = 0;
// Parse action lists zero is for the default value, when a symbol is not valid.
@ -1368,7 +1370,7 @@ impl Generator {
.len()
.saturating_sub(self.large_state_count),
);
let mut symbols_by_value = HashMap::<(usize, SymbolType), Vec<Symbol>>::new();
let mut symbols_by_value = FxHashMap::<(usize, SymbolType), Vec<Symbol>>::default();
for state in self.parse_table.states.iter().skip(self.large_state_count) {
small_state_indices.push(next_table_index);
symbols_by_value.clear();
@ -1689,7 +1691,7 @@ impl Generator {
fn get_parse_action_list_id(
entry: &ParseTableEntry,
parse_table_entries: &mut HashMap<ParseTableEntry, usize>,
parse_table_entries: &mut FxHashMap<ParseTableEntry, usize>,
next_parse_action_list_index: &mut usize,
) -> usize {
if let Some(&index) = parse_table_entries.get(entry) {
@ -1724,7 +1726,7 @@ impl Generator {
)
}
fn assign_symbol_id(&mut self, symbol: Symbol, used_identifiers: &mut HashSet<String>) {
fn assign_symbol_id(&mut self, symbol: Symbol, used_identifiers: &mut FxHashSet<String>) {
let mut id;
if symbol == Symbol::end() {
id = "ts_builtin_sym_end".to_string();

View file

@ -362,6 +362,12 @@ impl TokenSet {
self.terminal_bits.get(index).unwrap_or(false)
}
/// Raw u64 word slice backing the terminal bitset.
#[inline]
pub fn terminal_bits_words(&self) -> &[u64] {
self.terminal_bits.as_slice()
}
pub fn insert(&mut self, other: Symbol) {
let vec = match other.kind {
SymbolType::NonTerminal => panic!("Cannot store non-terminals in a TokenSet"),

View file

@ -24,9 +24,9 @@ pub enum ParseAction {
Recover,
Reduce {
symbol: Symbol,
child_count: usize,
child_count: u16,
dynamic_precedence: i32,
production_id: ProductionInfoId,
production_id: u16,
},
}