Add private actual-output rename acceptance

This commit is contained in:
2026-09-24 19:58:09 -07:00
parent 29417130c7
commit 4872141c03
5 changed files with 1071 additions and 5 deletions
@@ -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,
}
@@ -111,9 +133,8 @@ fn parse_text_producers(
)
}
/// Explicit retained text and entry producer fixture route.
#[cfg(test)]
fn parse_entry_producers(
/// 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();
@@ -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;
@@ -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;
@@ -0,0 +1,807 @@
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));
assert!(accepted.warnings().is_empty());
assert_eq!(
accepted.retained(),
Slots {
entries: 3,
occurrences: 0,
text: if unicode {
unicode_bytes + 42
} else {
bytes + 42
}
}
);
assert_eq!(b, before);
assert_eq!(desired, rich_desired(layer));
}
}
}
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 = &target;
} // 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!(pending.warnings().is_empty());
assert_eq!(pending.bundle, expected);
assert_eq!(
pending.retained,
Slots {
entries: 4,
occurrences: 0,
text: 526
}
);
}