diff --git a/crates/oa-core/src/language.rs b/crates/oa-core/src/language.rs index 16791650..920de343 100644 --- a/crates/oa-core/src/language.rs +++ b/crates/oa-core/src/language.rs @@ -1028,6 +1028,18 @@ fn edit_positions_from_tokens_with_text( count: usize, value_ends: Option<&BTreeMap>, text: impl FnMut(usize, Span) -> Result<(), Box>, +) -> Result, Box> { + edit_positions_from_tokens_with_producers(source, unit, token, count, value_ends, None, text) +} + +fn edit_positions_from_tokens_with_producers( + source: &str, + unit: &SourceUnit, + token: impl Fn(usize) -> Lexeme, + count: usize, + value_ends: Option<&BTreeMap>, + mut quota: Option, + text: impl FnMut(usize, Span) -> Result<(), Box>, ) -> Result, Box> { let mut ends: Vec<_> = (0..count).map(|at| token(at).span.end).collect(); let mut stack = Vec::new(); @@ -1044,10 +1056,27 @@ fn edit_positions_from_tokens_with_text( ends: Vec, value_ends: Option<&'a BTreeMap>, text: H, + source: &'a str, + quota: &'a mut Option, } impl Lexeme, H: FnMut(usize, Span) -> Result<(), Box>> Positions<'_, F, H> { + fn consume(&mut self, slots: usize, offset: usize) -> Result<(), Box> { + if let Some(remaining) = self.quota { + *remaining = remaining.checked_sub(slots).ok_or_else(|| { + Box::new(diagnostic( + self.source, + Error { + code: "invalid_source", + offset, + message: "Source8 projection quota underflow.".into(), + }, + )) + })?; + } + Ok(()) + } fn token(&self, start: usize) -> usize { let mut low = 0; let mut high = self.ends.len(); @@ -1068,6 +1097,7 @@ fn edit_positions_from_tokens_with_text( fields .iter() .map(|(key, ast)| { + self.consume(1, ast.offset())?; let value = self.value(ast)?; let start = (self.token_at)(self.token(value.span.start) - 1).span.start; let end = (self.token_at)(self.token(value.span.end)).span.end; @@ -1083,6 +1113,7 @@ fn edit_positions_from_tokens_with_text( .collect() } fn value(&mut self, ast: &Ast) -> Result> { + self.consume(1, ast.offset())?; let index = self.token(ast.offset()); let span = Span { start: ast.offset(), @@ -1119,8 +1150,11 @@ fn edit_positions_from_tokens_with_text( ends, value_ends, text, + source, + quota: &mut quota, }; - unit.declarations + let result = unit + .declarations .iter() .map(|declaration| { if declaration.id_insertion.is_some() { @@ -1133,6 +1167,7 @@ fn edit_positions_from_tokens_with_text( }, ))); } + positions.consume(7, declaration.span.start)?; let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) { &["kind", "alias", "id_keyword", "id"] } else { @@ -1161,7 +1196,18 @@ fn edit_positions_from_tokens_with_text( }, )) }) - .collect() + .collect::, _>>()?; + if quota.is_some_and(|remaining| remaining != 0) { + return Err(Box::new(diagnostic( + source, + Error { + code: "invalid_source", + offset: 0, + message: "Source8 projection quota leftover.".into(), + }, + ))); + } + Ok(result) } struct Parser<'a> { source: &'a str, @@ -1337,6 +1383,10 @@ impl Parser<'_> { .span .end; self.take(";")?; + #[cfg(test)] + if let Some(mixed) = &mut self.mixed { + mixed.projection(1, Span { start, end })?; + } Ok((key, value, Span { start, end })) } @@ -1370,7 +1420,12 @@ impl Parser<'_> { .span .end; if let Some(mixed) = &mut self.mixed { - mixed.value_ends.insert(ast.offset(), end); + let span = Span { + start: ast.offset(), + end, + }; + mixed.value_end(span)?; + mixed.projection(1, span)?; } } result @@ -1697,7 +1752,12 @@ impl Parser<'_> { let lexical_owner = open.last().map(|&owner| declarations[owner].id); #[cfg(test)] if let Some(mixed) = &mut self.mixed { - mixed.lexical_owners.push(open.last().copied()); + let span = Span { + start: offset, + end: body_start, + }; + mixed.lexical_owner(open.last().copied(), span)?; + mixed.projection(7, span)?; } declarations.push(Declaration { kind, diff --git a/crates/oa-core/src/language/analysis_source/budget.rs b/crates/oa-core/src/language/analysis_source/budget.rs index 6ede7fa9..97b5b46f 100644 --- a/crates/oa-core/src/language/analysis_source/budget.rs +++ b/crates/oa-core/src/language/analysis_source/budget.rs @@ -39,6 +39,11 @@ pub struct Budget { numeric: WorkMeter, failed: Option, text: Option, + entries: Option, +} +struct ProducerEntries { + carried: usize, + metadata: usize, } struct ProducerText { carried: usize, @@ -69,8 +74,64 @@ impl Budget { ), failed: None, text: None, + entries: None, }) } + pub(super) fn enable_entry_producers( + &mut self, + carried: usize, + files: usize, + ) -> Result<(), Error> { + assert!(self.entries.is_none()); + self.check_producer_entries(carried, files, 0, Span::default())?; + self.entries = Some(ProducerEntries { + carried, + metadata: files, + }); + Ok(()) + } + fn check_producer_entries( + &mut self, + carried: usize, + metadata: usize, + delta: usize, + span: Span, + ) -> Result<(), Error> { + self.active()?; + let total = carried + .checked_add(self.usage.entries) + .and_then(|n| n.checked_add(metadata)) + .and_then(|n| n.checked_add(delta)); + let message = match total { + None => Some("Source8 producer entry size overflow."), + Some(n) if n > self.limits.entries => Some("Source8 lowering entry limit exceeded."), + _ => None, + }; + if let Some(message) = message { + let failure = error(span, message); + self.failed = Some(failure.clone()); + return Err(failure); + } + Ok(()) + } + pub(super) fn producer_entries(&self) -> Option { + self.entries.as_ref().map(|e| e.metadata) + } + pub(super) fn reserve_metadata(&mut self, delta: usize, span: Span) -> Result<(), Error> { + if let Some(e) = &self.entries { + self.check_producer_entries(e.carried, e.metadata, delta, span)?; + self.entries.as_mut().expect("enabled entries").metadata += delta; + } + Ok(()) + } + pub(super) fn producer_entry_overflow(&mut self, span: Span) -> Error { + if let Some(failure) = &self.failed { + return failure.clone(); + } + let failure = error(span, "Source8 producer entry size overflow."); + self.failed = Some(failure.clone()); + failure + } pub(super) fn enable_text_producers(&mut self, carried: usize) -> Result<(), Error> { self.index_bytes(carried, Span::default())?; assert!(self.text.is_none()); @@ -200,6 +261,9 @@ impl Budget { span, "syntax entries", )?; + if let Some(e) = &self.entries { + self.check_producer_entries(e.carried, e.metadata, 1, span)?; + } self.usage.entries += 1; Ok(()) } diff --git a/crates/oa-core/src/language/analysis_source/mixed.rs b/crates/oa-core/src/language/analysis_source/mixed.rs index cc0d130a..a8c18bd4 100644 --- a/crates/oa-core/src/language/analysis_source/mixed.rs +++ b/crates/oa-core/src/language/analysis_source/mixed.rs @@ -68,8 +68,11 @@ pub(in crate::language) struct State<'a> { budget: &'a mut Budget, model: &'a mut ModelCounts, typed: Vec, - pub(in crate::language) value_ends: BTreeMap, - pub(in crate::language) lexical_owners: Vec>, + value_ends: BTreeMap, + lexical_owners: Vec>, + pending_projection: usize, + reserved_values: usize, + reserved_owners: usize, } fn ordinary_error(error: super::Error) -> Error { @@ -93,6 +96,42 @@ fn lexeme(token: lex::Token) -> Lexeme { } impl State<'_> { + pub(in crate::language) fn projection( + &mut self, + slots: usize, + span: Span, + ) -> Result<(), Error> { + if self.budget.producer_entries().is_some() { + self.pending_projection = self + .pending_projection + .checked_add(slots) + .ok_or_else(|| ordinary_error(self.budget.producer_entry_overflow(span)))?; + } + Ok(()) + } + pub(in crate::language) fn value_end(&mut self, span: Span) -> Result<(), Error> { + let new_key = !self.value_ends.contains_key(&span.start); + self.budget + .reserve_metadata(usize::from(new_key), span) + .map_err(ordinary_error)?; + if new_key { + self.reserved_values += 1; + } + self.value_ends.insert(span.start, span.end); + Ok(()) + } + pub(in crate::language) fn lexical_owner( + &mut self, + owner: Option, + span: Span, + ) -> Result<(), Error> { + self.budget + .reserve_metadata(1, span) + .map_err(ordinary_error)?; + self.lexical_owners.push(owner); + self.reserved_owners += 1; + Ok(()) + } pub(in crate::language) fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> { self.budget.own_text(bytes, span).map_err(ordinary_error) } @@ -263,7 +302,7 @@ fn parse_with_profile( limits: ProjectLimits, profile: Limits, ) -> Result, Vec> { - parse_impl(bundle, limits, profile, None, false) + parse_impl(bundle, limits, profile, None, false, false) } fn parse_impl<'s>( @@ -272,6 +311,7 @@ fn parse_impl<'s>( profile: Limits, carried: Option>, text_producers: bool, + entry_producers: bool, ) -> Result, Vec> { let entry_source = bundle.files.get(&bundle.entry).map_or("", String::as_str); let at_entry = |error| located(&bundle.entry, entry_source, ordinary_error(error)); @@ -313,7 +353,14 @@ fn parse_impl<'s>( .index_bytes(bytes, Span::default()) .map_err(at_entry)?; let mut metadata = bundle.files.len(); - if let Some(input) = &carried { + if entry_producers { + let input = carried + .as_ref() + .expect("entry mode requires carried authority"); + budget + .enable_entry_producers(input.slots().entries, metadata) + .map_err(at_entry)?; + } else if let Some(input) = &carried { input.check(&budget, 0, metadata).map_err(at_entry)?; } else { budget @@ -337,6 +384,7 @@ fn parse_impl<'s>( .expect("traversal checked source"); let failure = |error| located(path, source, ordinary_error(error)); let tokens = lex::tokens(source, &mut budget).map_err(failure)?; + let metadata_before = budget.producer_entries(); let mut parser = Parser { source, tokens: Vec::new(), @@ -351,6 +399,9 @@ fn parse_impl<'s>( typed: Vec::new(), value_ends: BTreeMap::new(), lexical_owners: Vec::new(), + pending_projection: 0, + reserved_values: 0, + reserved_owners: 0, }), }; let ordinary = parser @@ -360,7 +411,19 @@ fn parse_impl<'s>( drop(parser); // Projected ordinary AST nodes/fields are bounded before constructing a // second position representation. Count references as slots, not syntax. - let projection_slots = ordinary_position_slots(&ordinary); + let projection_slots = if entry_producers { + let slots = tokens + .len() + .checked_add(state.pending_projection) + .ok_or_else(|| failure(state.budget.producer_entry_overflow(Span::default())))?; + state + .budget + .reserve_metadata(slots, Span::default()) + .map_err(failure)?; + state.pending_projection + } else { + ordinary_position_slots(&ordinary) + }; metadata = metadata .checked_add(tokens.len()) .and_then(|n| n.checked_add(state.value_ends.len())) @@ -377,12 +440,13 @@ fn parse_impl<'s>( .lowering_entries(metadata, Span::default()) .map_err(failure)?; } - let positions = language::edit_positions_from_tokens_with_text( + let positions = language::edit_positions_from_tokens_with_producers( source, &ordinary, |at| lexeme(tokens[at]), tokens.len(), Some(&state.value_ends), + entry_producers.then_some(projection_slots), |bytes, span| { state .budget @@ -397,6 +461,23 @@ fn parse_impl<'s>( related: Vec::new(), }] })?; + if let Some(before) = metadata_before { + let expected = tokens + .len() + .checked_add(state.value_ends.len()) + .and_then(|n| n.checked_add(state.lexical_owners.len())) + .and_then(|n| n.checked_add(projection_slots)) + .and_then(|n| n.checked_add(before)); + if state.reserved_values != state.value_ends.len() + || state.reserved_owners != state.lexical_owners.len() + || expected != state.budget.producer_entries() + { + return Err(failure(super::error( + Span::default(), + "Source8 producer entry inventory mismatch.", + ))); + } + } for (raw, span) in &ordinary.includes { traversal.include(path, raw, *span)?; } @@ -421,6 +502,12 @@ fn parse_impl<'s>( } traversal.finish()?; budget.active().map_err(at_entry)?; + if budget.producer_entries().is_some_and(|m| m != metadata) { + return Err(at_entry(super::error( + Span::default(), + "Source8 producer entry inventory mismatch.", + ))); + } if let Some((_, temporary)) = budget.producer_text() { assert_eq!(temporary, 0, "completed syntax has no temporary text"); } diff --git a/crates/oa-core/src/language/analysis_source/mixed/carried.rs b/crates/oa-core/src/language/analysis_source/mixed/carried.rs index 851aebaf..3c07e1d8 100644 --- a/crates/oa-core/src/language/analysis_source/mixed/carried.rs +++ b/crates/oa-core/src/language/analysis_source/mixed/carried.rs @@ -91,6 +91,7 @@ fn parse_carried(input: CarriedInput<'_>) -> Result, Vec, +) -> Result, Vec> { + let (bundle, policy, guard) = input.into_parts(); + parse_impl( + bundle, + policy.project_limits, + policy.limits, + Some(guard), + true, + true, ) } @@ -163,3 +181,6 @@ mod tests; #[cfg(test)] mod text_producer; + +#[cfg(test)] +mod entry_producer; diff --git a/crates/oa-core/src/language/analysis_source/mixed/carried/entry_producer.rs b/crates/oa-core/src/language/analysis_source/mixed/carried/entry_producer.rs new file mode 100644 index 00000000..0eed3484 --- /dev/null +++ b/crates/oa-core/src/language/analysis_source/mixed/carried/entry_producer.rs @@ -0,0 +1,443 @@ +//! Literal entry oracles, independently authored before the producer implementation. +use super::*; +const LIMIT: &str = "Source8 lowering entry limit exceeded."; +fn doc(body: &str) -> String { + format!( + "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n" + ) +} +fn bundle(body: &str) -> SourceBundle { + SourceBundle { + entry: "main.arcadia".into(), + files: BTreeMap::from([("main.arcadia".into(), doc(body))]), + } +} +fn chain() -> SourceBundle { + let mut b = bundle("include \"part.arcadia\";"); + b.files.insert( + "part.arcadia".into(), + "include \"last.arcadia\";\r\n".into(), + ); + b.files.insert("last.arcadia".into(), "//é\r\n".into()); + b +} +fn held(bundle: SourceBundle, entries: usize) -> RetainedSnapshot { + RetainedSnapshot::fixture( + bundle, + SnapshotPolicy { + limits: Limits { + entries, + bytes: 4096, + ..Limits::source8(ParseLimits::UNLIMITED) + }, + project_limits: ProjectLimits::default(), + }, + ) + .unwrap() +} +fn failed(result: Result, Vec>) -> SourceDiagnostic { + let errors = match result { + Ok(_) => panic!("unexpected syntax success"), + Err(e) => e, + }; + assert_eq!(errors.len(), 1); + errors.into_iter().next().unwrap() +} +fn diagnostic(d: &SourceDiagnostic, path: &str, start: usize, end: usize) { + assert_eq!(d.path, path); + assert_eq!(d.diagnostic.code, "invalid_source"); + assert_eq!(d.diagnostic.message, LIMIT); + assert_eq!(d.diagnostic.span, Span { start, end }); + assert!(d.related.is_empty()); +} +#[test] +fn real_owner_syntax_and_resolver_boundaries_are_separate() { + for (b, c, u, m, syntax_cap, resolve_cap, refused_path) in [ + (bundle(""), 1, 1, 11, 13, 17, "main.arcadia"), + (chain(), 3, 3, 19, 25, 29, "part.arcadia"), + ] { + let owner = held(b.clone(), syntax_cap); + let syntax = parse_entry_producers(owner.input()).unwrap(); + assert_eq!(owner.retained.entries, c); + assert_eq!(syntax.budget.usage.entries, u); + assert_eq!(syntax.budget.producer_entries(), Some(m)); + assert_eq!(syntax.metadata, m); + assert_eq!( + project::text_producer_inventory(&syntax).unwrap().entries, + c + u + m + ); + drop(syntax); + let low = held(b.clone(), syntax_cap - 1); + diagnostic( + &failed(parse_entry_producers(low.input())), + refused_path, + 0, + 1, + ); + for cap in [resolve_cap, resolve_cap - 1] { + let owner = held(b.clone(), cap); + let syntax = parse_entry_producers(owner.input()).unwrap(); + let usage = syntax.budget.usage; + let numeric = syntax.budget.numeric_units(); + match project::resolve(syntax) { + Ok(result) => { + assert_eq!(cap, resolve_cap); + assert_eq!(result.phase_state().1.entries, c + u + m); + assert_eq!(result.phase_state().0.budget.usage, usage); + assert_eq!(result.phase_state().0.budget.numeric_units(), numeric); + let text = + project::resolve(parse_text_producers(owner.input()).unwrap()).unwrap(); + assert_eq!(result.project(), text.project()); + assert_eq!(result.warnings(), text.warnings()); + assert_eq!(result.phase_state().1, text.phase_state().1); + } + Err(failure) => { + assert_eq!(cap, resolve_cap - 1); + assert_eq!(failure.diagnostics.len(), 1); + diagnostic(&failure.diagnostics[0], "main.arcadia", 11, 73); + } + } + assert_eq!(owner.bundle, b); + } + } +} +#[test] +fn owner_and_value_refuse_at_the_original_producer() { + for (body, cap, offset, complete) in [ + ( + "oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { }", + 4, + 75, + 29, + ), + ( + "oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"\"; }", + 7, + 149, + 37, + ), + ] { + let owner = held(bundle(body), cap); + diagnostic( + &failed(parse_entry_producers(owner.input())), + "main.arcadia", + offset, + offset + 1, + ); + let owner = held(bundle(body), complete); + let syntax = parse_entry_producers(owner.input()).unwrap(); + assert_eq!( + owner.retained.entries + syntax.budget.usage.entries + syntax.metadata, + complete + ); + // The independent inventory walker has its own existing scratch peak. + let roomy = held(bundle(body), 4096); + let syntax = parse_entry_producers(roomy.input()).unwrap(); + assert_eq!( + project::text_producer_inventory(&syntax).unwrap().entries, + complete + ); + } +} +const NESTED: &str = "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\noa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { payload { items [@p, uuid \"00000000-0000-0000-0000-000000000001\", { flag true; }]; };\noa_activity b \"B\" id \"00000000-0000-0000-0000-000000000003\" { }\n}\nnode n id \"00000000-0000-0000-0000-000000000004\" { }\n}\n"; +fn nested() -> SourceBundle { + SourceBundle { + entry: "main.arcadia".into(), + files: BTreeMap::from([("main.arcadia".into(), NESTED.into())]), + } +} +#[test] +fn nested_inventory_and_prepaid_projection_quota() { + assert_eq!(NESTED.len(), 344); + let owner = held(nested(), 103); + let syntax = parse_entry_producers(owner.input()).unwrap(); + assert_eq!(syntax.budget.usage.entries, 13); + assert_eq!(syntax.budget.producer_entries(), Some(89)); + assert_eq!(syntax.metadata, 89); + assert_eq!( + owner.retained.entries + syntax.budget.usage.entries + syntax.metadata, + 103 + ); + let roomy = held(nested(), 4096); + let inventory_syntax = parse_entry_producers(roomy.input()).unwrap(); + assert_eq!( + project::text_producer_inventory(&inventory_syntax) + .unwrap() + .entries, + 103 + ); + let file = &syntax.files[0]; + assert_eq!(file.tokens.len(), 49); + assert_eq!(file.lexical_owners, [None, Some(0), None]); + assert_eq!(file.positions.len(), 3); + assert_eq!(file.positions[0].1.header.len(), 5); + assert_eq!(file.positions[2].1.header.len(), 4); + let language::EditKind::Record(fields) = &file.positions[0].1.fields.kind else { + panic!("body") + }; + let payload = &fields["payload"].value; + assert_eq!( + payload.span, + Span { + start: 145, + end: 221 + } + ); + let language::EditKind::Record(fields) = &payload.kind else { + panic!("payload") + }; + let items = &fields["items"].value; + assert_eq!( + items.span, + Span { + start: 153, + end: 218 + } + ); + let language::EditKind::List(items) = &items.kind else { + panic!("items") + }; + assert_eq!( + items.iter().map(|v| v.span).collect::>(), + [ + Span { + start: 154, + end: 156 + }, + Span { + start: 158, + end: 201 + }, + Span { + start: 203, + end: 217 + } + ] + ); + let language::EditKind::Record(fields) = &items[2].kind else { + panic!("member") + }; + assert_eq!( + fields["flag"].value.span, + Span { + start: 210, + end: 214 + } + ); + // Injected prepaid quota errors, not valid alternate source inventories. + for (quota, message) in [ + (29, "Source8 projection quota underflow."), + (31, "Source8 projection quota leftover."), + ] { + let error = language::edit_positions_from_tokens_with_producers( + file.source, + &file.ordinary, + |at| lexeme(file.tokens[at]), + file.tokens.len(), + None, + Some(quota), + |_, _| Ok(()), + ) + .unwrap_err(); + assert_eq!(error.message, message); + } + assert_eq!(syntax.budget.producer_entries(), Some(89)); + let low = held(nested(), 102); + diagnostic( + &failed(parse_entry_producers(low.input())), + "main.arcadia", + 0, + 1, + ); +} +fn make_state<'a>(budget: &'a mut Budget, model: &'a mut ModelCounts) -> State<'a> { + State { + source: "", + tokens: &[], + budget, + model, + typed: Vec::new(), + value_ends: BTreeMap::new(), + lexical_owners: Vec::new(), + pending_projection: 0, + reserved_values: 0, + reserved_owners: 0, + } +} +fn internal_budget(cap: usize) -> Budget { + Budget::new(Limits { + entries: cap, + ..Limits::source8(ParseLimits::UNLIMITED) + }) + .unwrap() +} +#[test] +fn internal_reserve_and_mutate_helpers_leave_collections_unchanged() { + // Scalar fault fixtures exercise atomic mutation; these do not invent real owners. + let mut model = ModelCounts { + limits: ProjectLimits::default(), + graph: 0, + records: 0, + includes: 0, + }; + let mut budget = internal_budget(3); + budget.enable_entry_producers(1, 1).unwrap(); + let mut state = make_state(&mut budget, &mut model); + state.value_end(Span { start: 10, end: 11 }).unwrap(); + state.value_end(Span { start: 10, end: 12 }).unwrap(); + assert_eq!(state.value_ends, BTreeMap::from([(10, 12)])); + assert_eq!(state.reserved_values, 1); + assert_eq!(state.budget.producer_entries(), Some(2)); + let error = state.value_end(Span { start: 20, end: 21 }).unwrap_err(); + assert_eq!(error.message, LIMIT); + assert_eq!(error.offset, 20); + let repeated = state.value_end(Span { start: 10, end: 99 }).unwrap_err(); + assert_eq!(repeated.offset, 20); + assert_eq!(repeated.message, LIMIT); + assert_eq!(state.value_ends, BTreeMap::from([(10, 12)])); + assert_eq!(state.budget.producer_entries(), Some(2)); + let error = state + .lexical_owner(Some(0), Span { start: 30, end: 31 }) + .unwrap_err(); + assert_eq!(error.offset, 20); // Original latched refusal survives a later producer. + assert!(state.lexical_owners.is_empty()); + assert_eq!(state.reserved_owners, 0); + let mut budget = internal_budget(3); + budget.enable_entry_producers(1, 1).unwrap(); + let mut state = make_state(&mut budget, &mut model); + state.lexical_owner(None, Span::default()).unwrap(); + assert!( + state + .lexical_owner(None, Span { start: 40, end: 41 }) + .is_err() + ); + assert_eq!(state.lexical_owners, [None]); + assert_eq!(state.reserved_owners, 1); + assert_eq!(state.budget.producer_entries(), Some(2)); +} +#[test] +fn internal_overflow_latch_and_syntax_work_precedence() { + let at = Span { start: 9, end: 10 }; + let mut budget = internal_budget(2); + budget.enable_entry_producers(1, 1).unwrap(); + let e = budget.entry(at).unwrap_err(); + assert_eq!(e.message, LIMIT); + assert_eq!(budget.producer_entry_overflow(Span::default()).span, at); + assert_eq!(budget.active().unwrap_err().message, LIMIT); + assert_eq!(budget.usage.entries, 0); + assert_eq!(budget.producer_entries(), Some(1)); + assert_eq!( + budget + .reserve_metadata(0, Span::default()) + .unwrap_err() + .span, + at + ); + let mut budget = internal_budget(0); + budget.enable_entry_producers(0, 0).unwrap(); + assert_eq!( + budget.entry(at).unwrap_err().message, + "Source8 syntax entries limit exceeded." + ); + let mut budget = internal_budget(100); + budget.enable_entry_producers(1, 1).unwrap(); + assert_eq!( + budget.reserve_metadata(usize::MAX, at).unwrap_err().message, + "Source8 producer entry size overflow." + ); + assert_eq!(budget.producer_entries(), Some(1)); + assert_eq!(budget.active().unwrap_err().span, at); + let mut budget = internal_budget(100); + assert!(budget.enable_entry_producers(usize::MAX, 1).is_err()); + assert_eq!(budget.producer_entries(), None); + assert!(budget.active().is_err()); + let mut budget = internal_budget(100); + budget.enable_entry_producers(0, 0).unwrap(); + let mut model = ModelCounts { + limits: ProjectLimits::default(), + graph: 0, + records: 0, + includes: 0, + }; + let mut state = make_state(&mut budget, &mut model); + state.pending_projection = usize::MAX; + assert_eq!( + state.projection(1, at).unwrap_err().message, + "Source8 producer entry size overflow." + ); + assert_eq!(state.pending_projection, usize::MAX); + assert!(state.budget.active().is_err()); +} +#[test] +fn explicit_zero_and_actual_owner_preserve_complete_oa_sa_and_old_routes() { + for (work, property) in [ + ("oa_activity", "oa_property"), + ("sa_function", "sa_property"), + ] { + let body = format!( + "calculation c \"C\" id \"00000000-0000-0000-0000-000000000003\" {{ input x id \"00000000-0000-0000-0000-000000000004\" : Number; result Number; body $x; }}\n{property} v \"V\" id \"00000000-0000-0000-0000-000000000002\" {{ subject @w; type Number; base @c(x: 1); }}\n{work} w \"W\" id \"00000000-0000-0000-0000-000000000005\" {{}}" + ); + let original = bundle(&body); + let owner = held(original.clone(), 4096); + for carried in [false, true] { + let seed = parse_text_producers(owner.input()).unwrap(); + let input = if carried { + owner.input() + } else { + CarriedInput::borrowed(&owner.bundle, &seed) + }; + let syntax = parse_entry_producers(input).unwrap(); + let input = if carried { + owner.input() + } else { + CarriedInput::borrowed(&owner.bundle, &seed) + }; + let prior = parse_text_producers(input).unwrap(); + assert_eq!(syntax.budget.usage, prior.budget.usage); + assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units()); + assert_eq!(syntax.metadata, prior.metadata); + assert_eq!(syntax.budget.producer_text(), prior.budget.producer_text()); + assert_eq!(syntax.budget.producer_entries(), Some(syntax.metadata)); + let result = project::resolve(syntax).unwrap(); + let prior = project::resolve(prior).unwrap(); + assert_eq!(result.project(), prior.project()); + assert_eq!(result.warnings(), prior.warnings()); + assert_eq!(result.phase_state().1, prior.phase_state().1); + } + assert_eq!(owner.bundle, original); + } + // Text-only and phase entry acceptance retain their old checkpoints. + for cap in [12, 11] { + let owner = held(bundle(""), cap); + let seed = parse_with_profile( + &owner.bundle, + owner.policy.project_limits, + owner.policy.limits, + ) + .unwrap(); + assert!(seed.budget.producer_entries().is_none()); + let result = parse_entry_producers(CarriedInput::borrowed(&owner.bundle, &seed)); + if cap == 12 { + assert_eq!(result.unwrap().metadata, 11); + } else { + diagnostic(&failed(result), "main.arcadia", 0, 1); + } + } + let low = held( + bundle("oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { }"), + 4, + ); + diagnostic( + &failed(parse_entry_producers(low.input())), + "main.arcadia", + 75, + 76, + ); + diagnostic( + &failed(parse_text_producers(low.input())), + "main.arcadia", + 0, + 1, + ); + diagnostic(&failed(parse_carried(low.input())), "main.arcadia", 0, 1); +} diff --git a/harness/analysis-entry-producer-implementation-20260925.md b/harness/analysis-entry-producer-implementation-20260925.md new file mode 100644 index 00000000..a99587ec --- /dev/null +++ b/harness/analysis-entry-producer-implementation-20260925.md @@ -0,0 +1,50 @@ +# Private retained-entry producer prerequisite + +Base `97a8703adc75467a7f8e75f5c6cb00ca542b707c`; implements the accepted +`harness/analysis-entry-producer-contract-20260925.md` in the parent source tree. +This is fixture-only phase/producer evidence, not emitted-output admission. + +The explicit entry route also enables text producers and derives C and exact +policy from the existing carried guard. Budget adds only optional scalar C/M. +Existing legacy, carried-phase and text-only entry paths retain their checkpoints. +U keeps its original event sites and work-limit precedence; prospective aggregate +checks latch without changing totals. F is installed before traversal. Private +State helpers reserve unique value-end keys and lexical owner occurrences before +mutation. Successful grammar events count projection slots with checked addition. +The new route reserves token-end slots and projection together, without the old +borrowed-work counting Vec. A scalar quota checks each projected envelope, field +and actual value before growth. Synthetic bodies remain included in seven. +Per-file actual map/owner lengths and final independent metadata reconcile M; +resolver C then U+M transfer and text reconciliation are unchanged. + +Six tests in `mixed::carried::entry_producer` cover independent literal E and H +syntax caps 13/12 and 25/24 separately from resolver 17/16 and 29/28, owner/value +prefix refusals 4/7, and the 344-byte nested syntax-only fixture (U13, M89, P30, +C1; 103/102). They check node/element envelopes, nested positions and owner order, +quota underflow/leftover, direct unchanged collections on refusal and repeated +map-key replacement. Labeled arithmetic controls exercise overflow, sticky-first +failure, prospective U and work-limit precedence. Whole OA/SA Project, warnings, +Usage, numeric work and retained totals are compared with text-only under actual +and explicit zero guards. Old-route diagnostics and original bytes are checked. +All byte policies are roomy 4096 so earlier text limits do not mask entry tests. + +Source review identified a possible internal overflow call overwriting an already +latched error. The helper now returns the first failure; a direct fault control +checks that behavior. Repeated map keys now perform a zero-delta reservation +before replacement, preserving a latched refusal without modifying the map; +a direct control checks that branch too. These were corrected before compilation, +not a runtime bug +observation. Test review also caught that the independent inventory walker adds +its own existing scratch slots: exact-cap ordinary syntax controls remain at +29/37/103, while a separate roomy owner of the same literal runs that walk. +No live policy is widened and no exact-cap resolver success is implied. +No prior tests, stack sizes, policies, inherited failure dispositions +or unrelated formatting are changed. Only touched Rust files were directly +formatted with skip_children. Parent owns compiler, full disposition, Clippy, +format and prologue/stack acceptance; no runtime result is claimed here. + +Allocator overhead, token storage under its existing token ceiling, delimiter and +other work stacks, numeric/decoder internals and model/validator heap remain +outside this retained metric. The extra shared projector wrapper and scalar +Budget/State fields require measured parent stack checks; no heap indirection or +larger stack is introduced. Final output acceptance remains a separate task.