tamer: parse: Introduce mutable Context

This resolves the performance issues caused by Rust's failure to elide the
ElementStack (ArrayVec) memcpys on move.

Since XIRF is invoked tens of millions of times in some cases for larger
systems, prior to this change, failure to optimize away moves for XIRF
resulted in tens of millions of memcpys.  This resulted in linking of one
program going from 1s -> ~15s.  This change reduces it to ~2.5s with the
wip-xmlo-xir-reader flag on, with the extra time coming from elsewhere (the
subject of future changes).

In particular, this change introduces a new mutable reference to
`ParseState::parse_token`, which is a reference to a `Context` owned by the
caller (e.g. `Parser`).  In the case of XIRF, this means that
`Parser<flat::State, _>` will own the `ElementStack`/`ArrayVec` instead of
`flat::State`; this allows the latter to remain pure and benefit from Rust's
move optimizations, without sacrificing the otherwise-pure implementation.

ParseStates that do not need a mutable context can use `NoContext` and
remain pure.

DEV-12024
main
Mike Gerwitz 2022-04-04 21:50:47 -04:00
parent 1a04d99f15
commit e77bdaf19a
6 changed files with 274 additions and 83 deletions

View File

@ -57,6 +57,14 @@
// For `Try` and `FromResidual`,
// allowing us to write our own `?`-compatible types.
#![feature(try_trait_v2)]
// Used primarily for convenience,
// rather than having to create type constructors as type aliases that are
// not associated with a trait.
// However,
// this also allows for the associated type default to be overridden by
// the implementer,
// in which case this feature's only substitute is a type parameter.
#![feature(associated_type_defaults)]
// We build docs for private items.
#![allow(rustdoc::private_intra_doc_links)]

View File

@ -20,7 +20,10 @@
use super::{SymAttrs, XmloError};
use crate::{
obj::xmlo::{Dim, SymDtype, SymType},
parse::{self, ParseState, Transition, TransitionResult, Transitionable},
parse::{
self, EmptyContext, NoContext, ParseState, Transition,
TransitionResult, Transitionable,
},
span::Span,
sym::{st::*, SymbolId},
xir::{attr::Attr, flat::Object as Xirf, QName},
@ -127,7 +130,7 @@ qname_const! {
}
/// A parser capable of being composed with [`XmloReaderState`].
pub trait XmloState = ParseState<Token = Xirf>
pub trait XmloState = ParseState<Token = Xirf, Context = EmptyContext>
where
<Self as ParseState>::Error: Into<XmloError>,
<Self as ParseState>::Object: Into<XmloEvent>;
@ -166,7 +169,11 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
type Object = XmloEvent;
type Error = XmloError;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
ctx: NoContext,
) -> TransitionResult<Self> {
use XmloReaderState::*;
match (self, tok) {
@ -206,7 +213,9 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
// TOOD: It'd be nice to augment errors with the symbol table
// span as well (e.g. "while processing symbol table at <loc>").
(Symtable(span, ss), tok) => ss.delegate(span, tok, Symtable),
(Symtable(span, ss), tok) => {
ss.delegate(ctx, tok, |ss| Symtable(span, ss))
}
(SymDepsExpected, Xirf::Open(QN_SYM_DEPS, span, _)) => {
Transition(SymDeps(span, SD::default())).incomplete()
@ -218,7 +227,9 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
Transition(FragmentsExpected).incomplete()
}
(SymDeps(span, sd), tok) => sd.delegate(span, tok, SymDeps),
(SymDeps(span, sd), tok) => {
sd.delegate(ctx, tok, |sd| SymDeps(span, sd))
}
(FragmentsExpected, Xirf::Open(QN_FRAGMENTS, span, _)) => {
Transition(Fragments(span, SF::default())).incomplete()
@ -229,7 +240,9 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
Xirf::Close(None | Some(QN_FRAGMENTS), span, _),
) if sf.is_accepting() => Transition(Eoh).ok(XmloEvent::Eoh(span)),
(Fragments(span, sf), tok) => sf.delegate(span, tok, Fragments),
(Fragments(span, sf), tok) => {
sf.delegate(ctx, tok, |sf| Fragments(span, sf))
}
(Eoh, Xirf::Close(Some(QN_PACKAGE), ..)) => {
Transition(Done).incomplete()
@ -273,7 +286,11 @@ impl ParseState for SymtableState {
type Object = (SymbolId, SymAttrs, Span);
type Error = XmloError;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
_: NoContext,
) -> TransitionResult<Self> {
use SymtableState::*;
match (self, tok) {
@ -530,7 +547,11 @@ impl ParseState for SymDepsState {
type Object = XmloEvent;
type Error = XmloError;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
_: NoContext,
) -> TransitionResult<Self> {
use SymDepsState::*;
match (self, tok) {
@ -611,7 +632,11 @@ impl ParseState for FragmentsState {
type Object = XmloEvent;
type Error = XmloError;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
_: NoContext,
) -> TransitionResult<Self> {
use FragmentsState::*;
match (self, tok) {

View File

@ -27,7 +27,7 @@ use std::fmt::Debug;
use std::hint::unreachable_unchecked;
use std::iter::{self, Empty};
use std::mem::take;
use std::ops::{ControlFlow, FromResidual, Try};
use std::ops::{ControlFlow, Deref, DerefMut, FromResidual, Try};
use std::{convert::Infallible, error::Error, fmt::Display};
/// Result of applying a [`Token`] to a [`ParseState`],
@ -129,6 +129,8 @@ pub trait ParseState: Default + PartialEq + Eq + Debug {
/// Errors specific to this set of states.
type Error: Debug + Error + PartialEq + Eq;
type Context: Default + Debug = EmptyContext;
/// Construct a parser.
///
/// Whether this method is helpful or provides any clarity depends on
@ -154,7 +156,24 @@ pub trait ParseState: Default + PartialEq + Eq + Debug {
/// which in turn makes it easier to compose parsers
/// (which conceptually involves stitching together state
/// machines).
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self>;
///
/// Since a [`ParseState`] produces a new version of itself with each
/// invocation,
/// it is functionally pure.
/// Generally,
/// Rust/LLVM are able to optimize moves into direct assignments.
/// However,
/// there are circumstances where this is _not_ the case,
/// in which case [`Context`] can be used to provide a mutable context
/// owned by the caller (e.g. [`Parser`]) to store additional
/// information that is not subject to Rust's move semantics.
/// If this is not necessary,
/// see [`NoContext`].
fn parse_token(
self,
tok: Self::Token,
ctx: &mut Self::Context,
) -> TransitionResult<Self>;
/// Whether the current state represents an accepting state.
///
@ -192,22 +211,24 @@ pub trait ParseState: Default + PartialEq + Eq + Debug {
/// use [`Self::delegate_lookahead`] instead.
///
/// _TODO: More documentation once this is finalized._
fn delegate<C, SP>(
fn delegate<SP, C>(
self,
context: C,
mut context: C,
tok: <Self as ParseState>::Token,
into: impl FnOnce(C, Self) -> SP,
into: impl FnOnce(Self) -> SP,
) -> TransitionResult<SP>
where
Self: StitchableParseState<SP>,
C: AsMut<<Self as ParseState>::Context>,
{
use ParseStatus::{Dead, Incomplete, Object as Obj};
let (Transition(newst), result) = self.parse_token(tok).into();
let (Transition(newst), result) =
self.parse_token(tok, context.as_mut()).into();
// This does not use `delegate_lookahead` so that we can have
// `into: impl FnOnce` instead of `Fn`.
Transition(into(context, newst)).result(match result {
Transition(into(newst)).result(match result {
Ok(Incomplete) => Ok(Incomplete),
Ok(Obj(obj)) => Ok(Obj(obj.into())),
Ok(Dead(tok)) => Ok(Dead(tok)),
@ -223,33 +244,137 @@ pub trait ParseState: Default + PartialEq + Eq + Debug {
/// rather than simply proxying [`ParseStatus::Dead`].
///
/// _TODO: More documentation once this is finalized._
fn delegate_lookahead<C, SP>(
fn delegate_lookahead<SP, C>(
self,
context: C,
mut context: C,
tok: <Self as ParseState>::Token,
into: impl FnOnce(C, Self) -> SP,
lookahead: impl FnOnce(
C,
Self,
<Self as ParseState>::Token,
) -> TransitionResult<SP>,
) -> TransitionResult<SP>
into: impl FnOnce(Self) -> SP,
) -> ControlFlow<TransitionResult<SP>, (Self, <Self as ParseState>::Token, C)>
where
Self: StitchableParseState<SP>,
C: AsMut<<Self as ParseState>::Context>,
{
use ControlFlow::*;
use ParseStatus::{Dead, Incomplete, Object as Obj};
let (Transition(newst), result) = self.parse_token(tok).into();
// NB: Rust/LLVM are generally able to elide these moves into direct
// assignments,
// but sometimes this does not work
// (e.g. XIRF's use of `ArrayVec`).
// If your [`ParseState`] has a lot of `memcpy`s or other
// performance issues,
// move heavy objects into `context`.
let (Transition(newst), result) =
self.parse_token(tok, context.as_mut()).into();
match result {
Ok(Incomplete) => Transition(into(context, newst)).incomplete(),
Ok(Obj(obj)) => Transition(into(context, newst)).ok(obj.into()),
Ok(Dead(tok)) => lookahead(context, newst, tok),
Err(e) => Transition(into(context, newst)).err(e),
Ok(Incomplete) => Break(Transition(into(newst)).incomplete()),
Ok(Obj(obj)) => Break(Transition(into(newst)).ok(obj.into())),
Ok(Dead(tok)) => Continue((newst, tok, context)),
Err(e) => Break(Transition(into(newst)).err(e)),
}
}
}
/// Empty [`Context`] for [`ParseState`]s with pure functional
/// implementations with no mutable state.
///
/// Using this value means that a [`ParseState`] does not require a
/// context.
/// All [`Context`]s implement [`AsMut<EmptyContext>`](AsMut),
/// and so all pure [`ParseState`]s have contexts compatible with every
/// other parser for composition
/// (provided that the other invariants in [`StitchableParseState`] are
/// met).
///
/// This can be clearly represented in function signatures using
/// [`EmptyContext`].
#[derive(Debug, PartialEq, Eq, Default)]
pub struct EmptyContext;
impl AsMut<EmptyContext> for EmptyContext {
fn as_mut(&mut self) -> &mut EmptyContext {
self
}
}
/// A [`ParseState`] does not require any mutable [`Context`].
///
/// A [`ParseState`] using this context is pure
/// (has no mutable state),
/// returning a new version of itself on each state change.
///
/// This type is intended to be self-documenting:
/// `_: EmptyContext` is nicer to readers than `_: &mut EmptyContext`.
///
/// See [`EmptyContext`] for more information.
pub type NoContext<'a> = &'a mut EmptyContext;
/// Mutable context for [`ParseState`].
///
/// [`ParseState`]s are immutable and pure---they
/// are invoked via [`ParseState::parse_token`] and return a new version
/// of themselves representing their new state.
/// Rust/LLVM are generally able to elide intermediate values and moves,
/// optimizing these parsers away into assignments.
///
/// However,
/// there are circumstances where moves may not be elided and may retain
/// their `memcpy` equivalents.
/// To work around this,
/// [`ParseState::parse_token`] accepts a mutable [`Context`] reference
/// which is held by the parent [`Parser`],
/// which can be mutated in-place without worrying about Rust's move
/// semantics.
///
/// Plainly: you should only use this if you have to.
/// This was added because certain parsers may be invoked millions of times
/// for each individual token in systems with many source packages,
/// which may otherwise result in millions of `memcpy`s.
///
/// When composing two [`ParseState`]s `A<B, C>`,
/// a [`Context<B, C>`](Context) must be contravariant over `B` and~`C`.
/// Concretely,
/// this means that [`AsMut<B::Context>`](AsMut) and
/// [`AsMut<C::Context>`](AsMut) must be implemented for `A::Context`.
/// This almost certainly means that `A::Context` is a product type.
/// Consequently,
/// a single [`Parser`] is able to hold a composite [`Context`] in a
/// single memory location.
///
/// [`Context<T>`](Context) implements [`Deref<T>`](Deref) for convenience.
///
/// If your [`ParseState`] does not require a mutable [`Context`],
/// see [`NoContext`].
#[derive(Debug, Default)]
pub struct Context<T: Debug + Default>(T, EmptyContext);
impl<T: Debug + Default> AsMut<EmptyContext> for Context<T> {
fn as_mut(&mut self) -> &mut EmptyContext {
&mut self.1
}
}
impl<T: Debug + Default> Deref for Context<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Debug + Default> DerefMut for Context<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Debug + Default> From<T> for Context<T> {
fn from(x: T) -> Self {
Context(x, EmptyContext)
}
}
/// Result of applying a [`Token`] to a [`ParseState`].
///
/// This is used by [`ParseState::parse_token`];
@ -382,6 +507,20 @@ impl<S: ParseState> FromResidual<Result<Infallible, TransitionResult<S>>>
}
}
impl<S: ParseState> FromResidual<ControlFlow<TransitionResult<S>, Infallible>>
for TransitionResult<S>
{
fn from_residual(
residual: ControlFlow<TransitionResult<S>, Infallible>,
) -> Self {
match residual {
ControlFlow::Break(result) => result,
// SAFETY: Infallible, so cannot hit.
ControlFlow::Continue(_) => unsafe { unreachable_unchecked() },
}
}
}
/// An object able to be used as data for a state [`Transition`].
///
/// This flips the usual order of things:
@ -439,6 +578,7 @@ pub struct Parser<S: ParseState, I: TokenStream<S::Token>> {
toks: I,
state: S,
last_span: Option<Span>,
ctx: S::Context,
}
impl<S: ParseState, I: TokenStream<S::Token>> Parser<S, I> {
@ -490,7 +630,7 @@ impl<S: ParseState, I: TokenStream<S::Token>> Parser<S, I> {
let result;
TransitionResult(Transition(self.state), result) =
take(&mut self.state).parse_token(tok);
take(&mut self.state).parse_token(tok, &mut self.ctx);
use ParseStatus::*;
match result {
@ -731,6 +871,7 @@ impl<S: ParseState, I: TokenStream<S::Token>> From<I> for Parser<S, I> {
toks,
state: Default::default(),
last_span: None,
ctx: Default::default(),
}
}
}
@ -856,7 +997,11 @@ pub mod test {
type Object = TestToken;
type Error = EchoStateError;
fn parse_token(self, tok: TestToken) -> TransitionResult<Self> {
fn parse_token(
self,
tok: TestToken,
_: NoContext,
) -> TransitionResult<Self> {
match tok {
TestToken::Comment(..) => Transition(Self::Done).ok(tok),
TestToken::Close(..) => {

View File

@ -20,7 +20,7 @@
//! Parse XIR attribute [`TokenStream`][super::super::TokenStream]s.
use crate::{
parse::{ParseState, Transition, TransitionResult},
parse::{NoContext, ParseState, Transition, TransitionResult},
span::Span,
xir::{QName, Token as XirToken},
};
@ -47,7 +47,11 @@ impl ParseState for AttrParseState {
type Object = Attr;
type Error = AttrParseError;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
_: NoContext,
) -> TransitionResult<Self> {
use AttrParseState::{Empty, Name};
match (self, tok) {
@ -121,7 +125,7 @@ mod test {
use super::*;
use crate::{
convert::ExpectInto,
parse::{ParseStatus, Parsed},
parse::{EmptyContext, ParseStatus, Parsed},
sym::GlobalSymbolIntern,
};
@ -143,7 +147,7 @@ mod test {
Transition(AttrParseState::default()),
Ok(ParseStatus::Dead(tok.clone()))
),
sut.parse_token(tok).into()
sut.parse_token(tok, &mut EmptyContext).into()
);
}
@ -175,12 +179,12 @@ mod test {
// This token indicates that we're expecting a value to come next in
// the token stream.
let TransitionResult(Transition(sut), result) =
sut.parse_token(XirToken::AttrName(attr, S));
sut.parse_token(XirToken::AttrName(attr, S), &mut EmptyContext);
assert_eq!(result, Ok(ParseStatus::Incomplete));
// But we provide something else unexpected.
let TransitionResult(Transition(sut), result) =
sut.parse_token(XirToken::Close(None, S2));
sut.parse_token(XirToken::Close(None, S2), &mut EmptyContext);
assert_eq!(
result,
Err(AttrParseError::AttrValueExpected(
@ -200,8 +204,8 @@ mod test {
// Rather than checking for that state,
// let's actually attempt a recovery.
let recover = "value".intern();
let TransitionResult(Transition(sut), result) =
sut.parse_token(XirToken::AttrValue(recover, S2));
let TransitionResult(Transition(sut), result) = sut
.parse_token(XirToken::AttrValue(recover, S2), &mut EmptyContext);
assert_eq!(
result,
Ok(ParseStatus::Object(Attr::new(attr, recover, (S, S2)))),

View File

@ -44,7 +44,8 @@ use super::{
};
use crate::{
parse::{
self, ParseState, ParsedResult, Token, Transition, TransitionResult,
self, Context, ParseState, ParsedResult, Token, Transition,
TransitionResult,
},
span::Span,
sym::SymbolId,
@ -163,9 +164,11 @@ impl From<Attr> for Object {
}
/// XIRF-compatible attribute parser.
pub trait FlatAttrParseState = ParseState<Token = XirToken, Object = Attr>
where
<Self as ParseState>::Error: Into<StateError>;
pub trait FlatAttrParseState<const MAX_DEPTH: usize> =
ParseState<Token = XirToken, Object = Attr>
where
<Self as ParseState>::Error: Into<StateError>,
StateContext<MAX_DEPTH>: AsMut<<Self as ParseState>::Context>;
/// Stack of element [`QName`] and [`Span`] pairs,
/// representing the current level of nesting.
@ -180,31 +183,36 @@ type ElementStack<const MAX_DEPTH: usize> = ArrayVec<(QName, Span), MAX_DEPTH>;
#[derive(Debug, Default, PartialEq, Eq)]
pub enum State<const MAX_DEPTH: usize, SA = AttrParseState>
where
SA: FlatAttrParseState,
SA: FlatAttrParseState<MAX_DEPTH>,
{
/// Document parsing has not yet begun.
#[default]
PreRoot,
/// Parsing nodes.
NodeExpected(ElementStack<MAX_DEPTH>),
NodeExpected,
/// Delegating to attribute parser.
AttrExpected(ElementStack<MAX_DEPTH>, SA),
AttrExpected(SA),
/// End of document has been reached.
Done,
}
pub type StateContext<const MAX_DEPTH: usize> =
Context<ElementStack<MAX_DEPTH>>;
impl<const MAX_DEPTH: usize, SA> ParseState for State<MAX_DEPTH, SA>
where
SA: FlatAttrParseState,
SA: FlatAttrParseState<MAX_DEPTH>,
{
type Token = XirToken;
type Object = Object;
type Error = StateError;
type Context = StateContext<MAX_DEPTH>;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
stack: &mut Self::Context,
) -> TransitionResult<Self> {
use State::{AttrExpected, Done, NodeExpected, PreRoot};
match (self, tok) {
@ -213,22 +221,20 @@ where
Transition(st).ok(Object::Comment(sym, span))
}
(PreRoot, tok @ XirToken::Open(..)) => {
Self::parse_node(Default::default(), tok)
}
(PreRoot, tok @ XirToken::Open(..)) => Self::parse_node(tok, stack),
(PreRoot, tok) => {
Transition(PreRoot).err(StateError::RootOpenExpected(tok))
}
(NodeExpected(stack), tok) => Self::parse_node(stack, tok),
(NodeExpected, tok) => Self::parse_node(tok, stack),
(AttrExpected(stack, sa), tok) => sa.delegate_lookahead(
stack,
tok,
AttrExpected,
|stack, _, lookahead| Self::parse_node(stack, lookahead),
),
(AttrExpected(sa), tok) => {
let (_sa, lookahead, stack) =
sa.delegate_lookahead(stack, tok, AttrExpected)?;
Self::parse_node(lookahead, stack)
}
(Done, tok) => Transition(Done).dead(tok),
}
@ -250,24 +256,22 @@ where
impl<const MAX_DEPTH: usize, SA> State<MAX_DEPTH, SA>
where
SA: FlatAttrParseState,
SA: FlatAttrParseState<MAX_DEPTH>,
{
/// Parse a token while in a state expecting a node.
fn parse_node(
mut stack: ElementStack<MAX_DEPTH>,
tok: <Self as ParseState>::Token,
stack: &mut ElementStack<MAX_DEPTH>,
) -> TransitionResult<Self> {
use Object::*;
use State::{AttrExpected, Done, NodeExpected};
match tok {
XirToken::Open(qname, span) if stack.len() == MAX_DEPTH => {
Transition(NodeExpected(stack)).err(
StateError::MaxDepthExceeded {
open: (qname, span),
max: Depth(MAX_DEPTH),
},
)
Transition(NodeExpected).err(StateError::MaxDepthExceeded {
open: (qname, span),
max: Depth(MAX_DEPTH),
})
}
XirToken::Open(qname, span) => {
@ -275,7 +279,7 @@ where
stack.push((qname, span));
// Delegate to the attribute parser until it is complete.
Transition(AttrExpected(stack, SA::default())).ok(Open(
Transition(AttrExpected(SA::default())).ok(Open(
qname,
span,
Depth(depth),
@ -289,7 +293,7 @@ where
(Some(qname), Some((open_qname, open_span)))
if qname != open_qname =>
{
Transition(NodeExpected(stack)).err(
Transition(NodeExpected).err(
StateError::UnbalancedTag {
open: (open_qname, open_span),
close: (qname, close_span),
@ -307,7 +311,7 @@ where
(..) => {
let depth = stack.len();
Transition(NodeExpected(stack)).ok(Close(
Transition(NodeExpected).ok(Close(
close_oqname,
close_span,
Depth(depth),
@ -317,16 +321,16 @@ where
}
XirToken::Comment(sym, span) => {
Transition(NodeExpected(stack)).ok(Comment(sym, span))
Transition(NodeExpected).ok(Comment(sym, span))
}
XirToken::Text(sym, span) => {
Transition(NodeExpected(stack)).ok(Text(sym, span))
Transition(NodeExpected).ok(Text(sym, span))
}
XirToken::CData(sym, span) => {
Transition(NodeExpected(stack)).ok(CData(sym, span))
Transition(NodeExpected).ok(CData(sym, span))
}
XirToken::Whitespace(ws, span) => {
Transition(NodeExpected(stack)).ok(Whitespace(ws, span))
Transition(NodeExpected).ok(Whitespace(ws, span))
}
// We should transition to `State::Attr` before encountering any

View File

@ -180,8 +180,8 @@ use super::{
use crate::{
parse::{
self, ParseError, ParseResult, ParseState, ParseStatus, ParsedResult,
Transition, TransitionResult,
self, EmptyContext, NoContext, ParseError, ParseResult, ParseState,
ParseStatus, ParsedResult, Transition, TransitionResult,
},
span::Span,
sym::SymbolId,
@ -504,7 +504,8 @@ where
pub trait StackAttrParseState = ParseState<Token = XirToken, Object = Attr>
where
<Self as ParseState>::Error: Into<StackError>;
<Self as ParseState>::Error: Into<StackError>,
EmptyContext: AsMut<<Self as ParseState>::Context>;
impl<SA: StackAttrParseState> Default for Stack<SA> {
fn default() -> Self {
@ -517,7 +518,11 @@ impl<SA: StackAttrParseState> ParseState for Stack<SA> {
type Object = Tree;
type Error = StackError;
fn parse_token(self, tok: Self::Token) -> TransitionResult<Self> {
fn parse_token(
self,
tok: Self::Token,
ctx: NoContext,
) -> TransitionResult<Self> {
use Stack::*;
match (self, tok) {
@ -543,7 +548,7 @@ impl<SA: StackAttrParseState> ParseState for Stack<SA> {
// Attribute parsing.
(AttrState(estack, attrs, sa), tok) => {
use ParseStatus::*;
match sa.parse_token(tok).into() {
match sa.parse_token(tok, ctx.as_mut()).into() {
(Transition(sa), Ok(Incomplete)) => {
Transition(AttrState(estack, attrs, sa)).incomplete()
}
@ -553,7 +558,7 @@ impl<SA: StackAttrParseState> ParseState for Stack<SA> {
}
(_, Ok(Dead(lookahead))) => {
BuddingElement(estack.consume_attrs(attrs))
.parse_token(lookahead)
.parse_token(lookahead, ctx)
}
(Transition(sa), Err(x)) => {
Transition(AttrState(estack, attrs, sa)).err(x.into())