diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index c58a560..7a52956 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -484,19 +484,58 @@ impl<'a> Parser<'a> { let mut suppress: Vec = Vec::new(); loop { match self.peek_head_ident() { - Some("doc") => doc = Some(self.parse_doc()?), + Some("doc") => { + if doc.is_some() { + let pos = self.peek().map(|t| t.span.start).unwrap_or(0); + return Err(ParseError::Production { + production: "fn-def", + message: format!( + "fn `{name}` has duplicate `(doc ...)` clause" + ), + pos, + }); + } + doc = Some(self.parse_doc()?); + } Some("suppress") => suppress.push(self.parse_suppress_attr()?), Some("type") => { - let t = self.parse_type_attr()?; - ty = Some(t); + if ty.is_some() { + let pos = self.peek().map(|t| t.span.start).unwrap_or(0); + return Err(ParseError::Production { + production: "fn-def", + message: format!( + "fn `{name}` has duplicate `(type ...)` clause" + ), + pos, + }); + } + ty = Some(self.parse_type_attr()?); } Some("params") => { - let p = self.parse_params_attr()?; - params = Some(p); + if params.is_some() { + let pos = self.peek().map(|t| t.span.start).unwrap_or(0); + return Err(ParseError::Production { + production: "fn-def", + message: format!( + "fn `{name}` has duplicate `(params ...)` clause" + ), + pos, + }); + } + params = Some(self.parse_params_attr()?); } Some("body") => { - let b = self.parse_body_attr()?; - body = Some(b); + if body.is_some() { + let pos = self.peek().map(|t| t.span.start).unwrap_or(0); + return Err(ParseError::Production { + production: "fn-def", + message: format!( + "fn `{name}` has duplicate `(body ...)` clause" + ), + pos, + }); + } + body = Some(self.parse_body_attr()?); } Some(other) => { let pos = self.peek().map(|t| t.span.start).unwrap_or(0); @@ -2260,4 +2299,98 @@ mod tests { }; assert_eq!(i.doc.as_deref(), Some("instance for ints")); } + + /// Iter 22-tidy.5: `parse_fn` rejects a duplicate `(doc ...)` + /// clause with a `fn-def` production diagnostic naming the + /// offending fn. Pre-22-tidy.5, the second clause silently + /// overwrote the first; this test pins the strict shape. + #[test] + fn parse_fn_rejects_duplicate_doc_clause() { + let err = parse( + r#" + (module m + (fn f + (doc "first") + (doc "second") + (type (fn-type (params) (ret (con Unit)))) + (params) + (body (do io/print_str "x")))) + "#, + ) + .unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("fn `f` has duplicate `(doc ...)` clause"), + "expected duplicate-doc diagnostic, got: {msg}" + ); + } + + /// Iter 22-tidy.5: `parse_fn` rejects a duplicate `(type ...)` + /// clause with a `fn-def` production diagnostic naming the + /// offending fn. + #[test] + fn parse_fn_rejects_duplicate_type_clause() { + let err = parse( + r#" + (module m + (fn f + (type (fn-type (params) (ret (con Unit)))) + (type (fn-type (params) (ret (con Unit)))) + (params) + (body (do io/print_str "x")))) + "#, + ) + .unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("fn `f` has duplicate `(type ...)` clause"), + "expected duplicate-type diagnostic, got: {msg}" + ); + } + + /// Iter 22-tidy.5: `parse_fn` rejects a duplicate `(params ...)` + /// clause with a `fn-def` production diagnostic naming the + /// offending fn. + #[test] + fn parse_fn_rejects_duplicate_params_clause() { + let err = parse( + r#" + (module m + (fn f + (type (fn-type (params) (ret (con Unit)))) + (params) + (params) + (body (do io/print_str "x")))) + "#, + ) + .unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("fn `f` has duplicate `(params ...)` clause"), + "expected duplicate-params diagnostic, got: {msg}" + ); + } + + /// Iter 22-tidy.5: `parse_fn` rejects a duplicate `(body ...)` + /// clause with a `fn-def` production diagnostic naming the + /// offending fn. + #[test] + fn parse_fn_rejects_duplicate_body_clause() { + let err = parse( + r#" + (module m + (fn f + (type (fn-type (params) (ret (con Unit)))) + (params) + (body (do io/print_str "x")) + (body (do io/print_str "y")))) + "#, + ) + .unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("fn `f` has duplicate `(body ...)` clause"), + "expected duplicate-body diagnostic, got: {msg}" + ); + } }