From fcc29f03687d850a498f55c123ae93828fc36b5a Mon Sep 17 00:00:00 2001 From: StreamDemon Date: Thu, 2 Jul 2026 20:11:26 +0800 Subject: [PATCH] Fix spurious error on non-ident-headed type arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `type_args_after_open` probed for an associated-type binding by calling `ident()` and backtracking on failure — but `ident()` records an "expected identifier" diagnostic before the backtrack, and the checkpoint only rewound the position, never the error list. The fallback type parse then succeeded while the stale error survived, so spec-legal arguments like `Vec<&str>`, `Vec<(i64, i64)>`, and `Vec<[u8; 4]>` were rejected. The binding now commits by peeking `IDENT "="` before consuming anything, the same shape as the parser's other lookaheads (turbofish, send-statement head), so no failed probe ever runs. A genuinely bad binding value (`Item = `) now recovers to the next comma or `>` instead of re-parsing the binding name as a type argument. Closes #78. The probe cases from the issue land as regression tests, plus a corpus fixture for non-ident-headed generic arguments. --- crates/sploosh-parser/src/lib.rs | 82 ++++++++++++++++++++++++++++---- tests/corpus/generic_args.sp | 17 +++++++ 2 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 tests/corpus/generic_args.sp diff --git a/crates/sploosh-parser/src/lib.rs b/crates/sploosh-parser/src/lib.rs index 50fc1f6..3d098ca 100644 --- a/crates/sploosh-parser/src/lib.rs +++ b/crates/sploosh-parser/src/lib.rs @@ -1205,19 +1205,24 @@ impl<'src> Parser<'src> { fn type_args_after_open(&mut self) -> Vec { let mut args = Vec::new(); while !self.at(TokenKind::Gt) && !self.eof() { - let checkpoint = self.pos; - if let Some(name) = self.ident() - && self.eat(TokenKind::Eq).is_some() - && let Some(ty) = self.ty() - { - args.push(TypeArg::Assoc { name, ty }); - } else { - self.pos = checkpoint; - if let Some(ty) = self.ty() { - args.push(TypeArg::Type(ty)); + // §16 `type_arg = type | IDENT "=" type`. Commit to the + // associated-type binding by peeking `IDENT "="` — probing with + // `ident()` and backtracking left its failure diagnostic in + // `errors`, wrongly rejecting non-ident-headed type args like + // `Vec<&str>` (issue #78). + if self.at(TokenKind::Ident) && self.peek_kind_at(1) == Some(TokenKind::Eq) { + if let Some(name) = self.ident() + && self.eat(TokenKind::Eq).is_some() + && let Some(ty) = self.ty() + { + args.push(TypeArg::Assoc { name, ty }); } else { self.recover_until(&[TokenKind::Comma, TokenKind::Gt]); } + } else if let Some(ty) = self.ty() { + args.push(TypeArg::Type(ty)); + } else { + self.recover_until(&[TokenKind::Comma, TokenKind::Gt]); } if self.eat(TokenKind::Comma).is_none() { break; @@ -2191,6 +2196,63 @@ mod tests { assert_eq!(names, ["Convert", "Loggable", "Bounded"]); } + #[test] + fn non_ident_headed_type_args_parse() { + // Issue #78: the assoc-binding probe left a stale "expected + // identifier" error behind, wrongly rejecting any type arg that does + // not start with an identifier. + for source in [ + "fn f(v: Vec<&str>) {}", + "fn f(v: Vec<(i64, i64)>) {}", + "fn f(v: Vec<[u8; 4]>) {}", + "fn f(v: Map>) {}", + ] { + let result = parse_program(source); + assert!(result.is_ok(), "{source}: {result:?}"); + } + // The reference type arg survives structurally, not just error-free. + let program = parse_program("fn f(v: Vec<&str>) {}").unwrap(); + let ItemKind::Function(func) = &program.items[0].kind else { + panic!("expected function"); + }; + let Param::Named { ty, .. } = &func.params[0] else { + panic!("expected named param"); + }; + let Type::Path { args, .. } = ty else { + panic!("expected generic path type"); + }; + let TypeArg::Type(Type::Reference { mutable, inner }) = &args[0] else { + panic!("expected reference type arg, got {args:?}"); + }; + assert!(!mutable); + assert!(matches!(inner.as_ref(), Type::Primitive(name) if name == "str")); + } + + #[test] + fn assoc_binding_with_non_ident_headed_value_parses() { + // The binding side of `type_arg` still works when its value type is + // itself non-ident-headed. + let program = parse_program("fn f(it: &dyn Iter>) {}").unwrap(); + let ItemKind::Function(func) = &program.items[0].kind else { + panic!("expected function"); + }; + let Param::Named { + ty: Type::Reference { inner, .. }, + .. + } = &func.params[0] + else { + panic!("expected reference param"); + }; + let Type::Dyn(trait_ref) = inner.as_ref() else { + panic!("expected dyn type"); + }; + let TypeArg::Assoc { name, ty } = &trait_ref.args[0] else { + panic!("expected assoc binding, got {:?}", trait_ref.args[0]); + }; + assert_eq!(name.name, "Item"); + assert!(matches!(ty, Type::Path { .. })); + } + #[test] fn dyn_trait_type_args_with_assoc_binding() { let program = parse_program("fn f(it: &dyn Iter) {}").unwrap(); diff --git a/tests/corpus/generic_args.sp b/tests/corpus/generic_args.sp new file mode 100644 index 0000000..a136526 --- /dev/null +++ b/tests/corpus/generic_args.sp @@ -0,0 +1,17 @@ +/// Generic type arguments (§16 type_arg = type | IDENT "=" type): non-ident- +/// headed argument types — references, tuples, arrays — alongside associated- +/// type bindings whose value types are themselves non-ident-headed. +struct Grid { + names: Vec<&str>, + cells: Vec<(i64, i64)>, + rows: Vec<[u8; 4]>, + lookup: Map>, +} + +struct Streams { + it: Box>>, +} + +fn columns(grid: &Grid) -> Vec<(i64, i64)> { + grid.cells +}