Compare commits

...
5 changed files with 1137 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,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 = &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_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,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.