Skip to content

Commit cf3141c

Browse files
committed
introduce cast variant reduction suppression
This helps resolve some annoying ambiguities while keeping the ones I think we want.
1 parent c732d22 commit cf3141c

File tree

3 files changed

+53
-5
lines changed

3 files changed

+53
-5
lines changed

crates/formality-core/src/parse/parser.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ where
5757
scope: &'s Scope<L>,
5858
text: &'t str,
5959
reductions: Vec<&'static str>,
60+
is_cast_variant: bool,
6061
}
6162

6263
impl<'s, 't, T, L> Parser<'s, 't, T, L>
@@ -117,12 +118,14 @@ where
117118

118119
/// Shorthand for `parse_variant` where the parsing operation is to
119120
/// parse the type `V` and then upcast it to the desired result type.
121+
/// Also marks the variant as a cast variant.
120122
pub fn parse_variant_cast<V>(&mut self, variant_precedence: usize)
121123
where
122124
V: CoreParse<L> + Upcast<T>,
123125
{
124126
let variant_name = std::any::type_name::<V>();
125127
Self::parse_variant(self, variant_name, variant_precedence, |p| {
128+
p.mark_as_cast_variant();
126129
let v: V = p.nonterminal()?;
127130
Ok(v.upcast())
128131
})
@@ -151,6 +154,7 @@ where
151154
scope: self.scope,
152155
text: self.start_text,
153156
reductions: vec![],
157+
is_cast_variant: false,
154158
};
155159
let result = op(&mut active_variant);
156160

@@ -160,7 +164,12 @@ where
160164

161165
match result {
162166
Ok(value) => {
163-
active_variant.reductions.push(variant_name);
167+
// Subtle: for cast variants, don't record the variant name in the reduction lits,
168+
// as it doesn't carry semantic weight. See `mark_as_cast_variant` for more details.
169+
if !active_variant.is_cast_variant {
170+
active_variant.reductions.push(variant_name);
171+
}
172+
164173
self.successes.push((
165174
SuccessfulParse {
166175
text: active_variant.text,
@@ -280,6 +289,37 @@ where
280289
self.text = skip_trailing_comma(self.text);
281290
}
282291

292+
/// Marks this variant as an cast variant,
293+
/// which means there is no semantic difference
294+
/// between the thing you parsed and the reduced form.
295+
/// We do this automatically for enum variants marked
296+
/// as `#[cast]` or calls to `parse_variant_cast`.
297+
///
298+
/// Cast variants interact differently with ambiguity detection.
299+
/// Consider this grammar:
300+
///
301+
/// ```
302+
/// X = Y | Z // X has two variants
303+
/// Y = A // Y has 1 variant
304+
/// Z = A B // Z has 1 variant
305+
/// A = "a" // A has 1 variant
306+
/// B = "b" // B has 1 variant
307+
/// ```
308+
///
309+
/// If you mark the two `X` variants (`X = Y` and `X = Z`)
310+
/// as cast variants, then the input `"a b"` is considered
311+
/// unambiguous and is parsed as `X = (Z = (A = "a') (B = "b))`
312+
/// with no remainder.
313+
///
314+
/// If you don't mark those variants as cast variants,
315+
/// then we consider this *ambiguous*, because
316+
/// it could be that you want `X = (Y = (A = "a"))` with
317+
/// a remainder of `"b"`. This is appropriate
318+
/// if choosing Y vs Z has different semantic meaning.
319+
pub fn mark_as_cast_variant(&mut self) {
320+
self.is_cast_variant = true;
321+
}
322+
283323
/// Expect *exactly* the given text (after skipping whitespace)
284324
/// in the input string. Reports an error if anything else is observed.
285325
/// In error case, consumes only whitespace.
@@ -349,6 +389,7 @@ where
349389
text: self.text,
350390
reductions: vec![],
351391
scope: self.scope,
392+
is_cast_variant: false,
352393
};
353394

354395
match this.identifier_like_string() {
@@ -399,6 +440,7 @@ where
399440
scope: &scope,
400441
text: self.text,
401442
reductions: vec![],
443+
is_cast_variant: false,
402444
};
403445
let result = op(&mut av);
404446
self.text = av.text;

crates/formality-macros/src/parse.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ fn parse_variant(
104104
let construct = variant.construct(field_ident);
105105
Ok(quote_spanned! {
106106
ast.ident.span() =>
107+
__p.mark_as_cast_variant();
107108
#(#build)*
108109
Ok(#construct)
109110
})

tests/parser-torture-tests/ambiguity.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
use formality_core::term;
1+
use formality_core::{term, test};
22
use std::sync::Arc;
33

44
#[test]
5-
#[should_panic(expected = "ambiguous parse")] // FIXME: we want this example to work
65
fn reduce_reduce_ok() {
76
#[term]
87
pub enum Root {
@@ -27,7 +26,10 @@ fn reduce_reduce_ok() {
2726
formality_core::id!(Id);
2827

2928
let term: Root = crate::ptt::term("my String");
30-
expect_test::expect![].assert_debug_eq(&term);
29+
expect_test::expect![[r#"
30+
my String
31+
"#]]
32+
.assert_debug_eq(&term);
3133
}
3234

3335
#[test]
@@ -53,5 +55,8 @@ fn reduce_reduce_ambig() {
5355
// Root = (Id Id Root::TwoId) (Id Root::OneId) Root::TwoRr)
5456
// Root = ((Id Root::OneId) (Id Root::OneId) (Id Root::OneId) Root::TwoRr)
5557
let term: Root = crate::ptt::term("a b c");
56-
expect_test::expect![].assert_debug_eq(&term);
58+
expect_test::expect![[r#"
59+
a b c
60+
"#]]
61+
.assert_debug_eq(&term);
5762
}

0 commit comments

Comments
 (0)