diff --git a/AGENTS.md b/AGENTS.md index bebfb3f8..0aa4b9fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ - We implement resilient parser, provides user-friendly error messages. - We call error messages "diagnostics" to avoid confusion with other errors (see `diagnostics/` folder). - We strive to achieve excellent stability by enforcing invariants in the code: - - `assert!` and `.expect()` for simple cases + - `panic!`, `assert!` or `.expect()` for simple cases - `invariants.rs` otherwise, to skip the coverage of unreachable code - We maintain the architecture decision records (ADRs) - AI agent is responsible for creating new ADR when such decision was made during agentic coding session diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index 5513b4b9..b2a0085e 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -6,7 +6,6 @@ use rowan::{Checkpoint, TextRange}; use super::core::Parser; -use super::invariants::assert_nonempty; use super::cst::token_sets::{ ALT_RECOVERY, EXPR_FIRST, QUANTIFIERS, SEPARATORS, SEQ_RECOVERY, TREE_RECOVERY, @@ -65,7 +64,11 @@ impl Parser<'_> { self.peek(); let ate_equals = self.eat(SyntaxKind::Equals); - self.assert_equals_eaten(ate_equals); + assert!( + ate_equals, + "parse_def: expected '=' but found {:?} (caller should verify Equals is present)", + self.current() + ); if EXPR_FIRST.contains(self.peek()) { self.parse_expr(); @@ -491,7 +494,12 @@ impl Parser<'_> { } let closing = self.peek(); - self.assert_string_quote_match(closing, open_quote); + assert_eq!( + closing, open_quote, + "bump_string_tokens: expected closing {:?} but found {:?} \ + (lexer should only produce quote tokens from complete strings)", + open_quote, closing + ); self.bump(); } @@ -612,7 +620,12 @@ impl Parser<'_> { self.start_node(SyntaxKind::Field); let kind = self.peek(); - self.assert_id_token(kind); + assert_eq!( + kind, + SyntaxKind::Id, + "parse_field: expected Id but found {:?} (caller should verify Id is present)", + kind + ); let span = self.current_span(); let text = token_text(self.source, &self.tokens[self.pos]); self.bump(); @@ -865,7 +878,7 @@ fn to_pascal_case(s: &str) -> String { } fn capitalize_first(s: &str) -> String { - assert_nonempty(s); + assert!(!s.is_empty(), "capitalize_first: called with empty string"); let mut chars = s.chars(); let c = chars.next().unwrap(); c.to_uppercase().chain(chars).collect() diff --git a/crates/plotnik-lib/src/parser/invariants.rs b/crates/plotnik-lib/src/parser/invariants.rs index ff2be6ff..9677f10f 100644 --- a/crates/plotnik-lib/src/parser/invariants.rs +++ b/crates/plotnik-lib/src/parser/invariants.rs @@ -3,7 +3,6 @@ #![cfg_attr(coverage_nightly, coverage(off))] use super::core::Parser; -use super::cst::SyntaxKind; impl Parser<'_> { #[inline] @@ -14,38 +13,4 @@ impl Parser<'_> { ); self.debug_fuel.set(self.debug_fuel.get() - 1); } - - #[inline] - pub(super) fn assert_equals_eaten(&self, ate_equals: bool) { - assert!( - ate_equals, - "parse_def: expected '=' but found {:?} (caller should verify Equals is present)", - self.current() - ); - } - - #[inline] - pub(super) fn assert_string_quote_match(&self, actual: SyntaxKind, expected: SyntaxKind) { - assert_eq!( - actual, expected, - "bump_string_tokens: expected closing {:?} but found {:?} \ - (lexer should only produce quote tokens from complete strings)", - expected, actual - ); - } - - #[inline] - pub(super) fn assert_id_token(&self, kind: SyntaxKind) { - assert_eq!( - kind, - SyntaxKind::Id, - "parse_field: expected Id but found {:?} (caller should verify Id is present)", - kind - ); - } -} - -#[inline] -pub(super) fn assert_nonempty(s: &str) { - assert!(!s.is_empty(), "capitalize_first: called with empty string"); } diff --git a/crates/plotnik-lib/src/query/alt_kinds.rs b/crates/plotnik-lib/src/query/alt_kinds.rs index e7f0067a..d305954e 100644 --- a/crates/plotnik-lib/src/query/alt_kinds.rs +++ b/crates/plotnik-lib/src/query/alt_kinds.rs @@ -6,9 +6,7 @@ use rowan::TextRange; use super::Query; -use super::invariants::{ - assert_alt_no_bare_exprs, assert_root_no_bare_exprs, ensure_both_branch_kinds, -}; +use super::invariants::ensure_both_branch_kinds; use crate::diagnostics::DiagnosticKind; use crate::parser::{AltExpr, AltKind, Branch, Expr}; @@ -20,13 +18,19 @@ impl Query<'_> { self.validate_alt_expr(&body); } - assert_root_no_bare_exprs(&self.ast); + assert!( + self.ast.exprs().next().is_none(), + "alt_kind: unexpected bare Expr in Root (parser should wrap in Def)" + ); } fn validate_alt_expr(&mut self, expr: &Expr) { if let Expr::AltExpr(alt) = expr { self.check_mixed_alternation(alt); - assert_alt_no_bare_exprs(alt); + assert!( + alt.exprs().next().is_none(), + "alt_kind: unexpected bare Expr in Alt (parser should wrap in Branch)" + ); } for child in expr.children() { diff --git a/crates/plotnik-lib/src/query/invariants.rs b/crates/plotnik-lib/src/query/invariants.rs index 5f6ce71f..5298b71c 100644 --- a/crates/plotnik-lib/src/query/invariants.rs +++ b/crates/plotnik-lib/src/query/invariants.rs @@ -2,23 +2,7 @@ #![cfg_attr(coverage_nightly, coverage(off))] -use crate::parser::{AltExpr, Branch, Root}; - -#[inline] -pub fn assert_root_no_bare_exprs(root: &Root) { - assert!( - root.exprs().next().is_none(), - "alt_kind: unexpected bare Expr in Root (parser should wrap in Def)" - ); -} - -#[inline] -pub fn assert_alt_no_bare_exprs(alt: &AltExpr) { - assert!( - alt.exprs().next().is_none(), - "alt_kind: unexpected bare Expr in Alt (parser should wrap in Branch)" - ); -} +use crate::parser::Branch; #[inline] pub fn ensure_both_branch_kinds<'a>( @@ -33,11 +17,3 @@ pub fn ensure_both_branch_kinds<'a>( ), } } - -#[inline] -pub fn ensure_ref_has_name(name: Option) -> T { - name.expect( - "shape_cardinalities: Ref without name token \ - (parser only creates Ref for PascalCase Id)", - ) -} diff --git a/crates/plotnik-lib/src/query/shapes.rs b/crates/plotnik-lib/src/query/shapes.rs index ee4a1501..31ec8c28 100644 --- a/crates/plotnik-lib/src/query/shapes.rs +++ b/crates/plotnik-lib/src/query/shapes.rs @@ -8,7 +8,6 @@ //! undefined refs, etc.). use super::Query; -use super::invariants::ensure_ref_has_name; use crate::diagnostics::DiagnosticKind; use crate::parser::{Expr, Ref, SeqExpr}; @@ -89,7 +88,10 @@ impl Query<'_> { } fn ref_cardinality(&mut self, r: &Ref) -> ShapeCardinality { - let name_tok = ensure_ref_has_name(r.name()); + let name_tok = r.name().expect( + "shape_cardinalities: Ref without name token \ + (parser only creates Ref for PascalCase Id)", + ); let name = name_tok.text(); let Some(body) = self.symbol_table.get(name).cloned() else {