From 754a9b4b7caca2e848742f5194d98dc9aee7f674 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Wed, 23 Sep 2026 04:42:32 -0700 Subject: [PATCH] Retain mixed parse policy and check aggregate budgets before traversal --- .../src/language/analysis_source/mixed.rs | 8 +- .../language/analysis_source/mixed/tests.rs | 216 ++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) diff --git a/crates/oa-core/src/language/analysis_source/mixed.rs b/crates/oa-core/src/language/analysis_source/mixed.rs index 312efcbf..cf82d30b 100644 --- a/crates/oa-core/src/language/analysis_source/mixed.rs +++ b/crates/oa-core/src/language/analysis_source/mixed.rs @@ -36,6 +36,9 @@ struct ModelCounts { pub(super) struct ParsedMixedSyntax<'s> { bundle: &'s SourceBundle, + // Preserve caller policy for the later semantic pass. Syntax storage uses + // separate ceilings and cannot stand in for these graph/record/step limits. + limits: ProjectLimits, files: Vec>, budget: Budget, } @@ -260,7 +263,6 @@ fn parse_with_profile( ))); } let mut budget = Budget::new(profile).map_err(at_entry)?; - let mut traversal = Traversal::new(bundle, limits)?; let bytes = bundle .files .values() @@ -273,6 +275,9 @@ fn parse_with_profile( budget .lowering_entries(metadata, Span::default()) .map_err(at_entry)?; + // Tighter private byte/file-slot limits precede folded-path table and + // traversal allocations. Public Traversal's own validation order is intact. + let mut traversal = Traversal::new(bundle, limits)?; let mut parsed = BTreeMap::new(); let mut model = ModelCounts { limits, @@ -361,6 +366,7 @@ fn parse_with_profile( budget.active().map_err(at_entry)?; Ok(ParsedMixedSyntax { bundle, + limits, files: parsed.into_values().collect(), budget, }) diff --git a/crates/oa-core/src/language/analysis_source/mixed/tests.rs b/crates/oa-core/src/language/analysis_source/mixed/tests.rs index bbb66169..02d5eb65 100644 --- a/crates/oa-core/src/language/analysis_source/mixed/tests.rs +++ b/crates/oa-core/src/language/analysis_source/mixed/tests.rs @@ -919,3 +919,219 @@ fn mixed_deep_typed_success_and_rejection_cleanup_on_128k_stack() { .join() .unwrap(); } + +#[test] +fn witness_retains_exact_caller_policy_separately_from_syntax_storage() { + let bundle = single(&ordinary("a", FIRST_ID, "description \"retained\";")); + let tight = ProjectLimits { + max_files: 1, + max_include_depth: 0, + max_include_edges: 0, + model: ParseLimits { + max_source_bytes: bundle.files["main.arcadia"].len(), + max_graph_objects: 1, + max_records: 2, + max_step_references: 0, + }, + }; + let loose = ProjectLimits { + model: ParseLimits { + max_graph_objects: 5, + max_records: 20, + ..tight.model + }, + ..tight + }; + let policy = |p: ProjectLimits| { + ( + p.max_files, + p.max_include_depth, + p.max_include_edges, + p.model.max_source_bytes, + p.model.max_graph_objects, + p.model.max_records, + p.model.max_step_references, + ) + }; + let first = parse(&bundle, tight).unwrap(); + let second = parse(&bundle, loose).unwrap(); + assert_eq!(policy(first.limits), policy(tight)); + assert_eq!(policy(second.limits), policy(loose)); + assert_ne!(policy(first.limits), policy(second.limits)); + assert!(std::ptr::eq(first.bundle, second.bundle)); + assert_eq!(first.budget.usage, second.budget.usage); + assert_eq!(first.budget.numeric_units(), second.budget.numeric_units()); + // Neither identical syntax nor its storage profile is allowed to erase the + // distinct policy needed for later full nested-record/model-step admission. +} + +#[test] +fn syntax_entries_accumulate_across_both_ordinary_and_typed_producer_orders() { + let ordinary = ordinary("a", FIRST_ID, "description \"second charge\";"); + let typed = calculation("c", CALC_ID, "1"); + for (body, failure_text, token_offset) in [ + (format!("{ordinary}\n{typed}"), "body 1;", 5), + (format!("{typed}\n{ordinary}"), "\"second charge\"", 0), + ] { + let bundle = single(&body); + let source = &bundle.files["main.arcadia"]; + // Independently count project1 + ordinary declaration/field/value3 + + // typed header/result-field/type/body-field/literal5 =9 syntax entries. + let positive = parse(&bundle, ProjectLimits::default()).unwrap(); + assert_eq!(positive.budget.usage.entries, 9); + let profile = Limits { + entries: 8, + ..Limits::source8(ParseLimits::UNLIMITED) + }; + let errors = match parse_with_profile(&bundle, ProjectLimits::default(), profile) { + Ok(_) => panic!("reset syntax budget accepted both producers"), + Err(errors) => errors, + }; + assert_eq!( + errors[0].diagnostic.message, + "Source8 syntax entries limit exceeded." + ); + assert_eq!(errors[0].path, "main.arcadia"); + assert_eq!( + errors[0].diagnostic.span.start, + source.find(failure_text).unwrap() + token_offset + ); + // A mistakenly reset counter might instead fail the later metadata cap; + // that different phase/location must not satisfy this oracle. + } +} + +#[test] +fn exact_work_and_reference_occurrences_accumulate_across_files() { + let split = |expression: &str| SourceBundle { + entry: "main.arcadia".into(), + files: BTreeMap::from([ + ( + "main.arcadia".into(), + document("include \"first.arcadia\"; include \"second.arcadia\";"), + ), + ( + "first.arcadia".into(), + calculation("first", FIRST_ID, expression), + ), + ( + "second.arcadia".into(), + calculation("second", SECOND_ID, expression), + ), + ]), + }; + // Literal1's specified meter weights: parse2 + power construction2 + + // mantissa product2 + one-word normalization67 =73. Two conversions cost146. + // These thresholds are not measured from a mixed witness or parse helper. + let numeric = split("1"); + let profile = Limits { + numeric_units: 146, + ..Limits::source8(ParseLimits::UNLIMITED) + }; + let accepted = parse_with_profile(&numeric, ProjectLimits::default(), profile).unwrap(); + assert_eq!(accepted.budget.numeric_units(), 146); + for alias in ["first", "second"] { + let alone = single(&calculation(alias, FIRST_ID, "1")); + let accepted = parse_with_profile( + &alone, + ProjectLimits::default(), + Limits { + numeric_units: 73, + ..profile + }, + ) + .unwrap(); + assert_eq!(accepted.budget.numeric_units(), 73); + } + let errors = match parse_with_profile( + &numeric, + ProjectLimits::default(), + Limits { + numeric_units: 145, + ..profile + }, + ) { + Ok(_) => panic!("per-file reset lost earlier numeric work"), + Err(errors) => errors, + }; + assert_eq!(errors[0].path, "second.arcadia"); + assert_eq!( + errors[0].diagnostic.span.start, + numeric.files["second.arcadia"].find("body 1;").unwrap() + 5 + ); + assert!( + errors[0] + .diagnostic + .message + .contains("NumericUnits: used 73, requested 73, limit 145") + ); + + let references = split("@missing"); + let profile = Limits { + references: 1, + ..Limits::source8(ParseLimits::UNLIMITED) + }; + for alias in ["first", "second"] { + let alone = single(&calculation(alias, FIRST_ID, "@missing")); + assert_eq!( + parse_with_profile(&alone, ProjectLimits::default(), profile) + .unwrap() + .budget + .usage + .references, + 1 + ); + } + let accepted = parse_with_profile( + &references, + ProjectLimits::default(), + Limits { + references: 2, + ..profile + }, + ) + .unwrap(); + assert_eq!(accepted.budget.usage.references, 2); + let errors = match parse_with_profile(&references, ProjectLimits::default(), profile) { + Ok(_) => panic!("per-file reset lost earlier reference occurrence"), + Err(errors) => errors, + }; + assert_eq!(errors[0].path, "second.arcadia"); + assert_eq!( + errors[0].diagnostic.message, + "Source8 references limit exceeded." + ); + assert_eq!( + errors[0].diagnostic.span.start, + references.files["second.arcadia"].find("@missing").unwrap() + 1 + ); +} + +#[test] +fn private_aggregate_preflights_run_before_traversal_path_allocation() { + let mut bundle = single(""); + // Normal private policy reaches traversal and rejects the non-root entry. + // Under tighter preflight policy, no traversal authority is constructed. + bundle.entry = "nested/main.arcadia".into(); + assert_eq!(rejects(&bundle)[0].diagnostic.code, "invalid_path"); + let normal = Limits::source8(ParseLimits::UNLIMITED); + for (profile, message) in [ + ( + Limits { bytes: 0, ..normal }, + "Source8 index/output byte limit exceeded.", + ), + ( + Limits { + entries: 0, + ..normal + }, + "Source8 lowering entry limit exceeded.", + ), + ] { + let errors = match parse_with_profile(&bundle, ProjectLimits::default(), profile) { + Ok(_) => panic!("exhausted private aggregate returned a witness"), + Err(errors) => errors, + }; + assert_eq!(errors[0].diagnostic.message, message); + } +}