tamer: xir::parse::ele: Streaming attribute parsing

This allows using a `[attr]` special form to stream attributes as they are
encountered rather than aggregating a static attribute list.  This is
necessary in particular for short-hand template application and short-hand
function application, since the attribute names are derived from template
and function parameter lists, which are runtime values.

The syntax for this is a bit odd since there's a semi-useless and confusing
`@ {} => obj` still, but this is only going to be used by a couple of NTs
and it's not worth the time to clean this up, given the rather significant
macro complexity already.

DEV-7145
main
Mike Gerwitz 2022-08-16 23:06:38 -04:00
parent 43c64babb0
commit 4177b8ed71
2 changed files with 152 additions and 4 deletions

View File

@ -427,6 +427,9 @@ macro_rules! ele_parse {
// (defaulting to Incomplete via @!ele_expand_body).
/$($close_span:ident)? => $closemap:expr,
// Streaming (as opposed to aggregate) attribute parsing.
$([attr]($attr_stream_binding:ident) => $attr_stream_map:expr,)?
// Nonterminal references.
<> {
$(
@ -806,7 +809,7 @@ macro_rules! ele_parse {
xir::{
EleSpan,
flat::XirfToken,
parse::parse_attrs,
parse::{parse_attrs, EleParseCfg},
},
};
@ -816,15 +819,37 @@ macro_rules! ele_parse {
RecoverEleIgnoreClosed_, ExpectClose_, Closed_
};
// Needed since `$ntfirst_cfg` cannot be nested within
// the conditional `[attr]` block.
#[allow(dead_code)]
const NTFIRST_CFG: EleParseCfg =
ele_parse!(@!ntref_cfg $($ntfirst_cfg)?);
match (self, tok) {
(
Expecting_(cfg) | NonPreemptableExpecting_(cfg),
XirfToken::Open(qname, span, depth)
) if $nt::matches(qname) => {
Transition(Attrs_(
let transition = Transition(Attrs_(
(cfg, qname, span.tag_span(), depth),
parse_attrs(qname, span)
)).incomplete()
));
// Streaming attribute parsing will cause the
// attribute map to be yielded immediately as
// the opening object,
// since we will not be aggregating attrs.
$(
// Used only to match on `[attr]`.
let [<_ $attr_stream_binding>] = ();
return transition.ok($attrmap);
)?
// If the `[attr]` special form was _not_
// provided,
// we'll be aggregating attributes.
#[allow(unreachable_code)]
transition.incomplete()
},
(
@ -855,6 +880,50 @@ macro_rules! ele_parse {
).incomplete()
},
// Streaming attribute matching takes precedence
// over aggregate.
// This is primarily me being lazy,
// because it's not worth a robust syntax for
// something that's rarely used
// (macro-wise, I mean;
// it's heavily utilized as a percentage of
// source file parsed since short-hand
// template applications are heavily used).
$(
(
st @ Attrs_(..),
XirfToken::Attr($attr_stream_binding),
) => Transition(st).ok($attr_stream_map),
// Override the aggregate attribute parser
// delegation by forcing the below match to
// become unreachable
// (xref anchor <<SATTR>>).
// Since we have already emitted the `$attrmap`
// object on `Open`,
// this yields an incomplete parse.
(Attrs_(meta, _), tok) => {
ele_parse!(@!ntref_delegate
stack,
$ntfirst(meta),
$ntfirst_st,
Transition(
Into::<$ntfirst_st>::into(
NTFIRST_CFG
)
).incomplete().with_lookahead(tok),
Transition($ntfirst(meta))
.incomplete()
.with_lookahead(tok)
)
}
)?
// This becomes unreachable when the `[attr]` special
// form is provided,
// which overrides this match directly above
// (xref <<SATTR>>).
#[allow(unreachable_patterns)]
(Attrs_(meta @ (_, qname, _, _), sa), tok) => {
sa.delegate_until_obj::<Self, _>(
tok,
@ -888,7 +957,7 @@ macro_rules! ele_parse {
$ntfirst_st,
Transition(
Into::<$ntfirst_st>::into(
ele_parse!(@!ntref_cfg $($ntfirst_cfg)?)
NTFIRST_CFG
)
).ok(obj),
Transition($ntfirst(meta)).ok(obj)

View File

@ -281,6 +281,7 @@ fn empty_element_ns_prefix_invalid_close_contains_matching_qname() {
);
}
// Static, aggregate attribute objects.
#[test]
fn empty_element_with_attr_bindings() {
#[derive(Debug, PartialEq, Eq)]
@ -369,6 +370,84 @@ fn empty_element_with_attr_bindings() {
);
}
// Rather than using aggregate attributes,
// `[test]` allows for dynamic streaming attribute parsing.
// This is necessary for elements like short-hand template applications.
#[test]
fn element_with_streaming_attrs() {
#[derive(Debug, PartialEq, Eq)]
enum Foo {
Open,
Attr(Attr),
Child,
Close,
}
impl crate::parse::Object for Foo {}
const QN_ROOT: QName = QN_PACKAGE;
const QN_CHILD: QName = QN_DIM;
ele_parse! {
enum Sut;
type Object = Foo;
Root := QN_ROOT {
// symbol soup
@ {} => Foo::Open,
/ => Foo::Close,
// This binds all attributes in place of `@ {}` above.
[attr](attr) => Foo::Attr(attr),
Child,
};
Child := QN_CHILD {
@ {} => Foo::Child,
};
}
let attr1 = Attr(QN_NAME, "one".into(), AttrSpan(S2, S3));
let attr2 = Attr(QN_TYPE, "two".into(), AttrSpan(S3, S4));
let toks = vec![
XirfToken::Open(QN_ROOT, OpenSpan(S1, N), Depth(0)),
// These attributes should stream,
// but only _after_ having emitted the opening object from `@ {}`.
XirfToken::Attr(attr1.clone()),
XirfToken::Attr(attr2.clone()),
// A child should halt attribute parsing just the same as `@ {}`
// would without the `[text]` special form.
XirfToken::Open(QN_CHILD, OpenSpan(S5, N), Depth(1)),
XirfToken::Close(None, CloseSpan::empty(S6), Depth(1)),
XirfToken::Close(Some(QN_ROOT), CloseSpan(S2, N), Depth(0)),
];
// Unlike other test cases,
// rather than attribute parsing yielding a single object,
// we will see both the `@ {}` object _and_ individual attributes
// from the `[attr]` map.
// Since we are not aggregating,
// and since streaming attributes must be emitted _after_ the opening
// object to ensure proper nesting in the downstream IR,
// the `@ {}` object is emitted immediately upon opening instead of
// emitting an incomplete parse.
use Parsed::*;
assert_eq!(
Ok(vec![
Object(Foo::Open), // [Root] Root Open
Object(Foo::Attr(attr1)), // [Root] attr1
Object(Foo::Attr(attr2)), // [Root] attr2
Incomplete, // [Child] Child Open (<LA)
Object(Foo::Child), // [Child@] Child Close (>LA)
Incomplete, // [Child] Child Close (<LA)
Object(Foo::Close), // [Root] Root Close
]),
Sut::parse(toks.into_iter()).collect(),
);
}
// An unexpected element produces an error for the offending token and
// then employs a recovery strategy so that parsing may continue.
#[test]