diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 508d7cf..8486431 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -505,54 +505,29 @@ impl<'a> Parser<'a> { }) } - fn parse_doc(&mut self) -> Result { - self.expect_lparen("doc-attr")?; - self.expect_keyword("doc")?; - let s = match self.peek().cloned() { - Some(Token { tok: Tok::Str(s), .. }) => { - self.cur += 1; - s - } - Some(t) => { - return Err(ParseError::Unexpected { - expected: "string literal (doc body)".into(), - got: tok_label(&t.tok), - pos: t.span.start, - }); - } - None => { - return Err(ParseError::UnexpectedEof { - expected: "string literal (doc body)".into(), - }); - } - }; - self.expect_rparen("doc-attr")?; + /// Parse a `( "")` attribute clause into its string + /// payload. `prod` is the production tag used for the paren-balance + /// diagnostics; `body_ctx` parameterises the string-literal error + /// (e.g. `"doc body"`, `"export C symbol"`). + fn parse_string_attr( + &mut self, + prod: &'static str, + kw: &'static str, + body_ctx: &'static str, + ) -> Result { + self.expect_lparen(prod)?; + self.expect_keyword(kw)?; + let s = self.expect_string(body_ctx)?; + self.expect_rparen(prod)?; Ok(s) } + fn parse_doc(&mut self) -> Result { + self.parse_string_attr("doc-attr", "doc", "doc body") + } + fn parse_export(&mut self) -> Result { - self.expect_lparen("export-attr")?; - self.expect_keyword("export")?; - let s = match self.peek().cloned() { - Some(Token { tok: Tok::Str(s), .. }) => { - self.cur += 1; - s - } - Some(t) => { - return Err(ParseError::Unexpected { - expected: "string literal (export C symbol)".into(), - got: tok_label(&t.tok), - pos: t.span.start, - }); - } - None => { - return Err(ParseError::UnexpectedEof { - expected: "string literal (export C symbol)".into(), - }); - } - }; - self.expect_rparen("export-attr")?; - Ok(s) + self.parse_string_attr("export-attr", "export", "export C symbol") } fn parse_ctor(&mut self) -> Result { @@ -713,8 +688,9 @@ impl<'a> Parser<'a> { Ok(Suppress { code, because }) } - /// helper — consume one string-literal token. Used by - /// [`Self::parse_suppress_attr`]. + /// helper — consume one string-literal token. Shared by + /// [`Self::parse_string_attr`] (doc / export bodies) and + /// [`Self::parse_suppress_attr`] (code / because bodies). fn expect_string(&mut self, ctx: &'static str) -> Result { match self.peek().cloned() { Some(Token { tok: Tok::Str(s), .. }) => { @@ -1217,27 +1193,46 @@ impl<'a> Parser<'a> { }) } - fn parse_effects_clause(&mut self) -> Result, ParseError> { - self.expect_lparen("effects-clause")?; - self.expect_keyword("effects")?; - // 1+ idents per grammar; round-trip allows empty too (to support - // any future use), but the printer never emits an empty clause. + /// Parse a `( item+)` clause requiring at least one item. The + /// `item` parser is applied repeatedly until the closing paren; an + /// empty body fails with `empty_msg` (a per-clause string so the + /// diagnostic stays specific). `prod` tags the paren-balance errors. + fn parse_repeating_clause( + &mut self, + prod: &'static str, + kw: &'static str, + empty_msg: &'static str, + item: fn(&mut Self) -> Result, + ) -> Result, ParseError> { + self.expect_lparen(prod)?; + self.expect_keyword(kw)?; let mut out = Vec::new(); while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) { - out.push(self.expect_ident("effect name")?); + out.push(item(self)?); } if out.is_empty() { let pos = self.peek().map(|t| t.span.start).unwrap_or(0); return Err(ParseError::Production { - production: "effects-clause", - message: "expected at least one effect name".into(), + production: prod, + message: empty_msg.into(), pos, }); } - self.expect_rparen("effects-clause")?; + self.expect_rparen(prod)?; Ok(out) } + fn parse_effects_clause(&mut self) -> Result, ParseError> { + // 1+ idents per grammar; round-trip allows empty too (to support + // any future use), but the printer never emits an empty clause. + self.parse_repeating_clause( + "effects-clause", + "effects", + "expected at least one effect name", + |p| p.expect_ident("effect name"), + ) + } + fn parse_forall_type(&mut self) -> Result { self.expect_lparen("forall-type")?; self.expect_keyword("forall")?; @@ -1267,22 +1262,12 @@ impl<'a> Parser<'a> { } fn parse_forall_constraints_clause(&mut self) -> Result, ParseError> { - self.expect_lparen("forall.constraints")?; - self.expect_keyword("constraints")?; - let mut out = Vec::new(); - while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) { - out.push(self.parse_constraint()?); - } - if out.is_empty() { - let pos = self.peek().map(|t| t.span.start).unwrap_or(0); - return Err(ParseError::Production { - production: "forall.constraints", - message: "(constraints …) clause must contain at least one (constraint …)".into(), - pos, - }); - } - self.expect_rparen("forall.constraints")?; - Ok(out) + self.parse_repeating_clause( + "forall.constraints", + "constraints", + "(constraints …) clause must contain at least one (constraint …)", + |p| p.parse_constraint(), + ) } fn parse_constraint(&mut self) -> Result { @@ -1340,36 +1325,27 @@ impl<'a> Parser<'a> { } } } - Some(Token { tok: Tok::Ident(s), .. }) => { - self.cur += 1; - if s == "true" { - Ok(Term::Lit { lit: Literal::Bool { value: true } }) - } else if s == "false" { - Ok(Term::Lit { lit: Literal::Bool { value: false } }) - } else { - Ok(Term::Var { name: s }) + _ => { + if let Some(lit) = self.try_parse_literal_token() { + return Ok(Term::Lit { lit }); + } + match self.peek().cloned() { + // A non-boolean ident is a variable reference, not a + // literal — `try_parse_literal_token` left it for us. + Some(Token { tok: Tok::Ident(s), .. }) => { + self.cur += 1; + Ok(Term::Var { name: s }) + } + Some(t) => Err(ParseError::Unexpected { + expected: "term".into(), + got: tok_label(&t.tok), + pos: t.span.start, + }), + None => Err(ParseError::UnexpectedEof { + expected: "term".into(), + }), } } - Some(Token { tok: Tok::Int(v), .. }) => { - self.cur += 1; - Ok(Term::Lit { lit: Literal::Int { value: v } }) - } - Some(Token { tok: Tok::Float(bits), .. }) => { - self.cur += 1; - Ok(Term::Lit { lit: Literal::Float { bits } }) - } - Some(Token { tok: Tok::Str(s), .. }) => { - self.cur += 1; - Ok(Term::Lit { lit: Literal::Str { value: s } }) - } - Some(t) => Err(ParseError::Unexpected { - expected: "term".into(), - got: tok_label(&t.tok), - pos: t.span.start, - }), - None => Err(ParseError::UnexpectedEof { - expected: "term".into(), - }), } } @@ -1879,49 +1855,44 @@ impl<'a> Parser<'a> { Ok(Pattern::Ctor { ctor, fields }) } + /// Consume one literal-form token — an `Int`, `Float`, `Str`, or the + /// idents `true` / `false` — and return its [`Literal`]. Any other + /// token (including a non-boolean ident) leaves the cursor untouched + /// and returns `None`, so each caller decides how to handle the + /// non-literal fallthrough (a bare ident is `Term::Var` in a term but + /// an error in a `pat-lit`). + fn try_parse_literal_token(&mut self) -> Option { + let lit = match &self.peek()?.tok { + Tok::Int(v) => Literal::Int { value: *v }, + Tok::Float(bits) => Literal::Float { bits: *bits }, + Tok::Str(s) => Literal::Str { value: s.clone() }, + Tok::Ident(s) if s == "true" => Literal::Bool { value: true }, + Tok::Ident(s) if s == "false" => Literal::Bool { value: false }, + _ => return None, + }; + self.cur += 1; + Some(lit) + } + fn parse_pat_lit(&mut self) -> Result { self.expect_lparen("pat-lit")?; self.expect_keyword("pat-lit")?; - let lit = match self.peek().cloned() { - Some(Token { tok: Tok::Int(v), .. }) => { - self.cur += 1; - Literal::Int { value: v } - } - Some(Token { tok: Tok::Float(bits), .. }) => { - self.cur += 1; - Literal::Float { bits } - } - Some(Token { tok: Tok::Str(s), .. }) => { - self.cur += 1; - Literal::Str { value: s } - } - Some(Token { tok: Tok::Ident(s), span }) => { - if s == "true" { - self.cur += 1; - Literal::Bool { value: true } - } else if s == "false" { - self.cur += 1; - Literal::Bool { value: false } - } else { + let lit = match self.try_parse_literal_token() { + Some(lit) => lit, + None => match self.peek() { + Some(t) => { return Err(ParseError::Unexpected { expected: "literal form (integer, float, string, `true`, or `false`)".into(), - got: tok_label(&Tok::Ident(s)), - pos: span.start, + got: tok_label(&t.tok), + pos: t.span.start, }); } - } - Some(t) => { - return Err(ParseError::Unexpected { - expected: "literal form (integer, float, string, `true`, or `false`)".into(), - got: tok_label(&t.tok), - pos: t.span.start, - }); - } - None => { - return Err(ParseError::UnexpectedEof { - expected: "literal form".into(), - }); - } + None => { + return Err(ParseError::UnexpectedEof { + expected: "literal form".into(), + }); + } + }, }; self.expect_rparen("pat-lit")?; Ok(Pattern::Lit { lit })