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-7145main
parent
43c64babb0
commit
4177b8ed71
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue