refactor(surface): share the copy-pasted parser sub-productions

Round-trip-preserving — the byte-isomorphic Form-A ↔ JSON-AST tests,
the hash pins, and the full fixture-corpus parse are all green; error
strings passed through verbatim, not homogenised.

- The Int/Float/Str/true/false literal-token match was inlined in three
  parsers; extracted try_parse_literal_token and routed parse_pat_lit
  and the parse_term fallthrough through it.
- parse_doc and parse_export were identical but for the keyword; both
  now go through one parse_string_attr, and the previously
  doc/export-bypassed expect_string helper is back on the shared path.
- parse_effects_clause and parse_forall_constraints_clause shared the
  expect-keyword / collect / reject-empty / expect-rparen shape;
  unified under parse_repeating_clause, with each clause's empty-error
  message passed in.

parse_intrinsic_attr was left inline-able-but-not-inlined: it has two
call sites, so inlining would re-duplicate a documented helper.
This commit is contained in:
2026-06-02 02:17:51 +02:00
parent e5534d1532
commit 9b0eb58cd9
+106 -135
View File
@@ -505,54 +505,29 @@ impl<'a> Parser<'a> {
})
}
fn parse_doc(&mut self) -> Result<String, ParseError> {
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 `(<kw> "<string>")` 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<String, ParseError> {
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<String, ParseError> {
self.parse_string_attr("doc-attr", "doc", "doc body")
}
fn parse_export(&mut self) -> Result<String, ParseError> {
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<Ctor, ParseError> {
@@ -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<String, ParseError> {
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<Vec<String>, 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 `(<kw> 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<T>(
&mut self,
prod: &'static str,
kw: &'static str,
empty_msg: &'static str,
item: fn(&mut Self) -> Result<T, ParseError>,
) -> Result<Vec<T>, 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<Vec<String>, 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<Type, ParseError> {
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<Vec<Constraint>, 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<Constraint, ParseError> {
@@ -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<Literal> {
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<Pattern, ParseError> {
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 })