Plan complete mixed source renames against whole target models
This commit is contained in:
@@ -429,3 +429,5 @@ fn word(unit: &FileSyntax<'_>, at: Span, expected: &str) -> Result<(), TypedErro
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(in crate::language::analysis_source) mod rename;
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
//! Complete-target rename planning only; no output emission or admission.
|
||||
use super::*;
|
||||
use crate::source_project::SourceBundle;
|
||||
use lower::Target;
|
||||
|
||||
pub(in crate::language::analysis_source) struct RenamePlan<'s, 't> {
|
||||
source: AuthoredMixedProject<'s>,
|
||||
desired: &'t Project,
|
||||
names: BTreeMap<Target, &'t str>,
|
||||
edits: BTreeMap<OrdinaryKey, Target>,
|
||||
}
|
||||
impl<'s> AuthoredMixedProject<'s> {
|
||||
pub fn plan_rename<'t>(mut self, desired: &'t Project) -> Result<RenamePlan<'s, 't>, Failure> {
|
||||
let mut failure_file = None;
|
||||
let result = (|| {
|
||||
let resolved = &mut self.indexed.resolved;
|
||||
let e = resolved.expressions.len();
|
||||
resolved.storage.reserve(
|
||||
&resolved.syntax.budget,
|
||||
Slots::entries(e),
|
||||
Span::default(),
|
||||
)?;
|
||||
let equal = same_project(&resolved.project, desired);
|
||||
resolved.storage.release(Slots::entries(e));
|
||||
if !equal {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename target changes non-name project semantics.",
|
||||
));
|
||||
}
|
||||
let mut names = BTreeMap::new();
|
||||
let mut bytes = 0;
|
||||
visit_names(desired, &mut |target, name| {
|
||||
let old = self.indexed.data.names.get(&target).ok_or_else(|| {
|
||||
error(
|
||||
Span::default(),
|
||||
"Rename target is absent from the original catalog.",
|
||||
)
|
||||
})?;
|
||||
if old != name {
|
||||
bytes = add(bytes, name.len(), Span::default())?;
|
||||
self.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.budget
|
||||
.index_bytes(bytes, Span::default())?;
|
||||
if !crate::model::valid_alias(name) {
|
||||
return Err(error(Span::default(), "Invalid rename alias."));
|
||||
}
|
||||
self.indexed.resolved.storage.reserve(
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
Slots::entries(1),
|
||||
Span::default(),
|
||||
)?;
|
||||
if names.insert(target, name).is_some() {
|
||||
return Err(error(Span::default(), "Duplicate rename target."));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if !names.is_empty() {
|
||||
let mut namespace = BTreeSet::new();
|
||||
for (target, old) in &self.indexed.data.names {
|
||||
let scope = match target {
|
||||
Target::Global(_) => None,
|
||||
Target::Parameter { calculation, .. } => Some(*calculation),
|
||||
};
|
||||
let name = names.get(target).copied().unwrap_or(old);
|
||||
self.indexed.resolved.storage.reserve(
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
Slots::entries(1),
|
||||
Span::default(),
|
||||
)?;
|
||||
if !namespace.insert((scope, name)) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename aliases collide in their final namespace.",
|
||||
));
|
||||
}
|
||||
}
|
||||
let q = namespace.len();
|
||||
drop(namespace);
|
||||
self.indexed.resolved.storage.release(Slots::entries(q));
|
||||
}
|
||||
let mut edits = BTreeMap::new();
|
||||
for (key, location) in &self.indexed.data.occurrences {
|
||||
if names.contains_key(&key.target) {
|
||||
failure_file = Some(location.file);
|
||||
insert_edit(
|
||||
&mut self.indexed.resolved.storage,
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
&mut edits,
|
||||
(location.file, location.span.start, location.span.end),
|
||||
key.target,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for (&key, occurrence) in &self.ordinary {
|
||||
let target = Target::Global(occurrence.target);
|
||||
if names.contains_key(&target) {
|
||||
failure_file = Some(key.0);
|
||||
insert_edit(
|
||||
&mut self.indexed.resolved.storage,
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
&mut edits,
|
||||
key,
|
||||
target,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok((names, edits))
|
||||
})();
|
||||
let (names, edits) = result.map_err(|error| {
|
||||
Failure::index(
|
||||
&self.indexed.resolved.syntax,
|
||||
lower::Failure {
|
||||
file: failure_file,
|
||||
error,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let plan = RenamePlan {
|
||||
source: self,
|
||||
desired,
|
||||
names,
|
||||
edits,
|
||||
};
|
||||
plan.validate()?;
|
||||
Ok(plan)
|
||||
}
|
||||
}
|
||||
fn insert_edit(
|
||||
storage: &mut Ledger,
|
||||
budget: &Budget,
|
||||
edits: &mut BTreeMap<OrdinaryKey, Target>,
|
||||
key: OrdinaryKey,
|
||||
target: Target,
|
||||
) -> Result<(), TypedError> {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
if edits.contains_key(&key) {
|
||||
return Err(error(at, "Duplicate rename physical token."));
|
||||
}
|
||||
storage.reserve(budget, Slots::entries(1), at)?;
|
||||
edits.insert(key, target);
|
||||
Ok(())
|
||||
}
|
||||
impl<'s, 't> RenamePlan<'s, 't> {
|
||||
pub fn source(&self) -> &AuthoredMixedProject<'s> {
|
||||
&self.source
|
||||
}
|
||||
pub fn desired(&self) -> &'t Project {
|
||||
self.desired
|
||||
}
|
||||
pub fn changed_names(&self) -> &BTreeMap<Target, &'t str> {
|
||||
&self.names
|
||||
}
|
||||
pub fn edits(&self) -> &BTreeMap<OrdinaryKey, Target> {
|
||||
&self.edits
|
||||
}
|
||||
pub fn validate_current(&self, current: &SourceBundle) -> Result<(), Failure> {
|
||||
let old = self.source.bundle();
|
||||
if old.entry != current.entry || old.files != current.files {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Current complete bundle differs from the rename snapshot.",
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn failure(&self, file: Option<usize>, error: TypedError) -> Failure {
|
||||
Failure::index(
|
||||
&self.source.indexed.resolved.syntax,
|
||||
lower::Failure { file, error },
|
||||
)
|
||||
}
|
||||
fn validate(&self) -> Result<(), Failure> {
|
||||
let mut previous: Option<OrdinaryKey> = None;
|
||||
for (&key, target) in &self.edits {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
let checked = (|| {
|
||||
let unit = self
|
||||
.source
|
||||
.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.files
|
||||
.get(key.0)
|
||||
.ok_or_else(|| error(at, "Rename token has no original file."))?;
|
||||
let original = self
|
||||
.source
|
||||
.names()
|
||||
.get(target)
|
||||
.ok_or_else(|| error(at, "Rename token has no original target."))?;
|
||||
if !self.names.contains_key(target) {
|
||||
return Err(error(at, "Rename token targets an unchanged name."));
|
||||
}
|
||||
if at.start >= at.end {
|
||||
return Err(error(at, "Rename token must be nonempty."));
|
||||
}
|
||||
word(unit, at, original)?;
|
||||
if previous.is_some_and(|p| p.0 == key.0 && p.2 > key.1) {
|
||||
return Err(error(at, "Rename token ranges overlap."));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
checked.map_err(|e| self.failure(Some(key.0), e))?;
|
||||
previous = Some(key);
|
||||
}
|
||||
let mut required = 0;
|
||||
for (key, location) in self.source.typed_occurrences() {
|
||||
self.require(
|
||||
(location.file, location.span.start, location.span.end),
|
||||
key.target,
|
||||
&mut required,
|
||||
)?;
|
||||
}
|
||||
for (&key, occurrence) in self.source.ordinary_occurrences() {
|
||||
self.require(key, Target::Global(occurrence.target), &mut required)?;
|
||||
}
|
||||
if required != self.edits.len() {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(Span::default(), "Rename edit set contains extra tokens."),
|
||||
));
|
||||
}
|
||||
self.output_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
fn require(
|
||||
&self,
|
||||
key: OrdinaryKey,
|
||||
target: Target,
|
||||
required: &mut usize,
|
||||
) -> Result<(), Failure> {
|
||||
if self.names.contains_key(&target) {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
if self.edits.get(&key) != Some(&target) {
|
||||
return Err(self.failure(
|
||||
Some(key.0),
|
||||
error(at, "Rename edit set is missing a required target token."),
|
||||
));
|
||||
}
|
||||
*required = add(*required, 1, at).map_err(|e| self.failure(Some(key.0), e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn output_bytes(&self) -> Result<usize, Failure> {
|
||||
let result = (|| {
|
||||
let mut total = 0;
|
||||
let mut edits = self.edits.iter().peekable();
|
||||
for (file, _, original) in self.source.files() {
|
||||
let mut removed = 0;
|
||||
let mut replaced = 0;
|
||||
while let Some((&key, target)) = edits.peek().copied() {
|
||||
if key.0 != file {
|
||||
break;
|
||||
}
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
removed = add(
|
||||
removed,
|
||||
key.2
|
||||
.checked_sub(key.1)
|
||||
.ok_or_else(|| error(at, "Invalid rename token size."))?,
|
||||
at,
|
||||
)?;
|
||||
replaced = add(
|
||||
replaced,
|
||||
self.names
|
||||
.get(target)
|
||||
.ok_or_else(|| error(at, "Rename token has no replacement."))?
|
||||
.len(),
|
||||
at,
|
||||
)?;
|
||||
edits.next();
|
||||
}
|
||||
let size = original.len().checked_sub(removed).ok_or_else(|| {
|
||||
error(Span::default(), "Rename removals exceed original file.")
|
||||
})?;
|
||||
total = add(
|
||||
total,
|
||||
add(size, replaced, Span::default())?,
|
||||
Span::default(),
|
||||
)?;
|
||||
}
|
||||
if edits.next().is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename edit file is outside original bundle.",
|
||||
));
|
||||
}
|
||||
self.source
|
||||
.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.budget
|
||||
.index_bytes(total, Span::default())?;
|
||||
Ok(total)
|
||||
})();
|
||||
result.map_err(|e| self.failure(None, e))
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustive exclusion patterns make every new field an explicit gate decision.
|
||||
macro_rules! fixed {
|
||||
($old:expr, $new:expr, $ty:path, [$($field:ident),*], [$($excluded:ident),*]) => {{
|
||||
let $ty { $($field,)* $($excluded: _,)* } = $old;
|
||||
true $(&& $field == &$new.$field)*
|
||||
}};
|
||||
}
|
||||
fn map_same<T>(
|
||||
old: &BTreeMap<Id, T>,
|
||||
new: &BTreeMap<Id, T>,
|
||||
same: impl Fn(&T, &T) -> bool,
|
||||
) -> bool {
|
||||
old.len() == new.len()
|
||||
&& old
|
||||
.iter()
|
||||
.zip(new)
|
||||
.all(|((a, x), (b, y))| a == b && same(x, y))
|
||||
}
|
||||
fn same_project(a: &Project, b: &Project) -> bool {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
Project,
|
||||
[schema_version, id, name, description],
|
||||
[
|
||||
alias,
|
||||
definitions,
|
||||
elements,
|
||||
relations,
|
||||
diagrams,
|
||||
artifact_links,
|
||||
traces
|
||||
]
|
||||
) && map_same(&a.definitions, &b.definitions, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
analysis::Definition,
|
||||
[id, name, description],
|
||||
[alias, body]
|
||||
) && match (&a.body, &b.body) {
|
||||
(
|
||||
analysis::DefinitionBody::Calculation(a),
|
||||
analysis::DefinitionBody::Calculation(b),
|
||||
) => {
|
||||
a.parameters.len() == b.parameters.len()
|
||||
&& a.parameters
|
||||
.iter()
|
||||
.zip(&b.parameters)
|
||||
.all(|(a, b)| fixed!(a, b, analysis::Parameter, [id, value_type], [name]))
|
||||
&& fixed!(
|
||||
a,
|
||||
b,
|
||||
analysis::CalculationDefinition,
|
||||
[result, body],
|
||||
[parameters]
|
||||
)
|
||||
}
|
||||
(
|
||||
analysis::DefinitionBody::QuantityKind(a),
|
||||
analysis::DefinitionBody::QuantityKind(b),
|
||||
) => a == b,
|
||||
(analysis::DefinitionBody::Unit(a), analysis::DefinitionBody::Unit(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}) && map_same(&a.elements, &b.elements, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Element,
|
||||
[id, name, description, parent, body],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.relations, &b.relations, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Relation,
|
||||
[id, name, description, body],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.diagrams, &b.diagrams, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Diagram,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
layer,
|
||||
projection,
|
||||
view_options,
|
||||
routes
|
||||
],
|
||||
[alias, nodes]
|
||||
) && map_same(&a.nodes, &b.nodes, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::NodePlacement,
|
||||
[id, element, position, size, collapsed, port_anchors],
|
||||
[alias]
|
||||
)
|
||||
})
|
||||
}) && map_same(&a.artifact_links, &b.artifact_links, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::ArtifactLink,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
owner,
|
||||
relation,
|
||||
uri,
|
||||
revision,
|
||||
fragment
|
||||
],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.traces, &b.traces, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Trace,
|
||||
[id, name, description, oa, sa, kind, baseline],
|
||||
[alias]
|
||||
)
|
||||
})
|
||||
}
|
||||
fn visit_names<'t>(
|
||||
project: &'t Project,
|
||||
f: &mut impl FnMut(Target, &'t str) -> Result<(), TypedError>,
|
||||
) -> Result<(), TypedError> {
|
||||
f(Target::Global(project.id), &project.alias)?;
|
||||
for d in project.definitions.values() {
|
||||
f(Target::Global(d.id), &d.alias)?;
|
||||
if let analysis::DefinitionBody::Calculation(c) = &d.body {
|
||||
for p in &c.parameters {
|
||||
f(
|
||||
Target::Parameter {
|
||||
calculation: d.id,
|
||||
parameter: p.id,
|
||||
},
|
||||
&p.name,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in project.elements.values() {
|
||||
f(Target::Global(e.id), &e.alias)?;
|
||||
}
|
||||
for r in project.relations.values() {
|
||||
f(Target::Global(r.id), &r.alias)?;
|
||||
}
|
||||
for d in project.diagrams.values() {
|
||||
f(Target::Global(d.id), &d.alias)?;
|
||||
for n in d.nodes.values() {
|
||||
f(Target::Global(n.id), &n.alias)?;
|
||||
}
|
||||
}
|
||||
for a in project.artifact_links.values() {
|
||||
f(Target::Global(a.id), &a.alias)?;
|
||||
}
|
||||
for t in project.traces.values() {
|
||||
f(Target::Global(t.id), &t.alias)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+952
@@ -0,0 +1,952 @@
|
||||
use super::super::super::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::language::analysis_source::mixed::parse_with_profile;
|
||||
use crate::source_project::ProjectLimits;
|
||||
use crate::{Layer, analysis as a};
|
||||
|
||||
fn authored(bundle: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap().index_ordinary().unwrap()
|
||||
}
|
||||
fn calculation(p: &mut Project, n: usize) -> &mut a::CalculationDefinition {
|
||||
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(n)).unwrap().body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c
|
||||
}
|
||||
fn renamed(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")] {
|
||||
p.definitions.get_mut(&id(n)).unwrap().alias = name.into();
|
||||
let c = calculation(&mut p, n);
|
||||
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
|
||||
}
|
||||
fn rejected(bundle: &SourceBundle, desired: &Project, contains: &str) {
|
||||
let e = match authored(bundle).plan_rename(desired) {
|
||||
Ok(_) => panic!("unexpected accepted target"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics.len(), 1);
|
||||
assert!(
|
||||
e.diagnostics[0].diagnostic.message.contains(contains),
|
||||
"{:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn noop_borrows_exact_target_and_preserves_source_usage_and_storage() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let source = authored(&bundle);
|
||||
let desired = rich_model(Layer::Oa);
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let numeric = source.indexed.resolved.syntax.budget.numeric_units();
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert!(std::ptr::eq(plan.desired(), &desired));
|
||||
assert!(std::ptr::eq(plan.source().bundle(), &bundle));
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(plan.changed_names().is_empty());
|
||||
assert_eq!(plan.source.indexed.resolved.storage.live, before);
|
||||
assert_eq!(plan.source.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(desired, rich_model(Layer::Oa));
|
||||
assert_eq!(plan.source.project(), &desired);
|
||||
assert_eq!(plan.output_bytes().unwrap(), 900);
|
||||
}
|
||||
#[test]
|
||||
fn final_names_reject_unchanged_collisions_invalid_names_and_scoped_collisions() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
for alias in ["w", "", "雨", "project", "bad-name"] {
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = alias.into();
|
||||
rejected(
|
||||
&bundle,
|
||||
&desired,
|
||||
if alias == "w" { "collide" } else { "Invalid" },
|
||||
);
|
||||
}
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
calculation(&mut desired, 10).parameters[0].name = "y".into();
|
||||
rejected(&bundle, &desired, "collide");
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
calculation(&mut desired, 10).parameters[0].name = "p".into();
|
||||
calculation(&mut desired, 11).parameters[0].name = "p".into();
|
||||
let plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.changed_names().len(), 2);
|
||||
}
|
||||
#[test]
|
||||
fn complete_model_gate_rejects_identity_collection_parameter_and_payload_changes() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
type Mutation = Box<dyn Fn(&mut Project)>;
|
||||
let mutations: Vec<Mutation> = vec![
|
||||
Box::new(|p| p.schema_version = 7),
|
||||
Box::new(|p| p.id = id(99)),
|
||||
Box::new(|p| p.name.push('!')),
|
||||
Box::new(|p| p.description.push('!')),
|
||||
Box::new(|p| {
|
||||
p.elements.remove(&id(2));
|
||||
}),
|
||||
Box::new(|p| {
|
||||
let e = p.elements.remove(&id(2)).unwrap();
|
||||
p.elements.insert(id(99), e);
|
||||
}),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().parent = Some(id(3))),
|
||||
Box::new(|p| {
|
||||
p.elements.get_mut(&id(2)).unwrap().body =
|
||||
crate::ElementBody::Sa(crate::SaBody::Function)
|
||||
}),
|
||||
Box::new(|p| p.definitions.get_mut(&id(10)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.definitions.get_mut(&id(10)).unwrap().name.push('!')),
|
||||
Box::new(|p| {
|
||||
p.definitions
|
||||
.get_mut(&id(10))
|
||||
.unwrap()
|
||||
.description
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).parameters.swap(0, 1)),
|
||||
Box::new(|p| {
|
||||
calculation(p, 10).parameters.pop();
|
||||
}),
|
||||
Box::new(|p| {
|
||||
let q = calculation(p, 10).parameters[0].clone();
|
||||
calculation(p, 10).parameters.push(q);
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).parameters[0].id = a::ParameterId(id(99))),
|
||||
Box::new(|p| {
|
||||
calculation(p, 10).parameters[0].value_type =
|
||||
a::AnalysisType::Scalar(a::ScalarType::Boolean)
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).result = a::AnalysisType::Scalar(a::ScalarType::Boolean)),
|
||||
Box::new(|p| calculation(p, 10).body = a::Binding::Unbound),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().layer = Layer::Sa),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(id(99))
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams.get_mut(&id(5)).unwrap().nodes.clear();
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.id = id(99)
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.element = id(2)
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = 1.
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.size
|
||||
.width = 1.
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.collapsed = true
|
||||
}),
|
||||
];
|
||||
for mutate in mutations {
|
||||
let mut p = rich_model(Layer::Oa);
|
||||
mutate(&mut p);
|
||||
rejected(&bundle, &p, "non-name");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn complete_current_bundle_rejects_entry_paths_missing_extra_and_untouched_bytes() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "renamed".into();
|
||||
let plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.validate_current(&bundle).unwrap();
|
||||
for case in 0..6 {
|
||||
let mut current = bundle.clone();
|
||||
match case {
|
||||
0 => current.entry = "a.arcadia".into(),
|
||||
1 => {
|
||||
current.files.remove("a.arcadia");
|
||||
}
|
||||
2 => {
|
||||
current.files.insert("extra.arcadia".into(), String::new());
|
||||
}
|
||||
3 => current.files.get_mut("a.arcadia").unwrap().push(' '),
|
||||
4 => current.files.get_mut("main.arcadia").unwrap().push(' '),
|
||||
_ => {
|
||||
let s = current.files.remove("a.arcadia").unwrap();
|
||||
current.files.insert("renamed.arcadia".into(), s);
|
||||
}
|
||||
}
|
||||
assert!(plan.validate_current(¤t).is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn exact_merged_map_cap_passes_all_predecessors_and_retains_r_plus_m() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let desired = renamed(Layer::Oa);
|
||||
let baseline = authored(&bundle).indexed.resolved.storage.live;
|
||||
for (extra, fits) in [(37, false), (38, true)] {
|
||||
let profile = Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
assert_eq!(source.indexed.resolved.storage.live, baseline);
|
||||
let result = source.plan_rename(&desired);
|
||||
if fits {
|
||||
let plan = result.unwrap();
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
baseline.add(Slots::entries(38), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("cap accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 412,
|
||||
end: 413
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn damaged_complete_edits_fail_missing_extra_wrong_target_and_utf8_authority() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
for s in bundle.files.values_mut() {
|
||||
*s = format!("// 雨\r\n{s}");
|
||||
}
|
||||
let desired = renamed(Layer::Oa);
|
||||
for case in 0..7 {
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
let key = *plan.edits.keys().next().unwrap();
|
||||
match case {
|
||||
0 => {
|
||||
plan.edits.remove(&key);
|
||||
}
|
||||
1 => {
|
||||
plan.edits.insert((0, 0, 1), Target::Global(id(10)));
|
||||
}
|
||||
2 => {
|
||||
plan.edits.insert(key, Target::Global(id(11)));
|
||||
}
|
||||
3 => {
|
||||
plan.edits.insert((0, 4, 5), Target::Global(id(10)));
|
||||
}
|
||||
4 => {
|
||||
plan.edits
|
||||
.insert((99, key.1, key.2), Target::Global(id(10)));
|
||||
}
|
||||
5 => {
|
||||
plan.edits
|
||||
.insert((0, key.1, key.2 + 1), Target::Global(id(10)));
|
||||
}
|
||||
_ => {
|
||||
plan.edits.insert((0, key.1, key.1), Target::Global(id(10)));
|
||||
}
|
||||
}
|
||||
let failure = plan.validate().unwrap_err();
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
if case == 3 || case == 4 {
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].path,
|
||||
if case == 3 {
|
||||
"a.arcadia"
|
||||
} else {
|
||||
"main.arcadia"
|
||||
}
|
||||
);
|
||||
}
|
||||
if case == 0 {
|
||||
assert_eq!(failure.diagnostics[0].path, "a.arcadia");
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span,
|
||||
Span { start: 20, end: 21 }
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Rename edit set is missing a required target token."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn categories() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
include_str!("../tests/fixtures/categories.arcadia").into(),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn every_ordinary_record_field_group_is_fixed_including_historical_payloads() {
|
||||
let bundle = categories();
|
||||
let original = authored(&bundle).project().clone();
|
||||
type Mutation = Box<dyn Fn(&mut Project)>;
|
||||
let changes: Vec<Mutation> = vec![
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().description.push('!')),
|
||||
Box::new(|p| {
|
||||
let body = p.relations[&id(15)].body.clone();
|
||||
p.relations.get_mut(&id(6)).unwrap().body = body;
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.relations.remove(&id(6));
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams.get_mut(&id(7)).unwrap().projection = crate::ProjectionKind::Allocation
|
||||
}),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(7)).unwrap().routes.clear()),
|
||||
Box::new(|p| {
|
||||
p.diagrams.remove(&id(7));
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().name.push('!')),
|
||||
Box::new(|p| {
|
||||
p.artifact_links
|
||||
.get_mut(&id(8))
|
||||
.unwrap()
|
||||
.description
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().owner = id(3)),
|
||||
Box::new(|p| {
|
||||
p.artifact_links.get_mut(&id(8)).unwrap().relation = crate::ArtifactRelation::Verifies
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().uri.push('!')),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().revision = Some("changed".into())),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().fragment = Some("changed".into())),
|
||||
Box::new(|p| {
|
||||
p.artifact_links.remove(&id(8));
|
||||
}),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().oa = id(4)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().sa = id(4)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().kind = crate::TraceKind::Refinement),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().baseline = None),
|
||||
Box::new(|p| {
|
||||
p.traces
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = None
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.traces
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.oa
|
||||
.name
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.traces.remove(&id(9));
|
||||
}),
|
||||
];
|
||||
for change in changes {
|
||||
let mut p = original.clone();
|
||||
change(&mut p);
|
||||
rejected(&bundle, &p, "non-name");
|
||||
}
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
drop(source.plan_rename(&original).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn combined_shrink_offsets_typed_growth_and_counts_untouched_files() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main.replace("node n ", &format!("node {} ", "n".repeat(31)));
|
||||
assert_eq!(bundle.files.values().map(String::len).sum::<usize>(), 930);
|
||||
for (name, expected) in [("renamed_f", Some(924)), ("renamed_func", None)] {
|
||||
let profile = Limits {
|
||||
bytes: 930,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
let mut desired = source.project().clone();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.alias = "n".into();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = name.into();
|
||||
let result = source.plan_rename(&desired);
|
||||
if let Some(bytes) = expected {
|
||||
let plan = result.unwrap();
|
||||
assert_eq!(plan.output_bytes().unwrap(), bytes);
|
||||
assert_eq!(plan.edits().len(), 4);
|
||||
} else {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("oversize accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn deep_and_wide_equality_and_late_refusal_drop_on_small_stack() {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
|
||||
let original = source.project().clone();
|
||||
drop(source.plan_rename(&original).unwrap());
|
||||
let mut desired = original.clone();
|
||||
desired.definitions.get_mut(&id(2)).unwrap().alias = "p".into();
|
||||
rejected(&bundle, &desired, "collide");
|
||||
let mut desired = original.clone();
|
||||
calculation(&mut desired, 2).body = a::Binding::Unbound;
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn zero_expression_original_rejects_target_only_deep_and_wide_variants() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
let original = source.project().clone();
|
||||
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 mut desired = original.clone();
|
||||
calculation(&mut desired, 2).body = a::Binding::Expression(expression);
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn signed_zero_geometry_uses_existing_semantic_equality() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.;
|
||||
assert!(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.edits()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
fn parameter(c: usize, p: usize) -> Target {
|
||||
Target::Parameter {
|
||||
calculation: id(c),
|
||||
parameter: a::ParameterId(id(p)),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_entire_rich_maps_oa_sa_unicode_crlf_and_borrowed_names() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for crlf in [false, true] {
|
||||
let mut bundle = rich_bundle(layer);
|
||||
if crlf {
|
||||
for source in bundle.files.values_mut() {
|
||||
*source = format!("// 雨\r\n{}", source.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let desired = renamed(layer);
|
||||
let source = authored(&bundle);
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
let mut names = BTreeMap::from([
|
||||
(Target::Global(id(1)), "w"),
|
||||
(Target::Global(id(2)), "p"),
|
||||
(Target::Global(id(3)), "n"),
|
||||
(Target::Global(id(4)), "v"),
|
||||
(Target::Global(id(5)), "z"),
|
||||
(Target::Global(id(10)), "g"),
|
||||
(Target::Global(id(11)), "f"),
|
||||
(parameter(10, 1), "y"),
|
||||
(parameter(10, 12), "x"),
|
||||
(parameter(11, 1), "y"),
|
||||
(parameter(11, 12), "x"),
|
||||
]);
|
||||
if layer == Layer::Sa {
|
||||
names.insert(Target::Global(id(6)), "u");
|
||||
}
|
||||
let mut edits = BTreeMap::new();
|
||||
let offsets = if crlf {
|
||||
[20, 77, 138, 214, 219]
|
||||
} else {
|
||||
[12, 68, 128, 203, 208]
|
||||
};
|
||||
for (file, calculation) in [(0, 10), (1, 11)] {
|
||||
for (start, target) in offsets.into_iter().zip([
|
||||
Target::Global(id(calculation)),
|
||||
parameter(calculation, 1),
|
||||
parameter(calculation, 12),
|
||||
parameter(calculation, 1),
|
||||
parameter(calculation, 12),
|
||||
]) {
|
||||
edits.insert((file, start, start + 1), target);
|
||||
}
|
||||
}
|
||||
let offsets = if crlf {
|
||||
[
|
||||
28, 133, 188, 200, 218, 281, 303, 305, 311, 315, 317, 323, 333, 335, 341, 363,
|
||||
426,
|
||||
]
|
||||
} else {
|
||||
[
|
||||
19, 122, 177, 189, 206, 269, 291, 293, 299, 303, 305, 311, 321, 323, 329, 350,
|
||||
412,
|
||||
]
|
||||
};
|
||||
let g = |n| Target::Global(id(n));
|
||||
for (start, target) in offsets.into_iter().zip([
|
||||
g(1),
|
||||
g(4),
|
||||
g(5),
|
||||
g(3),
|
||||
g(3),
|
||||
g(2),
|
||||
g(10),
|
||||
parameter(10, 12),
|
||||
parameter(10, 1),
|
||||
g(11),
|
||||
parameter(11, 1),
|
||||
parameter(11, 12),
|
||||
g(10),
|
||||
parameter(10, 1),
|
||||
parameter(10, 12),
|
||||
g(2),
|
||||
g(5),
|
||||
]) {
|
||||
edits.insert((2, start, start + 1), target);
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
let start = if crlf { 505 } else { 490 };
|
||||
edits.insert((2, start, start + 1), g(6));
|
||||
}
|
||||
assert_eq!(plan.changed_names(), &names);
|
||||
assert_eq!(plan.edits(), &edits);
|
||||
let delta = if layer == Layer::Oa { 38 } else { 40 };
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(delta), Span::default()).unwrap()
|
||||
);
|
||||
assert_eq!(plan.source.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert!(std::ptr::eq(plan.desired(), &desired));
|
||||
assert_eq!(desired, renamed(layer));
|
||||
assert_eq!(plan.source.project(), &rich_model(layer));
|
||||
assert!(std::ptr::eq(
|
||||
plan.names[&g(1)].as_ptr(),
|
||||
desired.alias.as_ptr()
|
||||
));
|
||||
assert_eq!(
|
||||
plan.output_bytes().unwrap(),
|
||||
match (layer, crlf) {
|
||||
(Layer::Oa, false) => 900,
|
||||
(Layer::Oa, true) => 943,
|
||||
(Layer::Sa, false) => 974,
|
||||
(Layer::Sa, true) => 1017,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_entire_category_map_includes_comment_separated_refs_and_excludes_history() {
|
||||
let bundle = categories();
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
let replacements = [
|
||||
"a", "p", "implicit", "child", "link", "s", "note", "d", "q", "t", "use", "sub", "right",
|
||||
"left", "ctx",
|
||||
];
|
||||
desired.alias = replacements[0].into();
|
||||
for e in desired.elements.values_mut() {
|
||||
e.alias = replacements[(1..=15).find(|n| id(*n) == e.id).unwrap() - 1].into();
|
||||
}
|
||||
for r in desired.relations.values_mut() {
|
||||
r.alias = replacements[(1..=15).find(|n| id(*n) == r.id).unwrap() - 1].into();
|
||||
}
|
||||
desired.diagrams.get_mut(&id(7)).unwrap().alias = "note".into();
|
||||
desired.artifact_links.get_mut(&id(8)).unwrap().alias = "d".into();
|
||||
desired.traces.get_mut(&id(9)).unwrap().alias = "q".into();
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
let spans: &[(usize, &[(usize, usize)])] = &[
|
||||
(1, &[(19, 20)]),
|
||||
(2, &[(168, 169), (344, 345)]),
|
||||
(3, &[(249, 254), (625, 630), (992, 997), (1741, 1746)]),
|
||||
(4, &[(369, 377), (640, 648), (1882, 1890)]),
|
||||
(5, &[(487, 488), (1003, 1004)]),
|
||||
(6, &[(561, 565), (741, 745), (789, 793), (885, 889)]),
|
||||
(7, &[(664, 665)]),
|
||||
(8, &[(818, 822)]),
|
||||
(9, &[(925, 926)]),
|
||||
(
|
||||
10,
|
||||
&[(1328, 1329), (1577, 1578), (1855, 1856), (2004, 2005)],
|
||||
),
|
||||
(11, &[(1422, 1425), (1617, 1620), (1712, 1715)]),
|
||||
(12, &[(1391, 1394), (1514, 1517), (2029, 2032)]),
|
||||
(13, &[(1487, 1491), (1648, 1652), (2047, 2051)]),
|
||||
(14, &[(1397, 1402), (1789, 1794), (2092, 2097)]),
|
||||
(15, &[(1935, 1942)]),
|
||||
];
|
||||
let expected: BTreeMap<_, _> = spans
|
||||
.iter()
|
||||
.flat_map(|(n, spans)| {
|
||||
spans
|
||||
.iter()
|
||||
.map(move |&(start, end)| ((0, start, end), Target::Global(id(*n))))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(plan.edits(), &expected);
|
||||
let names: BTreeMap<_, _> = replacements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &name)| (Target::Global(id(i + 1)), name))
|
||||
.collect();
|
||||
assert_eq!(plan.changed_names(), &names);
|
||||
assert_eq!(plan.output_bytes().unwrap(), 2145);
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(51), Span::default()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_zero_scalar_bounds_defaults_and_historical_facets_preserve_original_bytes() {
|
||||
let bundle = 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(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
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 real")
|
||||
};
|
||||
review.oa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
review.sa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(std::ptr::eq(plan.source.bundle(), &bundle));
|
||||
}
|
||||
#[test]
|
||||
fn nonzero_expression_lists_calls_and_definition_payloads_reject_changes() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result List(Number); body [1,2]; }}",
|
||||
id(2)
|
||||
));
|
||||
let original = authored(&bundle).project().clone();
|
||||
for bigger in [false, true] {
|
||||
let mut desired = original.clone();
|
||||
let a::Binding::Expression(a::Expr::List(items)) = &mut calculation(&mut desired, 2).body
|
||||
else {
|
||||
panic!("list")
|
||||
};
|
||||
if bigger {
|
||||
items.push(a::Expr::Boolean(true));
|
||||
} else {
|
||||
items.pop();
|
||||
}
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
let crate::ElementBody::Oa(crate::OaBody::Shared(crate::SharedBody::EngineeringProperty {
|
||||
base: a::Binding::Expression(a::Expr::Binary { left, .. }),
|
||||
..
|
||||
})) = &mut desired.elements.get_mut(&id(3)).unwrap().body
|
||||
else {
|
||||
panic!("property")
|
||||
};
|
||||
let a::Expr::Call { arguments, .. } = left.as_mut() else {
|
||||
panic!("call")
|
||||
};
|
||||
arguments.remove(&a::ParameterId(id(1)));
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.port_anchors
|
||||
.push(crate::PortAnchor {
|
||||
port: id(99),
|
||||
side: crate::PortAnchorSide::Left,
|
||||
offset: 0.,
|
||||
});
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
// Opaque quantity/unit payload comparison is tested directly, independently
|
||||
// of admission's semantic dimension rules; these are gate-level controls.
|
||||
let mut old = rich_model(Layer::Oa);
|
||||
old.definitions.get_mut(&id(10)).unwrap().body =
|
||||
a::DefinitionBody::QuantityKind(a::QuantityKindDefinition {
|
||||
dimension: crate::analysis_numeric::Dimension::ZERO,
|
||||
algebra: a::QuantityAlgebra::Linear,
|
||||
});
|
||||
let mut changed = old.clone();
|
||||
let a::DefinitionBody::QuantityKind(q) =
|
||||
&mut changed.definitions.get_mut(&id(10)).unwrap().body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
q.algebra = a::QuantityAlgebra::Affine {
|
||||
difference: a::DefinitionRef::Local(id(11)),
|
||||
};
|
||||
assert!(!same_project(&old, &changed));
|
||||
old.definitions.get_mut(&id(10)).unwrap().body = a::DefinitionBody::Unit(a::UnitDefinition {
|
||||
kind: a::DefinitionRef::Local(id(11)),
|
||||
scale: crate::analysis_numeric::Exact::from_integer(1),
|
||||
offset: crate::analysis_numeric::Exact::from_integer(0),
|
||||
symbol: "u".into(),
|
||||
});
|
||||
let mut changed = old.clone();
|
||||
let a::DefinitionBody::Unit(u) = &mut changed.definitions.get_mut(&id(10)).unwrap().body else {
|
||||
unreachable!()
|
||||
};
|
||||
u.symbol.push('x');
|
||||
assert!(!same_project(&old, &changed));
|
||||
assert!(!same_project(&old, &rich_model(Layer::Oa)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_spelling_different_callee_and_unchanged_target_corruptions_are_rejected() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let desired = renamed(Layer::Oa);
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.insert((2, 299, 300), parameter(11, 1));
|
||||
let e = plan.validate().unwrap_err();
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 299,
|
||||
end: 300
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Rename edit set is missing a required target token."
|
||||
);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "new_project".into();
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.insert((2, 122, 123), Target::Global(id(4)));
|
||||
let e = plan.validate().unwrap_err();
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 122,
|
||||
end: 123
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Rename token targets an unchanged name."
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn included_file_last_reservation_preserves_its_canonical_diagnostic() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let start = main.find("diagram d ").unwrap();
|
||||
let end = start + main[start..].find('\n').unwrap() + 1;
|
||||
let diagram = main[start..end].to_owned();
|
||||
main.replace_range(start..end, "include \"z.arcadia\";\n");
|
||||
bundle.files.insert("z.arcadia".into(), diagram);
|
||||
let baseline = authored(&bundle).indexed.resolved.storage.live;
|
||||
let desired = renamed(Layer::Oa);
|
||||
// The unchanged-profile pipeline must clear every predecessor. The final
|
||||
// ordinary header is now in the canonical last include, at d[8..9).
|
||||
let profile = Limits {
|
||||
entries: baseline.entries + 37,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source = resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
let e = match source.plan_rename(&desired) {
|
||||
Ok(_) => panic!("cap accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics[0].path, "z.arcadia");
|
||||
assert_eq!(e.diagnostics[0].diagnostic.span, Span { start: 8, end: 9 });
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Private complete-target mixed rename planning
|
||||
|
||||
2026-09-24. Descendant implementation of base `ee500908e3d77182ddd54e12524f9b6c6eb43b60`, under accepted parent contract `dc87babf` (`harness/analysis-mixed-rename-plan-contract-20260924.md` on the evidence branch). This note describes source and authored tests, not completed runtime acceptance.
|
||||
|
||||
`AuthoredMixedProject::plan_rename` consumes the complete admitted source witness and borrows an immutable whole desired Project. `RenamePlan` retains that witness, desired borrow, changed qualified names and one original-coordinate physical edit map. Read-only methods expose source, desired, names, edits, full-current-bundle validation and combined complete output-byte sizing. There is no emitter, rewritten-output admission, public activation or legacy adapter.
|
||||
|
||||
The whole-target gate uses exhaustive record patterns, collection keys/counts and parameter position/identity/type checks. Existing PartialEq semantics govern all unchanged payloads, including signed floating zero. Its expression work vectors run under an E-entry reservation, where E is the retained raw expression-site count. Current expression roots are calculation bindings, engineering-property base, case overrides and check predicates; historical semantic fields have no analysis Expr. E scratch is released before changed names are collected; the final namespace set is dropped before edits are collected. Successful retained growth is R+M entries, zero occurrence slots and zero copied text. Parsing, numeric and frame Usage remain unchanged.
|
||||
|
||||
Both authenticated occurrence families feed one map. Validation checks original token spelling/UTF-8 authority, whole Word spans, physical nonoverlap and exact required key/qualified-target membership. Bundle validation includes entry, every path/file and untouched bytes. Sizing subtracts the sum of removed bytes before adding replacements per file, then includes all files in the inherited byte check. Emission/re-admission lifecycle and aggregate output storage remain required successors.
|
||||
|
||||
Independent source review found and prompted two pre-compiler repairs: preserve canonical file attribution for edit-reservation failures, and check ordered parameter shape before calculation-body equality. No runtime failure claim is attached to these source findings. Existing historical mixed/ordinary failures and prior baseline results remain unchanged; this work does not overwrite their records.
|
||||
|
||||
The 17 authored tests cover independently specified complete rich OA/SA and Unicode/CRLF maps and final-name sets; all36 category edits including real comment-separated references and historical UUID exclusion; identity/map/parameter/non-name field rejections; QuantityKind/Unit payload gate controls; simultaneous global/scoped swaps and collisions; immutable exact target/source borrowing; no-op; unchanged Usage and R+M retention; stale touched/untouched/path/entry/file-set controls; safe corrupted UTF-8/file authority and same-spelling wrong-callee identity; signed-zero geometry, scalar bounds/defaults and historical real facets; deep/wide old equality and target-only trees with small-stack success/drop and late refusal.
|
||||
|
||||
Storage oracle: rich OA B=resolved+65, E15/Q11/R11/M27; added peak38 and retention38. Inherited B+37/B+38 tests run the entire predecessor pipeline before planning, and the negative checks the last ordinary diagram header main[412..413). A separate diagram moved to canonical last include z.arcadia checks precise included-file allocation diagnostics, contingent on its complete predecessor pipeline fitting the unchanged profile. No test replaces/tightens a live owner's Budget to manufacture a later-phase failure. Wide/deep equality tests assert authored E128/E129; no unsupported isolated E-cap claim is made.
|
||||
|
||||
Combined-size oracle uses original930 bytes: ordinary31-byte node alias shrinks to1 while three typed f aliases grow to9; complete924 fits although typed-only intermediate954 would not. A12-byte replacement produces933 and must fail under the same inherited930-byte cap, including the untouched213-byte file. This tests planned sizing only, never output correctness.
|
||||
|
||||
All source/tests require parent compilation, focused runtime tests and existing broader gates. Direct rustfmt is the only executed Rust tool during authoring. Parent owns commit/publication and verification evidence; no Cargo/build/test execution was performed by this implementation agent.
|
||||
Reference in New Issue
Block a user