Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18073a6f2e | ||
|
|
f1c095654a | ||
|
|
70c20c39de | ||
|
|
8159a17dd7 | ||
|
|
9e8e77e1d8 | ||
|
|
9388fcf2f0 |
@@ -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;
|
||||
+982
@@ -0,0 +1,982 @@
|
||||
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() {
|
||||
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)
|
||||
));
|
||||
for mode in 0..3 {
|
||||
// Admission/intent construction are prerequisites, outside the
|
||||
// bounded planner worker. No limits or live owner state are reset.
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
|
||||
let mut desired = source.project().clone();
|
||||
match mode {
|
||||
0 => {}
|
||||
1 => desired.definitions.get_mut(&id(2)).unwrap().alias = "p".into(),
|
||||
_ => calculation(&mut desired, 2).body = a::Binding::Unbound,
|
||||
}
|
||||
std::thread::scope(|scope| {
|
||||
let desired = &desired;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
// The complete owner moves into this worker; successful
|
||||
// plans and consumed failure owners die here. Nothing
|
||||
// carrying source/edit authority escapes the worker.
|
||||
match source.plan_rename(desired) {
|
||||
Ok(plan) => {
|
||||
assert_eq!(mode, 0, "mutation unexpectedly accepted");
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(std::ptr::eq(plan.desired(), desired));
|
||||
drop(plan);
|
||||
}
|
||||
Err(failure) => {
|
||||
assert_ne!(mode, 0, "unchanged target unexpectedly refused");
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
if mode == 1 {
|
||||
"Rename aliases collide in their final namespace."
|
||||
} else {
|
||||
"Rename target changes non-name project semantics."
|
||||
}
|
||||
);
|
||||
drop(failure);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
drop(desired);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[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,33 @@
|
||||
# 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.
|
||||
|
||||
## Recorded first runtime failure and narrow test repair
|
||||
|
||||
Parent's `9388fcf2` focused run compiled and passed four tests before the 128 KiB deep/wide worker terminated with Windows stack-overflow exit 0xc00000fd. Parent separately ran the remaining 16 tests from that exact original artifact: all passed, explicitly excluding the crashing test. This is not a 17-test acceptance result.
|
||||
|
||||
Diagnostic source `9e8e77e1` established that deep admission, whole original clone, rename success and successful plan drop completed; failure followed entry into the collision-rejection helper. Diagnostic source `8159a17d` further located the overflow inside that helper's `authored(bundle)` call: its flushed before-admission marker appeared, but its after-admission/before-plan marker did not. Thus that failing helper invocation had not entered rename planning. These observations do not prove a particular compiler frame size or establish model cloning as the cause.
|
||||
|
||||
The bounded repair removes diagnostic markers and restores the general helper. Only this small-stack test directly creates each fresh owner at worker scope and matches each planning result there. It still uses 128 KiB, deep 128 and wide 129 expression nodes, complete admission on the worker, success/plan-drop, late final-namespace collision/refusal-drop and non-name semantic refusal/drop for both shapes. Prior plan, diagnostic and desired owners are explicitly dropped before subsequent cases. No fixture depth/width or stack allowance is weakened; no production code changes. The repaired full 17-test focused run and broader gates remain parent-owned and pending.
|
||||
|
||||
The direct-call candidate at `70c20c3` also aborted the small-stack test after the same first four focused tests passed. That candidate is **not** an accepted repair. The next test-only candidate uses a compact mode loop: each shape runs no-op success, final-namespace collision and non-name refusal through one shared source local, one desired clone and one planning result expression, with explicit plan/failure/desired drops. Complete admission remains inside the 128 KiB worker for every case; depths and expected outcomes are unchanged. This removes repeated case-specific large debug temporaries as a bounded layout experiment; no exact compiler frame size or overflow cause is claimed. If it still fails, actual-frame diagnostics are required before further rearrangement. Parent-focused verification remains pending.
|
||||
|
||||
Parent's `f1c0956` debugger evidence supersedes the unlocated layout hypothesis: run3 backtrace reaches `mixed::parse_with_profile -> Parser::unit -> parse_typed -> declarations::parse -> syntax::binding -> Cursor::exact_literal -> Budget::exact -> Exact::parse_decimal -> BigInt::parse_bytes -> Windows heap allocation`, before any rename-plan call. The same artifact's debugger run2 passed, so the full admission-on-128-KiB observation remains an **UNRESOLVED environment-sensitive stack boundary**, not a demonstrated planner defect or a repaired production issue. Raw evidence is the parent-owned `rename-debugger-f1c0956-3.log` and receipt; all preceding failed full-pipeline observations remain valid and preserved.
|
||||
|
||||
The final narrowly scoped planner evaluator prepares each fresh complete admitted owner and immutable desired Project on the normal test thread. For all six deep128/wide129 × no-op/collision/non-name cases, a scoped128-KiB worker moves the complete owner, borrows desired, invokes planning, checks the exact expected outcome, and drops its successful plan or failure there. No owner/result escapes, policy resets or leaks occur. Desired is externally owned and destroyed outside the worker. Depth, width, worker stack allowance and all planning/drop cases are unchanged. This test now claims only planner comparison/ownership/drop behavior; it makes **no claim** that the complete upstream parse/admission pipeline reliably fits128KiB under every observed environment. Existing separate admission/index small-stack tests remain in the parent full-suite gates. Final focused/full runtime acceptance is still parent-owned and pending.
|
||||
Reference in New Issue
Block a user