Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edf096cd33 | ||
|
|
4872141c03 | ||
|
|
29417130c7 | ||
|
|
a629ae01d4 |
@@ -1028,6 +1028,18 @@ fn edit_positions_from_tokens_with_text(
|
||||
count: usize,
|
||||
value_ends: Option<&BTreeMap<usize, usize>>,
|
||||
text: impl FnMut(usize, Span) -> Result<(), Box<Diagnostic>>,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
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<usize, usize>>,
|
||||
mut quota: Option<usize>,
|
||||
text: impl FnMut(usize, Span) -> Result<(), Box<Diagnostic>>,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
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<usize>,
|
||||
value_ends: Option<&'a BTreeMap<usize, usize>>,
|
||||
text: H,
|
||||
source: &'a str,
|
||||
quota: &'a mut Option<usize>,
|
||||
}
|
||||
impl<F: Fn(usize) -> Lexeme, H: FnMut(usize, Span) -> Result<(), Box<Diagnostic>>>
|
||||
Positions<'_, F, H>
|
||||
{
|
||||
fn consume(&mut self, slots: usize, offset: usize) -> Result<(), Box<Diagnostic>> {
|
||||
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<EditValue, Box<Diagnostic>> {
|
||||
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::<Result<Vec<_>, _>>()?;
|
||||
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,
|
||||
|
||||
@@ -39,6 +39,11 @@ pub struct Budget {
|
||||
numeric: WorkMeter,
|
||||
failed: Option<Error>,
|
||||
text: Option<ProducerText>,
|
||||
entries: Option<ProducerEntries>,
|
||||
}
|
||||
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<usize> {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -68,8 +68,11 @@ pub(in crate::language) struct State<'a> {
|
||||
budget: &'a mut Budget,
|
||||
model: &'a mut ModelCounts,
|
||||
typed: Vec<declarations::Declaration>,
|
||||
pub(in crate::language) value_ends: BTreeMap<usize, usize>,
|
||||
pub(in crate::language) lexical_owners: Vec<Option<usize>>,
|
||||
value_ends: BTreeMap<usize, usize>,
|
||||
lexical_owners: Vec<Option<usize>>,
|
||||
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<usize>,
|
||||
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<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
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<carried::CarriedGuard<'s>>,
|
||||
text_producers: bool,
|
||||
entry_producers: bool,
|
||||
) -> Result<ParsedMixedSyntax<'s>, Vec<SourceDiagnostic>> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Fixture-constructible carried phase authority, not an emitted-output result.
|
||||
//! Complete pending-backed authority and fixture-only carried snapshots.
|
||||
//! Checks existing logical phase totals; does not meter every parser allocation.
|
||||
use super::*;
|
||||
use project::accounting::{Ledger, Slots};
|
||||
use project::ordinary_index::rename::emission::PendingRename;
|
||||
|
||||
struct SnapshotPolicy {
|
||||
limits: Limits,
|
||||
@@ -15,6 +16,7 @@ struct RetainedSnapshot {
|
||||
retained: Slots,
|
||||
}
|
||||
enum SnapshotBacking<'s> {
|
||||
Pending(&'s PendingRename<'s>),
|
||||
Borrowed {
|
||||
bundle: &'s SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
@@ -27,6 +29,7 @@ pub(super) struct CarriedInput<'s> {
|
||||
backing: SnapshotBacking<'s>,
|
||||
}
|
||||
enum GuardBacking<'s> {
|
||||
Pending(&'s PendingRename<'s>),
|
||||
Borrowed,
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
@@ -37,8 +40,26 @@ pub(super) struct CarriedGuard<'s> {
|
||||
backing: GuardBacking<'s>,
|
||||
}
|
||||
impl<'s> CarriedInput<'s> {
|
||||
pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self {
|
||||
Self {
|
||||
backing: SnapshotBacking::Pending(pending),
|
||||
}
|
||||
}
|
||||
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
|
||||
match self.backing {
|
||||
SnapshotBacking::Pending(pending) => {
|
||||
let (limits, project_limits) = pending.carried_policy();
|
||||
(
|
||||
pending.carried_bundle(),
|
||||
SnapshotPolicy {
|
||||
limits,
|
||||
project_limits,
|
||||
},
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Pending(pending),
|
||||
},
|
||||
)
|
||||
}
|
||||
SnapshotBacking::Borrowed { bundle, policy } => (
|
||||
bundle,
|
||||
policy,
|
||||
@@ -62,6 +83,7 @@ impl<'s> CarriedInput<'s> {
|
||||
impl CarriedGuard<'_> {
|
||||
pub(super) fn slots(&self) -> Slots {
|
||||
match &self.backing {
|
||||
GuardBacking::Pending(pending) => pending.carried_slots(),
|
||||
GuardBacking::Borrowed => Slots::default(),
|
||||
GuardBacking::Retained(owner) => owner.retained,
|
||||
}
|
||||
@@ -91,6 +113,7 @@ fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<S
|
||||
policy.limits,
|
||||
Some(guard),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,6 +129,22 @@ fn parse_text_producers(
|
||||
policy.limits,
|
||||
Some(guard),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Combined producer route, selected only by sealed pending or fixture authority.
|
||||
pub(super) fn parse_entry_producers(
|
||||
input: CarriedInput<'_>,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let (bundle, policy, guard) = input.into_parts();
|
||||
parse_impl(
|
||||
bundle,
|
||||
policy.project_limits,
|
||||
policy.limits,
|
||||
Some(guard),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,3 +202,6 @@ mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod text_producer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod entry_producer;
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
//! 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<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>) -> 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::<Vec<_>>(),
|
||||
[
|
||||
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 = match language::edit_positions_from_tokens_with_producers(
|
||||
file.source,
|
||||
&file.ordinary,
|
||||
|at| lexeme(file.tokens[at]),
|
||||
file.tokens.len(),
|
||||
None,
|
||||
Some(quota),
|
||||
|_, _| Ok(()),
|
||||
) {
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("injected projection quota must refuse"),
|
||||
};
|
||||
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);
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
//! Owned output only. No parser, extraction or accepted-result conversion.
|
||||
//! Owned unverified emission; consumed actual-output verification is a private child.
|
||||
pub(in crate::language::analysis_source) mod acceptance;
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::source_project::ProjectLimits;
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
//! Actual-byte verification and owning result; no installation or mutable extraction.
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Usage;
|
||||
use crate::language::analysis_source::mixed::{carried, project};
|
||||
use crate::source_project::SourceDiagnostic;
|
||||
|
||||
pub(in crate::language::analysis_source) struct AcceptedRename {
|
||||
bundle: SourceBundle,
|
||||
project: Project,
|
||||
warnings: Vec<SourceDiagnostic>,
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
retained: Slots,
|
||||
verification_usage: Usage,
|
||||
verification_numeric_units: u64,
|
||||
}
|
||||
impl AcceptedRename {
|
||||
pub fn bundle(&self) -> &SourceBundle {
|
||||
&self.bundle
|
||||
}
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
pub fn policy(&self) -> (Limits, ProjectLimits) {
|
||||
(self.limits, self.project_limits)
|
||||
}
|
||||
pub fn retained(&self) -> Slots {
|
||||
self.retained
|
||||
}
|
||||
pub fn verification_work(&self) -> (Usage, u64) {
|
||||
(self.verification_usage, self.verification_numeric_units)
|
||||
}
|
||||
}
|
||||
impl PendingRename<'_> {
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_bundle(&self) -> &SourceBundle {
|
||||
&self.bundle
|
||||
}
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_policy(
|
||||
&self,
|
||||
) -> (Limits, ProjectLimits) {
|
||||
(self.limits, self.project_limits)
|
||||
}
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_slots(&self) -> Slots {
|
||||
self.retained
|
||||
}
|
||||
pub fn verify(self) -> Result<AcceptedRename, Failure> {
|
||||
let (project, warnings, retained, verification_usage, verification_numeric_units) = {
|
||||
let syntax = carried::parse_entry_producers(carried::CarriedInput::from_pending(&self))
|
||||
.map_err(|diagnostics| Failure { diagnostics })?;
|
||||
let mut resolved = project::resolve(syntax)?;
|
||||
let equal = equal_target(&mut resolved, self.desired)?;
|
||||
if !equal {
|
||||
return Err(Failure::typed(
|
||||
&resolved.syntax,
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Verified output differs from the complete rename target.",
|
||||
),
|
||||
));
|
||||
}
|
||||
let retained = warning_inventory(&resolved.warnings)
|
||||
.and_then(|warnings| self.retained.add(warnings, Span::default()))
|
||||
.and_then(|retained| {
|
||||
check_destination(&resolved.syntax.budget, resolved.storage.live, retained)?;
|
||||
Ok(retained)
|
||||
})
|
||||
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
|
||||
let usage = resolved.syntax.budget.usage;
|
||||
let numeric = resolved.syntax.budget.numeric_units();
|
||||
let project::ResolvedMixedProject {
|
||||
syntax,
|
||||
project,
|
||||
warnings,
|
||||
expressions,
|
||||
occurrences,
|
||||
static_limits: _,
|
||||
static_usage: _,
|
||||
storage: _,
|
||||
} = resolved;
|
||||
// The full live ledger remains intact through the checks above.
|
||||
// Only scalar destination accounting escapes these actual drops.
|
||||
drop(expressions);
|
||||
drop(occurrences);
|
||||
drop(syntax);
|
||||
(project, warnings, retained, usage, numeric)
|
||||
};
|
||||
Ok(AcceptedRename {
|
||||
bundle: self.bundle,
|
||||
project,
|
||||
warnings,
|
||||
limits: self.limits,
|
||||
project_limits: self.project_limits,
|
||||
retained,
|
||||
verification_usage,
|
||||
verification_numeric_units,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Every actual Expr root is lowered into this arena. The old pending expression
|
||||
// count cannot authenticate parsed topology. Keep this boundary independently
|
||||
// testable without changing the inherited policy or instrumenting comparison.
|
||||
fn equal_target(
|
||||
resolved: &mut project::ResolvedMixedProject<'_>,
|
||||
desired: &Project,
|
||||
) -> Result<bool, Failure> {
|
||||
let frontier = Slots::entries(resolved.expressions.len());
|
||||
resolved
|
||||
.storage
|
||||
.reserve(&resolved.syntax.budget, frontier, Span::default())
|
||||
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
|
||||
let equal = resolved.project.eq(desired);
|
||||
resolved.storage.release(frontier);
|
||||
Ok(equal)
|
||||
}
|
||||
fn check_destination(budget: &Budget, live: Slots, retained: Slots) -> Result<(), TypedError> {
|
||||
let at = Span::default();
|
||||
if retained.entries > live.entries
|
||||
|| retained.occurrences > live.occurrences
|
||||
|| retained.text > live.text
|
||||
{
|
||||
return Err(error(
|
||||
at,
|
||||
"Accepted inventory exceeds live verification storage.",
|
||||
));
|
||||
}
|
||||
budget.lowering_entries(retained.entries, at)?;
|
||||
budget.lowering_occurrences(retained.occurrences, at)?;
|
||||
budget.index_bytes(retained.text, at)
|
||||
}
|
||||
fn warning_inventory(warnings: &[SourceDiagnostic]) -> Result<Slots, TypedError> {
|
||||
let mut inventory = Slots::default();
|
||||
for SourceDiagnostic {
|
||||
path,
|
||||
diagnostic,
|
||||
related,
|
||||
} in warnings
|
||||
{
|
||||
let language::Diagnostic {
|
||||
severity,
|
||||
code,
|
||||
object_id: _,
|
||||
field,
|
||||
line: _,
|
||||
column: _,
|
||||
message,
|
||||
span: _,
|
||||
} = diagnostic;
|
||||
if !related.is_empty() || *severity == validation::Severity::Error {
|
||||
return Err(error(Span::default(), "Unexpected accepted warning shape."));
|
||||
}
|
||||
let text = [
|
||||
path.len(),
|
||||
code.len(),
|
||||
field.as_ref().map_or(0, String::len),
|
||||
message.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.try_fold(0, |n, k| add(n, k, Span::default()))?;
|
||||
inventory = inventory.add(
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text,
|
||||
},
|
||||
Span::default(),
|
||||
)?;
|
||||
}
|
||||
Ok(inventory)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+845
@@ -0,0 +1,845 @@
|
||||
use super::*;
|
||||
use crate::language::{ParseLimits, analysis_source::mixed::parse_with_profile};
|
||||
use crate::{Layer, analysis as a};
|
||||
use project::tests::{id, rich_bundle, rich_model, single};
|
||||
const MISMATCH: &str = "Verified output differs from the complete rename target.";
|
||||
const NEXT: &str =
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
const UNNAMED: &str =
|
||||
"arcadia 8;\nproject next \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
fn limited(b: &SourceBundle, limits: Limits, policy: ProjectLimits) -> AuthoredMixedProject<'_> {
|
||||
project::resolve(parse_with_profile(b, policy, limits).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
}
|
||||
fn authored(b: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
limited(b, profile(100_000, 1_000_000), ProjectLimits::default())
|
||||
}
|
||||
fn expected(name: &str) -> Project {
|
||||
Project {
|
||||
schema_version: 8,
|
||||
id: id(1),
|
||||
alias: "next".into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
fn failure<T>(result: Result<T, Failure>) -> Failure {
|
||||
match result {
|
||||
Ok(_) => panic!("unexpected verification success"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
fn diagnostic(failure: &Failure, message: &str, start: usize, end: usize) {
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
let d = &failure.diagnostics[0];
|
||||
assert_eq!(d.path, "main.arcadia");
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.message, message);
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
fn same_policy(accepted: &AcceptedRename, limits: Limits, policy: ProjectLimits) {
|
||||
let (a, b) = accepted.policy();
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[
|
||||
limits.bytes,
|
||||
limits.tokens,
|
||||
limits.entries,
|
||||
limits.references,
|
||||
limits.frames
|
||||
]
|
||||
);
|
||||
assert_eq!(a.numeric_units, limits.numeric_units);
|
||||
assert_eq!(
|
||||
[
|
||||
b.max_files,
|
||||
b.max_include_depth,
|
||||
b.max_include_edges,
|
||||
b.model.max_source_bytes,
|
||||
b.model.max_graph_objects,
|
||||
b.model.max_records,
|
||||
b.model.max_step_references
|
||||
],
|
||||
[
|
||||
policy.max_files,
|
||||
policy.max_include_depth,
|
||||
policy.max_include_edges,
|
||||
policy.model.max_source_bytes,
|
||||
policy.model.max_graph_objects,
|
||||
policy.model.max_records,
|
||||
policy.model.max_step_references
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn actual_minimal_bytes_full_model_and_late_decode_boundary() {
|
||||
for bytes in [152, 151] {
|
||||
let b = single("");
|
||||
let before = b.clone();
|
||||
let limits = profile(17, bytes);
|
||||
let source = limited(&b, limits, ProjectLimits::default());
|
||||
assert_eq!(source.indexed.resolved.syntax.budget.usage.bytes, 78);
|
||||
let desired = expected("Project");
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
let result = pending.verify();
|
||||
if bytes == 152 {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(
|
||||
accepted.bundle(),
|
||||
&SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), NEXT.into())])
|
||||
}
|
||||
);
|
||||
assert_eq!(accepted.project(), &expected("Project"));
|
||||
assert!(accepted.warnings().is_empty());
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 105
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.verification_work(),
|
||||
(
|
||||
Usage {
|
||||
bytes: 81,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
same_policy(&accepted, limits, ProjectLimits::default());
|
||||
} else {
|
||||
let e = failure(result);
|
||||
diagnostic(&e, "Source8 index/output byte limit exceeded.", 37, 38);
|
||||
assert_eq!(
|
||||
(
|
||||
e.diagnostics[0].diagnostic.line,
|
||||
e.diagnostics[0].diagnostic.column
|
||||
),
|
||||
(2, 27)
|
||||
);
|
||||
}
|
||||
assert_eq!(b, before);
|
||||
assert_eq!(desired, expected("Project"));
|
||||
}
|
||||
}
|
||||
fn warning() -> SourceDiagnostic {
|
||||
SourceDiagnostic {
|
||||
path: "main.arcadia".into(),
|
||||
related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning,
|
||||
code: "unnamed".into(),
|
||||
object_id: Some(id(1)),
|
||||
field: Some("name".into()),
|
||||
message: "Give the project a descriptive name.".into(),
|
||||
span: Span { start: 11, end: 69 },
|
||||
line: 2,
|
||||
column: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn actual_warning_mapping_boundary_and_final_transfer() {
|
||||
for bytes in [161, 160] {
|
||||
let mut b = single("");
|
||||
b.files.insert(
|
||||
"main.arcadia".into(),
|
||||
"arcadia 8;\nproject p \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n"
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(b.files["main.arcadia"].len(), 71);
|
||||
let limits = profile(18, bytes);
|
||||
let source = limited(&b, limits, ProjectLimits::default());
|
||||
assert_eq!(
|
||||
source.warnings()[0].diagnostic.span,
|
||||
Span { start: 11, end: 66 }
|
||||
);
|
||||
let desired = expected("");
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
let result = pending.verify();
|
||||
if bytes == 161 {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(accepted.bundle().files["main.arcadia"], UNNAMED);
|
||||
assert_eq!(accepted.project(), &expected(""));
|
||||
assert_eq!(accepted.warnings(), [warning()]);
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 2,
|
||||
occurrences: 0,
|
||||
text: 157
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.verification_work(),
|
||||
(
|
||||
Usage {
|
||||
bytes: 74,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
same_policy(&accepted, limits, ProjectLimits::default());
|
||||
} else {
|
||||
let e = failure(result);
|
||||
diagnostic(&e, "Source8 index/output byte limit exceeded.", 11, 69);
|
||||
assert_eq!(
|
||||
(
|
||||
e.diagnostics[0].diagnostic.line,
|
||||
e.diagnostics[0].diagnostic.column
|
||||
),
|
||||
(2, 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn owning_result_survives_all_inputs_and_preserves_every_policy_field() {
|
||||
let limits = Limits {
|
||||
bytes: 2048,
|
||||
tokens: 101,
|
||||
entries: 99,
|
||||
references: 31,
|
||||
frames: 17,
|
||||
numeric_units: 8192,
|
||||
};
|
||||
let policy = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 3,
|
||||
max_include_edges: 5,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 3000,
|
||||
max_graph_objects: 67,
|
||||
max_records: 123,
|
||||
max_step_references: 37,
|
||||
},
|
||||
};
|
||||
let accepted = {
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
let pending = limited(&b, limits, policy)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
// A short pending borrow carries a longer independently owned target borrow.
|
||||
{
|
||||
let syntax =
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
|
||||
.unwrap();
|
||||
assert_eq!(syntax.bundle, &pending.bundle);
|
||||
drop(syntax);
|
||||
}
|
||||
pending.verify().unwrap()
|
||||
};
|
||||
assert_eq!(accepted.bundle().files["main.arcadia"], NEXT);
|
||||
assert_eq!(accepted.project(), &expected("Project"));
|
||||
assert!(accepted.warnings().is_empty());
|
||||
same_policy(&accepted, limits, policy);
|
||||
}
|
||||
#[test]
|
||||
fn equal_byte_wrong_models_malformed_and_admission_failure_are_distinct() {
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
for (start, end, replacement) in [(19, 23, "nope"), (25, 32, "Changed"), (8, 9, "?")] {
|
||||
let mut pending = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
pending
|
||||
.bundle
|
||||
.files
|
||||
.get_mut("main.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(start..end, replacement);
|
||||
assert_eq!(pending.bundle.files["main.arcadia"].len(), 81);
|
||||
if start != 8 {
|
||||
let actual = project::resolve(
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(actual.project(), &desired); // Wrong output really is admissible.
|
||||
drop(actual);
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
} else {
|
||||
let e = failure(pending.verify());
|
||||
assert_eq!(e.diagnostics[0].diagnostic.message, "Invalid source token.");
|
||||
}
|
||||
}
|
||||
let b = single(&format!(
|
||||
"oa_activity a \"A\" id \"{}\" {{ parent @b; }}\noa_activity b \"B\" id \"{}\" {{}}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
let text = pending.bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let at = text.find("parent @b").unwrap() + 8;
|
||||
text.replace_range(at..at + 1, "a");
|
||||
let syntax =
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap();
|
||||
drop(syntax); // This corruption is grammar-valid but model-invalid.
|
||||
let e = failure(pending.verify());
|
||||
assert!(
|
||||
e.diagnostics
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.code == "containment_cycle")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn actual_expression_arena_controls_equality_reservation() {
|
||||
let b = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body 10000; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = limited(&b, profile(4096, 4096), ProjectLimits::default());
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, 1);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 195
|
||||
}
|
||||
);
|
||||
pending
|
||||
.bundle
|
||||
.files
|
||||
.get_mut("main.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(160..165, "1 + 2");
|
||||
let mut actual = project::resolve(
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual.expressions.len(), 3);
|
||||
let before = actual.storage.live;
|
||||
assert!(!equal_target(&mut actual, &desired).unwrap());
|
||||
assert_eq!(actual.storage.live, before);
|
||||
// Labeled ledger fault: two free slots would fit old E1, but not actual E3.
|
||||
// This is not an end-to-end late cap or a policy mutation.
|
||||
actual.storage.live.entries = 4094;
|
||||
let e = equal_target(&mut actual, &desired).unwrap_err();
|
||||
diagnostic(&e, "Source8 lowering entry limit exceeded.", 0, 0);
|
||||
assert_eq!(actual.storage.live.entries, 4094);
|
||||
drop(actual);
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
}
|
||||
#[test]
|
||||
fn internal_warning_shape_and_destination_inventory_faults() {
|
||||
assert_eq!(
|
||||
warning_inventory(&[warning()]).unwrap(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 59
|
||||
}
|
||||
);
|
||||
let mut bad = warning();
|
||||
bad.related.push(crate::source_project::SourceLocation {
|
||||
path: "extra".into(),
|
||||
span: Span::default(),
|
||||
});
|
||||
assert_eq!(
|
||||
warning_inventory(&[bad]).unwrap_err().message,
|
||||
"Unexpected accepted warning shape."
|
||||
);
|
||||
let mut bad = warning();
|
||||
bad.diagnostic.severity = validation::Severity::Error;
|
||||
assert!(warning_inventory(&[bad]).is_err());
|
||||
let budget = Budget::new(profile(100, 4096)).unwrap();
|
||||
let live = Slots {
|
||||
entries: 3,
|
||||
occurrences: 2,
|
||||
text: 200,
|
||||
};
|
||||
for retained in [
|
||||
Slots { entries: 4, ..live },
|
||||
Slots {
|
||||
occurrences: 3,
|
||||
..live
|
||||
},
|
||||
Slots { text: 201, ..live },
|
||||
] {
|
||||
assert_eq!(
|
||||
check_destination(&budget, live, retained)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Accepted inventory exceeds live verification storage."
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
Slots::text(usize::MAX)
|
||||
.add(Slots::text(1), Span::default())
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
Slots::entries(usize::MAX)
|
||||
.add(Slots::entries(1), Span::default())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
fn rich_desired(layer: Layer) -> Project {
|
||||
let mut p = rich_model(layer);
|
||||
p.alias = "w".into();
|
||||
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
|
||||
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
|
||||
let d = p.diagrams.get_mut(&id(5)).unwrap();
|
||||
d.alias = "z".into();
|
||||
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
|
||||
for (n, name) in [(10, "g"), (11, "f")] {
|
||||
let def = p.definitions.get_mut(&id(n)).unwrap();
|
||||
def.alias = name.into();
|
||||
let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c.parameters[0].name = "y".into();
|
||||
c.parameters[1].name = "x".into();
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
|
||||
}
|
||||
p
|
||||
}
|
||||
// Independently authored entire expected files: no edit iterator or output text
|
||||
// contributes to these strings. The LF/CRLF transform applies to whole literals.
|
||||
const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n";
|
||||
const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n";
|
||||
fn rich_expected(layer: Layer) -> SourceBundle {
|
||||
let (property, work, diagram, extra) = if layer == Layer::Oa {
|
||||
("oa_property", "oa_activity", "", "")
|
||||
} else {
|
||||
(
|
||||
"sa_property",
|
||||
"sa_function",
|
||||
"layer Sa;",
|
||||
"sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}",
|
||||
)
|
||||
};
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("a.arcadia".into(), RICH_A.into()),
|
||||
("b.arcadia".into(), RICH_B.into()),
|
||||
(
|
||||
"main.arcadia".into(),
|
||||
format!(
|
||||
"arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n"
|
||||
),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn rich_oa_sa_swaps_unicode_crlf_complete_actual_bytes_and_models() {
|
||||
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
|
||||
for unicode in [false, true] {
|
||||
let mut b = rich_bundle(layer);
|
||||
let mut expected = rich_expected(layer);
|
||||
if unicode {
|
||||
for text in b.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
for text in expected.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let before = b.clone();
|
||||
let desired = rich_desired(layer);
|
||||
let accepted = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.bundle(), &expected);
|
||||
assert_eq!(accepted.project(), &rich_desired(layer));
|
||||
let (span, line) = if unicode {
|
||||
(
|
||||
Span {
|
||||
start: 351,
|
||||
end: 416,
|
||||
},
|
||||
7,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Span {
|
||||
start: 338,
|
||||
end: 403,
|
||||
},
|
||||
6,
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
accepted.warnings(),
|
||||
leaf_work_warnings("main.arcadia", span, line)
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 5,
|
||||
occurrences: 0,
|
||||
text: if unicode {
|
||||
unicode_bytes + 42 + 227
|
||||
} else {
|
||||
bytes + 42 + 227
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(b, before);
|
||||
assert_eq!(desired, rich_desired(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn leaf_work_warnings(path: &str, span: Span, line: usize) -> [SourceDiagnostic; 2] {
|
||||
// Allocation validation precedes advisory coverage. Both locate the complete
|
||||
// ordinary Work declaration; neither fixture supplies allocation or context.
|
||||
[
|
||||
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
|
||||
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
|
||||
].map(|(code, field, message)| SourceDiagnostic {
|
||||
path: path.into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning, code: code.into(),
|
||||
object_id: Some(id(2)), field: field.map(str::to_owned),
|
||||
message: message.into(), span, line, column: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
fn calculation(p: &mut Project) -> &mut a::CalculationDefinition {
|
||||
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(2)).unwrap().body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c
|
||||
}
|
||||
#[test]
|
||||
fn scoped_small_stack_actual_deep_wide_verification_and_drop() {
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let body = format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
|
||||
id(2)
|
||||
);
|
||||
let b = single(&body);
|
||||
let before = b.clone();
|
||||
let expected_bytes = format!(
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
);
|
||||
for mismatch in [false, true] {
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut target = desired.clone();
|
||||
if result == "List(Number)" {
|
||||
let a::Binding::Expression(a::Expr::List(items)) =
|
||||
&mut calculation(&mut target).body
|
||||
else {
|
||||
panic!("list")
|
||||
};
|
||||
items.pop(); // Actual left remains wide; target-only shape differs.
|
||||
} else {
|
||||
target.name = "Different".into();
|
||||
}
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, nodes);
|
||||
if mismatch {
|
||||
pending.desired = ⌖
|
||||
} // Labeled target-only fault after valid emission.
|
||||
std::thread::scope(|scope| {
|
||||
let expected = &expected_bytes;
|
||||
let desired = &desired;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
let result = pending.verify();
|
||||
if mismatch {
|
||||
diagnostic(&failure(result), MISMATCH, 0, 0);
|
||||
} else {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(&accepted.bundle().files["main.arcadia"], expected);
|
||||
assert_eq!(accepted.project(), desired);
|
||||
drop(accepted);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(b, before);
|
||||
}
|
||||
}
|
||||
// Genuine inherited151 profile reaches a late parser refusal inside worker.
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
let pending = limited(&b, profile(17, 151), ProjectLimits::default())
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
std::thread::scope(|scope| {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
diagnostic(
|
||||
&failure(pending.verify()),
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
37,
|
||||
38,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn zero_actual_expression_rejects_large_target_only_variants() {
|
||||
let b = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
|
||||
id(2)
|
||||
));
|
||||
let mut deep = a::Expr::Boolean(true);
|
||||
for _ in 0..2048 {
|
||||
deep = a::Expr::Unary {
|
||||
op: a::UnaryOp::Not,
|
||||
operand: Box::new(deep),
|
||||
};
|
||||
}
|
||||
for expression in [deep, a::Expr::List(vec![a::Expr::Boolean(true); 4096])] {
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut damaged = desired.clone();
|
||||
calculation(&mut damaged).body = a::Binding::Expression(expression);
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, 0);
|
||||
pending.desired = &damaged;
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn signed_zero_geometry_scalar_and_historical_equality_moves_actual_model() {
|
||||
let b = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "next".into();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.0;
|
||||
let accepted = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.project(), &desired);
|
||||
assert_eq!(
|
||||
accepted.project().diagrams[&id(5)].nodes[&id(4)]
|
||||
.position
|
||||
.x
|
||||
.to_bits(),
|
||||
0.0f32.to_bits()
|
||||
);
|
||||
let b = single(&format!(
|
||||
r#"
|
||||
oa_data_type a "A" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
sa_data_type b "B" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
trace t "T" id "{}" {{ oa @a; sa @b; kind Item; baseline {{
|
||||
oa {{ name "A"; description ""; }}; sa {{ name "B"; description ""; }};
|
||||
semantic {{ facets [{{ Data {{ RealMinimum {{ oa {{ Value 0.0; }}; sa {{ Value 0.0; }}; }}; }}; }}]; }};
|
||||
}}; }}
|
||||
"#,
|
||||
id(2),
|
||||
id(3),
|
||||
id(4)
|
||||
));
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
for e in desired.elements.values_mut() {
|
||||
let body = match &mut e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("data")
|
||||
};
|
||||
let crate::DataTypeDefinition::Real { minimum, .. } = definition else {
|
||||
panic!("real")
|
||||
};
|
||||
*minimum = Some(-0.0);
|
||||
*default = Some(crate::DataValue::Real(-0.0));
|
||||
}
|
||||
let facet = &mut desired
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets[0];
|
||||
let crate::semantic::FacetReview::Data(crate::semantic::DataReview::RealMinimum(review)) =
|
||||
facet
|
||||
else {
|
||||
panic!("historical")
|
||||
};
|
||||
review.oa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
review.sa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
let accepted = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.project(), &desired);
|
||||
for e in accepted.project().elements.values() {
|
||||
let body = match &e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition:
|
||||
crate::DataTypeDefinition::Real {
|
||||
minimum: Some(minimum),
|
||||
..
|
||||
},
|
||||
default: Some(crate::DataValue::Real(value)),
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("real")
|
||||
};
|
||||
assert_eq!(minimum.to_bits(), 0.0f64.to_bits());
|
||||
assert_eq!(value.to_bits(), 0.0f64.to_bits());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() {
|
||||
let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into();
|
||||
desired.elements.get_mut(&id(2)).unwrap().alias = "f".into();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
let pending = pending.verify().unwrap();
|
||||
assert_eq!(pending.project(), &desired);
|
||||
assert_eq!(
|
||||
pending.warnings(),
|
||||
leaf_work_warnings("b.arcadia", Span { start: 25, end: 90 }, 2)
|
||||
);
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 6,
|
||||
occurrences: 0,
|
||||
text: 526 + 221
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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.
|
||||
|
||||
Initial a629ae01 focused compilation failed before any test ran: the quota
|
||||
control's unwrap_err required Debug on its successful EditDeclaration vector.
|
||||
The test now matches Err explicitly and panics on unexpected Ok, preserving
|
||||
the same assertion without changing production types. Original compiler output
|
||||
and source-bound receipt remain with parent gate evidence.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Private actual-output rename acceptance
|
||||
|
||||
Base `29417130c71f70c4eaefe8ed4c879efba78eb72c`. Implements the parent-selected
|
||||
`harness/analysis-mixed-acceptance-contract-20260925.md` (root b86c8dbc) within the
|
||||
private experimental mixed source path. No public source8, save, history,
|
||||
installation or repeated carried-source emission is enabled.
|
||||
|
||||
A consumed PendingRename now verifies its actual complete bundle through the
|
||||
combined text/entry producer parser and existing complete resolver/admission.
|
||||
CarriedInput and its slim guard borrow that entire pending owner; covariance
|
||||
shortens the desired borrow to the verification scope without another parser
|
||||
lifetime, policy copy in guards, arbitrary tuple constructor or heap wrapper.
|
||||
Narrow immutable views supply the exact bundle, policy and retained O together.
|
||||
The existing fixture constructors and old parser modes remain available unchanged.
|
||||
|
||||
The actual resolved expression arena supplies E for equality reservation against
|
||||
its full live storage. Complete actual Project PartialEq compares every field
|
||||
with desired; old name-exempt equality and pending's original E are not used.
|
||||
A static entry/default-span diagnostic reports mismatch. Warning inventory uses
|
||||
an exhaustive borrowed diagnostic-field match, checks empty related/non-Error
|
||||
shapes and sums checked entries/text. O+W is checked as a componentwise live
|
||||
subset and against inherited ceilings without resetting or double-charging the
|
||||
full ledger. Actual model/warnings move after equality; expression/occurrence
|
||||
arenas, syntax and complete pending guard drop before the bundle moves into
|
||||
AcceptedRename. Its private fields have read-only accessors, exact policies,
|
||||
final retained slots and separate verification Usage/numeric work; no input or
|
||||
desired borrow remains.
|
||||
|
||||
Eleven new tests use the acceptance module filter. Independent minimal literal
|
||||
and whole model verify 152/151 through one unchanged profile; warning literals
|
||||
and full diagnostics verify 161/160 and final O+W=(2,0,157). Exact policy fields
|
||||
and a lexical scope dropping all inputs exercise returned ownership. Previously
|
||||
authored OA/SA/scoped-global swap and UTF-8/CRLF/all-file literals are reused,
|
||||
including an untouched fourth file. Equal-byte wrong alias/name, malformed
|
||||
source and parseable containment-cycle corruption separate equality, syntax and
|
||||
model-admission refusal. Actual E1->3 corruption plus a labeled internal ledger
|
||||
fault distinguishes reserving3 from trusting old1, without a fabricated late
|
||||
end-to-end equality cap. Other internal controls cover warning shapes, final
|
||||
subset and checked inventory overflow.
|
||||
|
||||
Scoped 128 KiB workers perform actual verification/assertions/drop for deep128
|
||||
and wide129 expression nodes (success and target-only mismatch), plus the genuine
|
||||
151-byte late refusal. All original prerequisites are prepared outside those
|
||||
workers. Separate E0 target-only deep/wide and signed-zero geometry/scalar/history
|
||||
cases exercise complete equality. Positive-zero bits in actual returned model
|
||||
help distinguish moving the parsed model from substituting desired intent.
|
||||
|
||||
Before execution, source review prompted correcting the carried module summary;
|
||||
parent readback caught an unnamed-source test slice, replaced with the exact
|
||||
independent 71-byte original literal. Local type inspection corrected the f32
|
||||
geometry bit assertion. These are pre-execution authoring corrections, not
|
||||
runtime failures. No prior tests or stack sizes are edited. Only touched Rust
|
||||
files were directly formatted with skip_children; compiler, full prior outcome
|
||||
comparison, Clippy, format and measured prologue/stack gates belong to the parent.
|
||||
No runtime result was claimed at that initial handoff; see the correction below.
|
||||
|
||||
The selected retained/decode/equality domain remains logical, not universal heap
|
||||
or before-every-allocation accounting. Existing traversal/path-copy, delimiter,
|
||||
ordinary-open, Pratt and duplicate-name scratch keep their independent controls
|
||||
and stated exclusions; model/validator/numeric/decoder internals and native stack
|
||||
remain separate domains. File counts do not tightly bound traversal path bytes.
|
||||
Actual lifetime coercion and stack headroom still require the parent gates.
|
||||
|
||||
## Initial focused failure and test-oracle correction
|
||||
|
||||
Parent's initial focused run of `4872141c036c16825f1c098bd7cf44709897e814`
|
||||
compiled successfully and completed 9 passes / 2 failures. The failing assertions
|
||||
incorrectly required empty warnings in the rich OA/SA and four-file fixtures.
|
||||
The original run, log and artifact remain preserved under the parent's
|
||||
`analysis-output-acceptance-gates-20260925/acceptance-focused-4872141-1` records;
|
||||
artifact cba18de5 prefix, 43,060,224 bytes. Those failures are not erased or called
|
||||
production failures. The other nine controls, including both late byte caps and
|
||||
scoped 128 KiB verification/drop, passed in that run.
|
||||
|
||||
Source-derived correction: validation.rs emits unallocated_leaf first, with
|
||||
field allocation and message "Allocate leaf 'Work' to a performer."; its later
|
||||
coverage.validate call emits work_without_behavior_context, field None, message
|
||||
"Include this leaf activity/function in a process/chain or scenario through a
|
||||
work action or exchange occurrence." Both are Warning, object ID2, empty related,
|
||||
and map to the full Work declaration. The first local derivation missed the
|
||||
coverage advisory; independent review caught it before the correction commit.
|
||||
The pre-existing project expected_leaf_work_warnings oracle confirms both/order.
|
||||
|
||||
New tests now require both complete mapped diagnostics, with independent literal
|
||||
spans: rich OA/SA main338..403 line6 column1, or Unicode/CRLF351..416 line7 column1;
|
||||
four-file b.arcadia25..90 line2 column1. Warning storage is (2,0,227) for main,
|
||||
(2,0,221) for b.arcadia. Final rich entries5/text1169,1212,1243,1286 for OA LF,
|
||||
OA transformed, SA LF, SA transformed; four-file entries6/text747. Production
|
||||
acceptance and all original test bodies remain unchanged. This is a test-only
|
||||
oracle repair plus this note; corrected runtime acceptance awaits parent gates.
|
||||
Reference in New Issue
Block a user