perf(generate): inline single-action parse table entries

`ParseTableEntry` held a `Vec<ParseAction>` per entry. `ActionList` keeps up
to one action inline and spills to a Vec only for conflict entries.

Reduces walltime by >20%, peak rss by ~30%.
This commit is contained in:
Will Lillis 2026-08-09 11:51:45 -05:00
parent 2a81995058
commit b14413e4f4
4 changed files with 100 additions and 16 deletions

View file

@ -30,7 +30,7 @@ use crate::{
node_types::VariableInfo,
rules::{AliasMap, Symbol, SymbolType, TokenSet},
strpool::StrPool,
tables::{LexTable, ParseAction, ParseTable, ParseTableEntry},
tables::{ActionList, LexTable, ParseAction, ParseTable, ParseTableEntry},
};
pub struct Tables {
@ -204,7 +204,7 @@ fn populate_error_state(
let recover_entry = ParseTableEntry {
reusable: false,
actions: vec![ParseAction::Recover],
actions: ActionList::One(ParseAction::Recover),
};
// Exclude from the error-recovery state any token that conflicts with one of

View file

@ -21,7 +21,7 @@ use crate::{
rules::{Associativity, Precedence, Symbol, SymbolType, TokenSet},
strpool::StrPool,
tables::{
FieldLocation, GotoAction, ParseAction, ParseState, ParseStateId, ParseTable,
ActionList, FieldLocation, GotoAction, ParseAction, ParseState, ParseStateId, ParseTable,
ParseTableEntry, ProductionInfo, ProductionInfoId,
},
};
@ -688,10 +688,10 @@ impl<'a> ParseTableBuilder<'a> {
.entry(*terminal)
.or_insert(ParseTableEntry {
reusable: true,
actions: vec![ParseAction::Shift {
actions: ActionList::One(ParseAction::Shift {
state: *state_id,
is_repetition: false,
}],
}),
});
}
@ -709,7 +709,7 @@ impl<'a> ParseTableBuilder<'a> {
.entry(*extra_token)
.or_insert(ParseTableEntry {
reusable: true,
actions: vec![ParseAction::ShiftExtra],
actions: ActionList::One(ParseAction::ShiftExtra),
});
}
}
@ -840,7 +840,7 @@ impl<'a> ParseTableBuilder<'a> {
}
if shift_is_more && !shift_is_less {
entry.actions.drain(0..entry.actions.len() - 1);
entry.actions.keep_last();
}
// If the REDUCE actions have higher precedence, remove the SHIFT action.
else if shift_is_less && !shift_is_more {
@ -861,7 +861,7 @@ impl<'a> ParseTableBuilder<'a> {
(false, false, true)
)
{
entry.actions.drain(0..entry.actions.len() - 1);
entry.actions.keep_last();
} else {
entry.actions.pop();
conflicting_items.retain(|item| item.is_done());
@ -884,7 +884,7 @@ impl<'a> ParseTableBuilder<'a> {
conflicting_items.retain(|item| item.is_done());
}
(false, false, true) => {
entry.actions.drain(0..entry.actions.len() - 1);
entry.actions.keep_last();
}
_ => {}
}

View file

@ -20,8 +20,8 @@ use super::{
rules::{AliasMap, Symbol, SymbolType, TokenSet},
strpool::{StrId, StrPool},
tables::{
AdvanceAction, FieldLocation, GotoAction, LexState, LexTable, ParseAction, ParseTable,
ParseTableEntry,
ActionList, AdvanceAction, FieldLocation, GotoAction, LexState, LexTable, ParseAction,
ParseTable, ParseTableEntry,
},
};
@ -1311,7 +1311,7 @@ impl Generator {
// Parse action lists zero is for the default value, when a symbol is not valid.
Self::get_parse_action_list_id(
&ParseTableEntry {
actions: Vec::new(),
actions: ActionList::Empty,
reusable: false,
},
&mut parse_table_entries,
@ -1501,9 +1501,9 @@ impl Generator {
entry.actions.len(),
entry.reusable
);
for action in entry.actions {
for action in &entry.actions {
add!(self, " ");
match action {
match *action {
ParseAction::Accept => add!(self, " ACCEPT_INPUT()"),
ParseAction::Recover => add!(self, "RECOVER()"),
ParseAction::ShiftExtra => add!(self, "SHIFT_EXTRA()"),

View file

@ -38,9 +38,93 @@ pub enum GotoAction {
ShiftExtra,
}
/// Action list for one parse table entry. Holds entries with <= 1 action inline.
#[derive(Clone, Debug, Default, Eq)]
pub enum ActionList {
#[default]
Empty,
One(ParseAction),
Many(Vec<ParseAction>),
}
impl ActionList {
pub fn push(&mut self, action: ParseAction) {
match self {
Self::Empty => *self = Self::One(action),
Self::One(first) => *self = Self::Many(vec![*first, action]),
Self::Many(actions) => actions.push(action),
}
}
pub fn pop(&mut self) -> Option<ParseAction> {
match self {
Self::Empty => None,
Self::One(action) => {
let action = *action;
*self = Self::Empty;
Some(action)
}
Self::Many(actions) => actions.pop(),
}
}
pub fn clear(&mut self) {
*self = Self::Empty;
}
pub fn keep_last(&mut self) {
if let Self::Many(actions) = self
&& let Some(&last) = actions.last()
{
*self = Self::One(last);
}
}
}
impl std::ops::Deref for ActionList {
type Target = [ParseAction];
fn deref(&self) -> &[ParseAction] {
match self {
Self::Empty => &[],
Self::One(action) => std::slice::from_ref(action),
Self::Many(actions) => actions,
}
}
}
impl std::ops::DerefMut for ActionList {
fn deref_mut(&mut self) -> &mut [ParseAction] {
match self {
Self::Empty => &mut [],
Self::One(action) => std::slice::from_mut(action),
Self::Many(actions) => actions,
}
}
}
impl PartialEq for ActionList {
fn eq(&self, other: &Self) -> bool {
self[..] == other[..]
}
}
impl std::hash::Hash for ActionList {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self[..].hash(state);
}
}
impl<'a> IntoIterator for &'a ActionList {
type Item = &'a ParseAction;
type IntoIter = std::slice::Iter<'a, ParseAction>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ParseTableEntry {
pub actions: Vec<ParseAction>,
pub actions: ActionList,
pub reusable: bool,
}
@ -100,7 +184,7 @@ impl ParseTableEntry {
pub const fn new() -> Self {
Self {
reusable: true,
actions: Vec::new(),
actions: ActionList::Empty,
}
}
}