Struct ParseTable
pub struct ParseTable {
pub token_actions: TiVec<StateId, TiVec<TokenId, Action>>,
pub goto_actions: TiVec<StateId, TiVec<NontermId, Action>>,
pub rules: TiVec<RuleId, Rule>,
pub id_to_token: TiVec<TokenId, String>,
pub nonterm_names: TiVec<NontermId, String>,
pub display_names: TiVec<DisplayId, String>,
pub start_states: Vec<(String, StateId)>,
pub end_states: Vec<(String, StateId)>,
pub filtered_terminals: OnceLock<Vec<bool>>,
}Expand description
The LALR parse table. token_actions is a locked cross-phase contract:
per-state rows, dense state ids, carrying both shift and
reduce-lookahead terminals — accept-sets and accepts()
read through it; a shift-only row would silently shrink them. goto_actions
stays per-state addressable too: choices() re-merges both maps.
Loader-preserved order is canon — state ids, rule
order, __ANON ids pass through exactly as serialized, never renumbered.
Must be compiler/loader-built.
- The engine (
ParserState::feed_token) indexes these rows and pops the value stack by rule arity without re-checking bounds — the hot loop trusts the table’s internal consistency. - The fields are
pubfor the loader/conformance crate, but a hand-built table with a dangling action target, id, or rule arity panics inside the parse. - Build via
build_lalr/load_grammar, never by populating the fields directly.
Fields§
§token_actions: TiVec<StateId, TiVec<TokenId, Action>>[state][terminal id] — shift and reduce entries.
goto_actions: TiVec<StateId, TiVec<NontermId, Action>>[state][nonterminal id] — always shift-style goto targets.
rules: TiVec<RuleId, Rule>Reduce targets, referenced by index from action cells.
id_to_token: TiVec<TokenId, String>Terminal names by id — MUST agree with the lexer’s conf-order ids (one id space across lexer and table).
nonterm_names: TiVec<NontermId, String>§display_names: TiVec<DisplayId, String>Tree data names indexed by Rule::display_id.
start_states: Vec<(String, StateId)>Per declared start symbol.
end_states: Vec<(String, StateId)>§filtered_terminals: OnceLock<Vec<bool>>Lazily-computed per-token-id “always filter_out” flag for the reduce-time
fold (see Self::filtered_terminals) — grammar-constant, so computed once
and shared across every parse rather than rebuilt per fold. Construct empty
(OnceLock::new()); it fills on first use.
Implementations§
§impl ParseTable
impl ParseTable
pub fn token_id(&self, name: &str) -> Option<TokenId>
pub fn filtered_terminals(&self) -> &[bool]
pub fn filtered_terminals(&self) -> &[bool]
Per-token-id “always filter_out” flag for the reduce-time fold: true
iff the terminal is filter_out in EVERY rule it appears in (none keeping
it via keep_all_tokens). Those tokens the tree never materializes, so the
fold’s shift skips them (a zero-span marker) rather than on_leaf-ing a
value it would only drop. Grammar-constant — computed once, shared.
pub fn rpn_wire_safe(&self) -> bool
pub fn rpn_wire_safe(&self) -> bool
Whether the reduce-time RPN wire fast path (hyperlark-wasm’s
_parseFoldRpn) folds this grammar byte-identically to the arena+serialize
path. That sink emits a post-order stream — a token at shift, a rule record
at reduce — and skips filtered_terminals at
shift so a rule’s kept-child count is just the values it consumed. Two
grammar properties make that exact:
- no placeholder holes: a
[...]optional undermaybe_placeholdersinserts aNonebetween already-emitted children — an append-only buffer can’t. Checked against the actual builtRuleShapes (not the raw flag), so a grammar with no optionals is safe even undermaybe_placeholders=true(e.g. calc); and - a clean filter: no terminal is
filter_outin one rule yet kept in another. An unclean terminal isn’t always-filtered, so it is NOT skipped at shift, and a rule that drops it would miscount its children.
Both hold for typical grammars (json, calc). The general path serves the rest. Cheap and grammar-constant; the caller (a per-instance binding) caches the answer rather than recomputing per parse.
pub fn display_id(&self, name: &str) -> Option<DisplayId>
pub fn display_id(&self, name: &str) -> Option<DisplayId>
The data_id whose display name is name — the rule-name peer of
Self::token_id (filtered tree cursors take the numeric id). The
ambiguity sentinel names (_ambig/_iambig/_inter) occupy no
ParseTable::display_names slot and resolve to None; compare
Self::data_name, which synthesizes them on the way out.
pub fn is_splice_display(&self, data_id: DisplayId) -> bool
pub fn is_splice_display(&self, data_id: DisplayId) -> bool
Whether data_id names a _-transparent (splice) display — a rule
whose reduce produces no tree node of its own, its children splicing
into the parent instead.
Grammar-shape knowledge belongs to core (see RouteSet::build), and
bindings need it to REJECT such a display where a user names a rule to
hook or stream: there is no node to hand back, and seeding one into a
RouteSet collapses routing to all-hooked, which for a streaming
binding means emitting every node in the parse instead of none.
pub fn data_name(&self, data_id: DisplayId) -> &str
pub fn data_name(&self, data_id: DisplayId) -> &str
Tree data name for a data_id, resolving the ambiguity sentinels
(AMBIG_DATA_ID/IAMBIG_DATA_ID/INTER_DATA_ID) that carry no
ParseTable::display_names slot. Every consumer of Earley
ambiguity='explicit' trees MUST route through this rather than index
display_names directly (which would panic on a sentinel id).