tamer: parse::util::expand::StitchExpansion: Began transition from ParseState to method

My initial plan with expansion was to wrap a `PasteState` in another that
unwraps `Expansion` and converts into a `Dead` state, so that existing
`TransitionResult` stitching methods (`delegate`, specifically) could be
used.

But the desire to use that existing method was primarily because stitching
was a complex operation that was abstracted away _as part of the `delegate`
method_, which made writing new ones verbose and difficult.  Thus began the
previous commits to begin to move that responsibility elsewhere so that it
could be more composable.

This continues with that, introducing a new trait that will culminate in the
removal of a wrapping `ParseState` in favor of a stitching method.  The old
`StitchableExpansionState` is still used for tests, which demonstrates that
the boilerplate problem still exists despite improvements made here  These
will become more generalized in the future as I have time (and the
functional aspects of the code more formalized too, now that they're taking
shape).

The benefit of this is that we avoid having to warp our abstractions in ways
that don't make sense (use of a dead state transition) just to satisfy
existing APIs.  It also means that we do not need the boilerplate of a
`ParseState` any time we want to introduce this type of
stitching/delegation.  It also means that those methods can eventually be
extracted into more general traits in the future as well.

Ultimately, though, the two would have accomplished the same thing.  But the
difference is most emphasized in the _parent_---the actual stitching still
has to take place for desugaring in the attribute parser, and I'd like for
that abstraction to still be in terms of expansion.  But if I utilized
`StitchableExpansionState`, which converted into a dead state, I'd have to
either forego the expansion abstraction---which would make the parser even
more confusing---or I'd have to create _another_ abstraction around the dead
state, which would mean that I stripped one abstraction just to introduce
another one that's essentially the same thing.  It didn't feel right, but it
would have worked.

The use of `PhantomData` in `StitchableExpansionState` was also a sign that
something wasn't quite right, in terms of how the abstractions were
integrating with one-another.

And so here we are, as I struggle to wade my way through all of the yak
shavings and make any meaningful progress on this project, while others
continue to suffer due to slow build times.

I'm sorry.  Even if the system is improving.

DEV-13156
main
Mike Gerwitz 2022-11-17 14:41:49 -05:00
parent 1ce36225f6
commit 1aca0945df
3 changed files with 188 additions and 166 deletions

View File

@ -225,6 +225,40 @@ impl<S: ParseState> TransitionResult<S> {
Result(result, la) => falive(st, result, la),
}
}
/// Conditionally map to a [`TransitionResult`] based on whether the
/// inner [`TransitionData`] represents an object.
pub(in super::super) fn branch_obj_la<SB: ParseState>(
self,
fobj: impl FnOnce(
Transition<S>,
<S as ParseState>::Object,
Option<Lookahead<<S as ParseState>::Token>>,
) -> TransitionResult<<SB as ParseState>::Super>,
fother: impl FnOnce(Transition<S>) -> Transition<SB>,
) -> TransitionResult<<SB as ParseState>::Super>
where
S: PartiallyStitchableParseState<SB>,
{
use ParseStatus::{Incomplete, Object};
use TransitionData::{Dead, Result};
let Self(st, data) = self;
match data {
Result(Ok(Object(obj)), la) => fobj(st, obj, la).into_super(),
// Can't use `TransitionData::inner_into` since we only have a
// `PartiallyStitchableParseState`,
// and `into_inner` requires being able to convert the inner
// object that we handled above.
Result(Ok(Incomplete), la) => {
fother(st).incomplete().maybe_with_lookahead(la)
}
Result(Err(e), la) => fother(st).err(e).maybe_with_lookahead(la),
Dead(Lookahead(la)) => fother(st).dead(la),
}
}
}
/// Token to use as a lookahead token in place of the next token from the
@ -350,48 +384,6 @@ impl<S: ParseState> TransitionData<S> {
}
}
/// Map [`TransitionData`] when the inner result is of type
/// [`ParseStatus::Object`].
///
/// This will map over `self` within the context of an inner
/// [`ParseStatus::Object`] and an associated optional token of
/// [`Lookahead`].
/// This allows using objects to influence parser operations more
/// broadly.
///
/// _This method is private to this module because it requires that the
/// caller be diligent in not discarding the provided token of
/// lookahead._
/// Since this token may be stored and later emitted,
/// there is no reliable automated way at present to ensure that this
/// invariant is upheld;
/// such an effort is far beyond the scope of current work at the
/// time of writing.
pub(in super::super) fn map_when_obj<SB: ParseState>(
self,
f: impl FnOnce(S::Object, Option<Lookahead<S::Token>>) -> TransitionData<SB>,
) -> TransitionData<SB>
where
SB: ParseState<Token = S::Token, Error = S::Error>,
{
// Ideally this will be decomposed into finer-grained functions
// (as in a more traditional functional style),
// but such wasn't needed at the time of writing.
// But this is dizzying.
match self {
TransitionData::Result(Ok(ParseStatus::Object(obj)), la) => {
f(obj, la)
}
TransitionData::Result(Ok(ParseStatus::Incomplete), la) => {
TransitionData::Result(Ok(ParseStatus::Incomplete), la)
}
TransitionData::Result(Err(e), la) => {
TransitionData::Result(Err(e), la)
}
TransitionData::Dead(la) => TransitionData::Dead(la),
}
}
/// Asserts a reflexive relationship between the [`TransitionData`] of
/// our own [`ParseState`] `S` and a target [`ParseState`] `SB`.
///
@ -575,25 +567,17 @@ impl<S: ParseState> Transition<S> {
)
}
/// Map over the inner [`ParseState`] `S` to another
/// Produce a map over the inner [`ParseState`] `S` to another
/// [`ParseState`] `SB`.
///
/// Unlike other parts of this API which mandate explicit instantiation
/// of [`Transition`] for self-documentation,
/// this maps over the inner value since [`Transition`] is already
/// apparent.
/// This is consequently much less verbose,
/// as it allows using tuple constructions for `f`,
/// and most [`ParseState`]s are implemented as tuples
/// (or tuple enums)
/// in practice.
pub fn map<SB: ParseState>(
self,
f: impl FnOnce(S) -> SB,
) -> Transition<SB> {
match self {
Self(st) => Transition(f(st)),
}
/// Note that this is a curried associated function,
/// not a method.
/// The intent is to maintain self-documentation by invoking it
/// qualified as [`Transition::fmap`].
pub fn fmap<SB: ParseState>(
f: impl Fn(S) -> SB,
) -> impl Fn(Transition<S>) -> Transition<SB> {
move |Self(st)| Transition(f(st))
}
}

View File

@ -21,12 +21,21 @@
//!
//! _Expansion_ refers to the production of many [`Object`]s that are
//! derived from a single [`Token`].
//! An [`ExpandableParseState`] is a [`ClosedParseState`] that,
//! provided a [`Token`],
//! produces an [`Expansion`] of zero or more [`Expansion::Expanded`]
//! [`Object`]s before terminating with a [`Expansion::DoneExpanding`]
//! [`Token`] intended to replace the originally provided [`Token`].
//!
//! An [`ExpandableParseState`] can be stitched with a parent parser using
//! [`StitchExpansion`],
//! giving the perception of expanding into that parent's token stream.
use super::super::{
prelude::*,
state::{Lookahead, StitchableParseState, TransitionData},
use super::super::{prelude::*, state::Lookahead};
use crate::{
diagnose::{panic::DiagnosticOptionPanic, Annotate},
parse::state::PartiallyStitchableParseState,
};
use std::{fmt::Display, marker::PhantomData};
/// Represents an expansion operation on some source token of type `T`.
///
@ -39,6 +48,14 @@ pub enum Expansion<T, O: Object> {
/// Expansion is complete and the source token should be replaced with
/// the inner `T`.
///
/// Since the expectation is that the parser has completed parsing and
/// no longer requires the token provided to it,
/// the parser yielding this variant _must not_ yield a token of
/// lookahead,
/// otherwise the system assume that the parser has an
/// implementation defect (bug) and will be forced to panic rather
/// than discard it.
DoneExpanding(T),
}
@ -55,116 +72,73 @@ where
/// An [`ExpandableParseState`] capable of expanding into the token stream
/// of a parent [`ParseState`] `SP`.
///
/// This trait asserts that an [`ExpandableParseState`] is a
/// [`StitchableParseState<SP>`](StitchableParseState) after being wrapped
/// by [`StitchableExpansionState`].
pub trait ExpandableInto<SP: ParseState> =
ExpandableParseState<<SP as ParseState>::Object>
where
StitchableExpansionState<Self, <SP as ParseState>::Object>:
StitchableParseState<SP>;
Self: ExpandableParseState<<SP as ParseState>::Object>
+ PartiallyStitchableParseState<SP>;
/// Convert a [`ClosedParseState`] yielding an [`Expansion<T,O>`](Expansion)
/// object into a parser yielding `O` with a dead state yielding `T`.
/// [`ExpandableParseState`] state stitching.
///
/// It is more convenient and clear to write parsers using [`Expansion`],
/// since those variants not only state directly what the intent of the
/// operations are,
/// but also avoid having to work with dead states.
/// However,
/// their wrapping in [`Expansion`] makes them difficult to delegate to
/// (compose with)
/// other parsers using [`ParseState`]'s `delegate_*` family of
/// functions.
///
/// This parser handles this translation by stripping away the
/// [`Expansion`] abstraction and producing a [`ParseState`] that looks
/// and acts like what would have been implemented in the absence of such
/// an abstraction.
#[derive(Debug, PartialEq, Eq)]
pub struct StitchableExpansionState<S: ClosedParseState, O: Object> {
st: S,
_phantom: PhantomData<O>,
}
// We implement Default if the parser `S` that we're wrapping does.
impl<S: ClosedParseState, O: Object> Default for StitchableExpansionState<S, O>
where
S: Default,
{
fn default() -> Self {
Self {
st: Default::default(),
_phantom: Default::default(),
}
}
}
impl<S: ClosedParseState, O: Object> ParseState
for StitchableExpansionState<S, O>
where
S: ExpandableParseState<O>,
{
type Token = S::Token;
type Object = O;
type Error = S::Error;
type Context = S::Context;
#[inline]
fn parse_token(
/// See [`Self::stitch_expansion`] for more information.
pub trait StitchExpansion: ClosedParseState {
/// Stitch a [`ExpandableParseState`] that is
/// [`ExpandableInto<SP>`](ExpandableInto).
///
/// This combines the state machine of an [`ExpandableParseState`],
/// allowing that parser to expand into the token stream of [`Self`].
///
/// Panics
/// ======
/// This will panic with diagnostic information if a token of lookahead
/// is provided with a [`Expansion::DoneExpanding`] variant.
/// See that variant for more information.
fn stitch_expansion<SP: ParseState, C>(
self,
tok: Self::Token,
ctx: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use Expansion::*;
tok: <Self as ParseState>::Token,
mut ctx: C,
into: impl Fn(Transition<Self>) -> Transition<SP>,
done: impl FnOnce(
Transition<Self>,
<SP as ParseState>::Token,
) -> TransitionResult<SP>,
) -> TransitionResult<<SP as ParseState>::Super>
where
Self: ExpandableInto<SP>,
C: AsMut<<Self as ParseState>::Context>,
{
use Expansion::{DoneExpanding, Expanded};
let Self { st, _phantom } = self;
self.parse_token(tok, ctx.as_mut()).branch_obj_la(
|st, obj, la| match (obj, la) {
(Expanded(obj), la) => {
into(st).ok(obj).maybe_with_lookahead(la)
}
st.parse_token(tok, ctx).bimap(
|st| Self { st, _phantom },
|data| {
data.map_when_obj(|obj, la| match (obj, la) {
(Expanded(obj), la) => {
TransitionData::Result(Ok(ParseStatus::Object(obj)), la)
}
// Since we are converting the `DoneExpanding` variant
// into a lookahead token,
// we would have nothing to do with a token of
// lookahead if one were provided to us.
(DoneExpanding(tok), Some(la)) => la.overwrite_panic(
tok,
(DoneExpanding(tok), la) => {
// Uphold parser lookahead invariant.
la.diagnostic_expect_none(
|Lookahead(la_tok)| {
vec![
la_tok.span().note(
"this token of lookahead would be lost",
),
tok.span().internal_error(
"unexpected token of lookahead while \
completing expansion of this token",
),
]
},
"cannot provide lookahead token with \
Expansion::DoneExpanding",
),
);
(DoneExpanding(tok), None) => {
TransitionData::Dead(Lookahead(tok))
}
})
done(st, tok).into_super()
}
},
&into,
)
}
fn is_accepting(&self, ctx: &Self::Context) -> bool {
self.st.is_accepting(ctx)
}
}
impl<S: ClosedParseState, O: Object> Display
for StitchableExpansionState<S, O>
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self {
st: parser,
_phantom,
} => {
write!(f, "{parser}, with Expansion stripped")
}
}
}
}
#[cfg(test)]

View File

@ -23,7 +23,75 @@ use crate::{
span::{dummy::*, Span},
sym::{st::raw, SymbolId},
};
use std::{assert_matches::assert_matches, convert::Infallible};
use std::{
assert_matches::assert_matches, convert::Infallible, fmt::Display,
marker::PhantomData,
};
#[derive(Debug, PartialEq, Eq)]
pub struct StitchableExpansionState<S: ClosedParseState, O: Object> {
st: S,
_phantom: PhantomData<O>,
}
impl<S: ClosedParseState, O: Object> Default for StitchableExpansionState<S, O>
where
S: Default,
{
fn default() -> Self {
Self {
st: Default::default(),
_phantom: Default::default(),
}
}
}
impl<S: ClosedParseState, O: Object> ParseState
for StitchableExpansionState<S, O>
where
S: ExpandableParseState<O> + StitchExpansion,
<S as ParseState>::Context: AsMut<<S as ParseState>::Context>,
{
type Token = S::Token;
type Object = O;
type Error = S::Error;
type Context = S::Context;
#[inline]
fn parse_token(
self,
tok: Self::Token,
ctx: &mut Self::Context,
) -> TransitionResult<Self::Super> {
let Self { st, _phantom } = self;
st.stitch_expansion(
tok,
ctx,
Transition::fmap(|st| Self { st, _phantom }),
|Transition(st), tok| Transition(Self { st, _phantom }).dead(tok),
)
}
fn is_accepting(&self, ctx: &Self::Context) -> bool {
self.st.is_accepting(ctx)
}
}
impl<S: ClosedParseState, O: Object> Display
for StitchableExpansionState<S, O>
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self {
st: parser,
_phantom,
} => {
write!(f, "{parser}, with Expansion stripped")
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct TestObject(SPair);
@ -94,6 +162,8 @@ impl Display for TestParseState {
}
}
impl StitchExpansion for TestParseState {}
const STOP: SymbolId = raw::L_YIELD;
const DEAD_SYM: SymbolId = raw::L_WARNING;
@ -177,12 +247,6 @@ fn expandable_into_is_stitchable_with_target() {
}
}
// Asserts that the wrapping `StitchableParseState` has transformed the
// `TestParseState` into something stitchable.
//
// This serves as a sanity check for the below.
assert_impl_all!(ExpansionSut: StitchableParseState<TargetParseState>);
// The `ExpandableInto` trait alias is responsible for asserting that a
// given parser is an expansion parser that is able to be converted
// into a parser stitchable with the target.