From b2c970b4673e062e47eadd12906d2316c4b1a7b3 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Sat, 12 Sep 2026 22:06:04 -0700 Subject: [PATCH] fix(source): count typed braces in shared syntax frames --- .../language/analysis_source/declarations.rs | 6 ++ .../analysis_source/declarations/tests.rs | 84 ++++++++++++++++++- crates/oa-core/src/language/raw_tokens.rs | 2 +- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/crates/oa-core/src/language/analysis_source/declarations.rs b/crates/oa-core/src/language/analysis_source/declarations.rs index 89337f7e..64af3939 100644 --- a/crates/oa-core/src/language/analysis_source/declarations.rs +++ b/crates/oa-core/src/language/analysis_source/declarations.rs @@ -264,6 +264,7 @@ pub(super) fn parse( let name_span = p.span(); let name = p.quoted()?; let identity = p.identity()?; + p.open()?; let opening = p.take("{")?; let header = Header { kind, @@ -363,6 +364,7 @@ pub(super) fn parse( } } let closing = p.take("}")?; + p.close(); let body = fields.finish(kind, parameters, closing)?; Ok(Declaration { header, @@ -527,6 +529,7 @@ impl Cursor<'_, '_> { } fn dimension(&mut self) -> Result { + self.open()?; self.take("{")?; let mut exponents = [0; 7]; let mut seen = [false; 7]; @@ -577,6 +580,7 @@ impl Cursor<'_, '_> { }); } self.take("}")?; + self.close(); Ok(DimensionSyntax { exponents, entries }) } @@ -595,6 +599,7 @@ impl Cursor<'_, '_> { } fn overrides(&mut self) -> Result, Error> { + self.open()?; self.take("{")?; let mut entries = Vec::new(); let mut names = BTreeSet::new(); @@ -621,6 +626,7 @@ impl Cursor<'_, '_> { }); } self.take("}")?; + self.close(); Ok(entries) } } diff --git a/crates/oa-core/src/language/analysis_source/declarations/tests.rs b/crates/oa-core/src/language/analysis_source/declarations/tests.rs index 3d739c8f..6aaf734f 100644 --- a/crates/oa-core/src/language/analysis_source/declarations/tests.rs +++ b/crates/oa-core/src/language/analysis_source/declarations/tests.rs @@ -506,7 +506,7 @@ fn declaration_budgets_share_literals_types_references_and_failure_state() { #[test] fn typed_frame_budget_and_truncated_declaration_cleanup_have_nearby_successes() { let source = "calculation c \"C\" { result List(Quantity(@q)); body unbound; }"; - for (maximum, succeeds) in [(2, true), (1, false)] { + for (maximum, succeeds) in [(3, true), (2, false)] { let mut profile = limits(); profile.frames = maximum; let mut shared = Budget::new(profile).unwrap(); @@ -529,3 +529,85 @@ fn typed_frame_budget_and_truncated_declaration_cleanup_have_nearby_successes() declaration("oa_check fresh \"Fresh\" { predicate true; }"); } } + +#[test] +fn typed_brace_frames_have_exact_zero_one_two_boundaries() { + let mut profile = limits(); + profile.frames = 0; + let mut shared = Budget::new(profile).unwrap(); + let tokens = lex::tokens("unbound", &mut shared).unwrap(); + let mut cursor = Cursor::new("unbound", &tokens, &mut shared); + assert!(matches!( + syntax::binding(&mut cursor).unwrap(), + Binding::Unbound(_) + )); + assert_eq!(cursor.frames, 0); + assert_eq!(shared.usage.deepest_frame, 0); + + for (source, needed) in [ + ("calculation c \"C\" { result Number; body unbound; }", 1), + ("quantity_kind q \"Q\" { dimension {}; algebra Linear; }", 2), + ( + "oa_analysis a \"A\" { base ModelBaseline; overrides {}; }", + 2, + ), + ] { + for maximum in 0..=2 { + let mut profile = limits(); + profile.frames = maximum; + let mut shared = Budget::new(profile).unwrap(); + let tokens = lex::tokens(source, &mut shared).unwrap(); + let mut cursor = Cursor::new(source, &tokens, &mut shared); + let result = parse(&mut cursor, 8, None); + assert_eq!( + result.is_ok(), + maximum >= needed, + "{source}, limit {maximum}" + ); + if let Err(error) = result { + assert!(error.message.contains("frames")); + assert_eq!(text(source, error.span), "{"); + assert_eq!( + error.span.start, + if maximum == 0 { + source.find('{').unwrap() + } else { + source.rfind('{').unwrap() + } + ); + } else { + assert_eq!(cursor.frames, 0); + cursor.end().unwrap(); + assert_eq!(shared.usage.deepest_frame, needed); + } + } + } +} + +#[test] +fn declaration_frames_restore_the_callers_inherited_context() { + let source = "calculation a \"A\" { result Number; body unbound; } calculation b \"B\" { result Number; body unbound; }"; + for maximum in [1, 2] { + let mut profile = limits(); + profile.frames = maximum; + let mut shared = Budget::new(profile).unwrap(); + let tokens = lex::tokens(source, &mut shared).unwrap(); + let mut cursor = Cursor::new(source, &tokens, &mut shared); + // Future project/include integration owns this outer context. The + // declaration consumer must add to it, then restore it after closing. + cursor.frames = 1; + let first = parse(&mut cursor, 8, None); + if maximum == 1 { + let error = first.unwrap_err(); + assert!(error.message.contains("frames")); + assert_eq!(text(source, error.span), "{"); + } else { + assert!(first.is_ok()); + assert_eq!(cursor.frames, 1); + parse(&mut cursor, 8, None).unwrap(); + assert_eq!(cursor.frames, 1); + cursor.end().unwrap(); + assert_eq!(shared.usage.deepest_frame, 2); + } + } +} diff --git a/crates/oa-core/src/language/raw_tokens.rs b/crates/oa-core/src/language/raw_tokens.rs index d361cd3f..c977b752 100644 --- a/crates/oa-core/src/language/raw_tokens.rs +++ b/crates/oa-core/src/language/raw_tokens.rs @@ -302,7 +302,7 @@ mod tests { (String, "\"雪\""), (Whitespace, " "), (Punctuation, "@"), - (Identifier, "alias"), + (Keyword, "alias"), ], ), ("\"incomplete 雪", &[(Error, "\"incomplete 雪")]),