Compare commits

...
9 changed files with 2497 additions and 4 deletions
@@ -3,9 +3,11 @@
mod budget;
mod cursor;
mod declarations;
mod index;
mod lex;
mod lower;
mod syntax;
mod update;
mod write;
#[cfg(test)]
@@ -143,6 +143,14 @@ impl Budget {
}
Ok(())
}
/// Aggregate owned text for an index/update phase, checked before copying.
pub fn index_bytes(&self, count: usize, span: Span) -> Result<(), Error> {
self.active()?;
if count > self.limits.bytes {
return Err(error(span, "Source8 index/output byte limit exceeded."));
}
Ok(())
}
pub fn lowering_occurrences(&self, count: usize, span: Span) -> Result<(), Error> {
self.active()?;
// Parameter declarations are syntax entries, not reference uses.
@@ -0,0 +1,709 @@
//! Temporary typed source witnesses. No arena index becomes a model identity.
//! The staged entry point parses each supplied typed file once. A future bundle
//! parser must retain the same opaque parse/lower provenance. Occurrence
//! completeness trusts that lowerer; the private builder does not prove the
//! presence of every syntax reference in a caller-fabricated Lowered value.
use super::cursor::Cursor;
use super::lower::{self, Child, Field, Lowered, Role, Target, Unit};
use super::{Budget, Error, Span, declarations, error, lex};
use crate::{Id, Project, analysis as a};
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::Arc;
/// Persistent structural path: cloning shares ancestors, and equality/order
/// compare the complete edge sequence, never allocation or arena identities.
/// Depth is checked against the inherited frame limit before allocation.
/// Comparison and Debug walk leaf-to-root without allocating or recursing;
/// this private total order need not be root-first lexicographic order.
#[derive(Clone, Default)]
pub(super) struct Path(Option<Arc<Step>>);
struct Step {
parent: Path,
edge: Child,
}
impl PartialEq for Path {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Path {}
impl PartialOrd for Path {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Path {
fn cmp(&self, other: &Self) -> Ordering {
let (mut left, mut right) = (self, other);
loop {
match (left.0.as_deref(), right.0.as_deref()) {
(Some(a), Some(b)) => {
let order = a.edge.cmp(&b.edge);
if order != Ordering::Equal {
return order;
}
(left, right) = (&a.parent, &b.parent);
}
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
}
}
}
}
impl fmt::Debug for Path {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
// Debug intentionally shows the same leaf-to-root order as comparison.
let mut list = formatter.debug_list();
let mut path = self;
while let Some(step) = &path.0 {
list.entry(&step.edge);
path = &step.parent;
}
list.finish()
}
}
impl Drop for Path {
fn drop(&mut self) {
let mut next = self.0.take();
while let Some(link) = next {
// Shared ancestors remain owned elsewhere. The final owner peels
// unique links here, taking the parent before Step is dropped.
let Some(mut step) = Arc::into_inner(link) else {
break;
};
next = step.parent.0.take();
}
}
}
impl Path {
pub fn edges(&self) -> Vec<Child> {
let mut result = Vec::new();
let mut path = self;
while let Some(step) = &path.0 {
result.push(step.edge);
path = &step.parent;
}
result.reverse();
result
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct ExpressionKey {
pub owner: Id,
pub field: Field,
pub path: Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum TokenRole {
Declaration,
Use(Role),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct OccurrenceKey {
pub owner: Id,
/// None is a global declaration header, not a binding field.
pub field: Option<Field>,
/// None is outside an expression; Some(empty) is its root call site.
pub path: Option<Path>,
pub role: TokenRole,
pub target: Target,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Location {
pub file: usize,
pub span: Span,
}
#[derive(Debug)]
pub(super) struct Index<'s> {
// Private provenance: callers can inspect locations, not supply replacements
// for the source witness, keys, catalog or occurrence spans.
sources: &'s BTreeMap<usize, String>,
names: BTreeMap<Target, String>,
occurrences: BTreeMap<OccurrenceKey, Location>,
expressions: BTreeMap<ExpressionKey, Location>,
}
impl<'s> Index<'s> {
pub fn sources(&self) -> &BTreeMap<usize, String> {
self.sources
}
pub fn names(&self) -> &BTreeMap<Target, String> {
&self.names
}
pub fn occurrences(&self) -> &BTreeMap<OccurrenceKey, Location> {
&self.occurrences
}
pub fn expressions(&self) -> &BTreeMap<ExpressionKey, Location> {
&self.expressions
}
/// Typed-only staged adapter. `ordinary` has already been parsed by its
/// owner. Missing typed IDs fail in lowering; this never invents UUIDs.
/// The caller merges the returned additions and admits the complete model.
pub fn parse(
ordinary: &Project,
sources: &'s BTreeMap<usize, String>,
budget: &mut Budget,
) -> Result<(Self, Lowered), lower::Failure> {
let mut units = Vec::new();
let mut witnesses = BTreeMap::new();
for (&file, source) in sources {
let failure = |error| lower::Failure {
file: Some(file),
error,
};
budget
.lowering_entries(units.len() + 1, Span::default())
.map_err(failure)?;
let tokens = lex::tokens(source, budget).map_err(failure)?;
let mut cursor = Cursor::new(source, &tokens, budget);
let mut parsed = Vec::new();
while cursor.cursor < tokens.len() {
parsed.push(declarations::parse(&mut cursor, 8, None).map_err(failure)?);
}
units.push(Unit {
file,
declarations: parsed,
});
witnesses.insert(file, tokens);
}
let lowered = lower::lower(ordinary, &units, budget)?;
// A build failure after our own parse/lower is a phase/invariant error,
// not an invented file assignment for an ambiguous numeric span.
let index = Self::build(ordinary, sources, &units, &witnesses, &lowered, budget)
.map_err(|error| lower::Failure { file: None, error })?;
Ok((index, lowered))
}
// Same-parse/lower internal seam only. Checks below reject damaged supplied
// sites but occurrence completeness is an invariant of our own lowerer.
// Do not expose a constructor accepting independently fabricated Lowered.
fn build(
ordinary: &Project,
sources: &'s BTreeMap<usize, String>,
units: &[Unit],
witnesses: &BTreeMap<usize, Vec<lex::Token>>,
lowered: &Lowered,
budget: &mut Budget,
) -> Result<Self, Error> {
budget.active()?;
budget.lowering_entries(lowered.expressions.len(), Span::default())?;
budget.lowering_occurrences(lowered.occurrences.len(), Span::default())?;
let mut names = BTreeMap::new();
let mut name_bytes = 0usize;
let mut insert_name = |target, name: &str| -> Result<(), Error> {
budget.lowering_entries(names.len() + 1, Span::default())?;
name_bytes = name_bytes
.checked_add(name.len())
.ok_or_else(|| error(Span::default(), "Index name size overflow."))?;
budget.index_bytes(name_bytes, Span::default())?;
if names.insert(target, name.to_owned()).is_some() {
return Err(error(
Span::default(),
"Duplicate qualified catalog identity.",
));
}
Ok(())
};
// Lowering has already bounded and validated the complete ordinary
// identity catalog before this existing convenience visitor allocates.
for (id, alias) in ordinary.identities() {
insert_name(Target::Global(id), alias)?;
}
for definition in ordinary
.definitions
.values()
.chain(lowered.definitions.values())
{
if lowered.definitions.contains_key(&definition.id) {
insert_name(Target::Global(definition.id), &definition.alias)?;
}
if let a::DefinitionBody::Calculation(calculation) = &definition.body {
for parameter in &calculation.parameters {
insert_name(
Target::Parameter {
calculation: definition.id,
parameter: parameter.id,
},
&parameter.name,
)?;
}
}
}
for element in lowered.elements.values() {
insert_name(Target::Global(element.id), &element.alias)?;
}
let mut index = Self {
sources,
names,
occurrences: BTreeMap::new(),
expressions: BTreeMap::new(),
};
let mut owners = BTreeMap::new();
let mut physical = BTreeSet::new();
for unit in units {
for declaration in &unit.declarations {
let owner = declaration.header.identity.value.ok_or_else(|| {
error(
declaration.span,
"Missing ID requires source stabilization.",
)
})?;
let location = Location {
file: unit.file,
span: declaration.span,
};
checked_source(sources, location)?;
budget.lowering_entries(owners.len() + 1, declaration.span)?;
if owners.insert(owner, location).is_some() {
return Err(error(declaration.span, "Duplicate source owner."));
}
let key = OccurrenceKey {
owner,
field: None,
path: None,
role: TokenRole::Declaration,
target: Target::Global(owner),
};
index.insert(
key,
Location {
file: unit.file,
span: declaration.header.alias.span,
},
witnesses,
&mut physical,
budget,
)?;
}
}
// Parent-before-child is part of the lowering contract. One forward
// pass rejects forward, cyclic, cross-file/owner/field and duplicate
// paths. Each node allocates one shared edge, not a copied path vector.
let mut paths: Vec<(ExpressionKey, usize)> = Vec::new();
for (position, site) in lowered.expressions.iter().enumerate() {
if !matches!(
site.field,
Field::Body | Field::Base | Field::Predicate | Field::Override(_)
) {
return Err(error(
site.span,
"Expression occurs outside a binding field.",
));
}
let location = Location {
file: site.file,
span: site.span,
};
checked_source(sources, location)?;
let owner = owners
.get(&site.owner)
.ok_or_else(|| error(site.span, "Unknown expression owner."))?;
contained(*owner, location)?;
let (path, depth) = if let Some((parent, edge)) = site.parent {
if parent >= position {
return Err(error(
site.span,
"Expression parent must precede its child.",
));
}
let ancestor = &lowered.expressions[parent];
if ancestor.owner != site.owner
|| ancestor.field != site.field
|| ancestor.file != site.file
{
return Err(error(
site.span,
"Expression parent crosses its owner, field or file.",
));
}
contained(
Location {
file: ancestor.file,
span: ancestor.span,
},
location,
)?;
if ancestor.span == site.span {
return Err(error(
site.span,
"Child expression must be a proper source subtree.",
));
}
let depth = paths[parent].1 + 1;
budget.frame(depth, site.span)?;
(
Path(Some(Arc::new(Step {
parent: paths[parent].0.path.clone(),
edge,
}))),
depth,
)
} else {
budget.frame(1, site.span)?;
(Path::default(), 1)
};
let key = ExpressionKey {
owner: site.owner,
field: site.field,
path,
};
if index.expressions.insert(key.clone(), location).is_some() {
return Err(error(site.span, "Duplicate qualified expression path."));
}
paths.push((key, depth));
}
for occurrence in &lowered.occurrences {
let location = Location {
file: occurrence.file,
span: occurrence.span,
};
let owner = owners
.get(&occurrence.owner)
.ok_or_else(|| error(occurrence.span, "Unknown occurrence owner."))?;
contained(*owner, location)?;
let path = match occurrence.expression {
Some(site) => {
let expression = lowered
.expressions
.get(site)
.ok_or_else(|| error(occurrence.span, "Unknown occurrence expression."))?;
if expression.owner != occurrence.owner
|| expression.field != occurrence.field
|| expression.file != occurrence.file
{
return Err(error(
occurrence.span,
"Occurrence crosses its expression owner, field or file.",
));
}
contained(
Location {
file: expression.file,
span: expression.span,
},
location,
)?;
Some(paths[site].0.path.clone())
}
None => None,
};
match (occurrence.role, occurrence.target) {
(
Role::ParameterDeclaration,
Target::Parameter {
calculation,
parameter,
},
) if calculation == occurrence.owner
&& occurrence.field == Field::Input(parameter)
&& path.is_none() => {}
(Role::ParameterUse, Target::Parameter { calculation, .. })
if calculation == occurrence.owner
&& occurrence.field == Field::Body
&& path.is_some() => {}
(Role::ArgumentLabel, Target::Parameter { .. }) if path.is_some() => {}
(Role::OverrideTarget, Target::Global(id))
if occurrence.field == Field::Override(id) && path.is_none() => {}
(Role::CallTarget, Target::Global(_)) if path.is_some() => {}
(Role::Reference, Target::Global(_)) => {}
_ => return Err(error(occurrence.span, "Invalid qualified occurrence role.")),
}
let key = OccurrenceKey {
owner: occurrence.owner,
field: Some(occurrence.field),
path,
role: TokenRole::Use(occurrence.role),
target: occurrence.target,
};
index.insert(key, location, witnesses, &mut physical, budget)?;
}
// Argument labels belong to this precise call site and its callee, not
// merely to a parameter UUID/name present somewhere in the catalog.
let mut calls = BTreeMap::new();
for (key, at) in &index.occurrences {
if key.role == TokenRole::Use(Role::CallTarget) {
let site = (key.owner, key.field, key.path.clone());
if calls.insert(site, key.target).is_some() {
return Err(error(at.span, "Multiple callees at one expression site."));
}
}
}
for (key, at) in &index.occurrences {
if let (TokenRole::Use(Role::ArgumentLabel), Target::Parameter { calculation, .. }) =
(key.role, key.target)
&& calls.get(&(key.owner, key.field, key.path.clone()))
!= Some(&Target::Global(calculation))
{
return Err(error(
at.span,
"Argument label crosses its called calculation.",
));
}
}
for (position, site) in lowered.expressions.iter().enumerate() {
if let Some((parent, Child::Argument(parameter))) = site.parent {
let key = &paths[parent].0;
let Some(Target::Global(calculation)) =
calls.get(&(key.owner, Some(key.field), Some(key.path.clone())))
else {
return Err(error(site.span, "Argument child has no owning call."));
};
let label = OccurrenceKey {
owner: site.owner,
field: Some(site.field),
path: Some(key.path.clone()),
role: TokenRole::Use(Role::ArgumentLabel),
target: Target::Parameter {
calculation: *calculation,
parameter,
},
};
if !index.occurrences.contains_key(&label) || paths[position].0.path == key.path {
return Err(error(site.span, "Argument child has no qualified label."));
}
}
}
index.check_syntax_edges(units, lowered, budget)?;
Ok(index)
}
/// Check structural edges against the already parsed syntax, not just the
/// arena's internally consistent claims. This is one iterative tree walk,
/// with indexed span lookup; it does not lex or copy source/path strings.
fn check_syntax_edges<'a>(
&self,
units: &'a [Unit],
lowered: &Lowered,
budget: &Budget,
) -> Result<(), Error> {
let mut globals = BTreeMap::new();
let mut parameters = BTreeMap::new();
for (target, name) in &self.names {
match target {
Target::Global(id) => {
globals.insert(name.as_str(), *id);
}
Target::Parameter {
calculation,
parameter,
} => {
parameters.insert((*calculation, name.as_str()), *parameter);
}
}
}
let mut sites = BTreeMap::new();
for (position, site) in lowered.expressions.iter().enumerate() {
if sites
.insert(
(
site.file,
site.owner,
site.field,
site.span.start,
site.span.end,
),
position,
)
.is_some()
{
return Err(error(site.span, "Duplicate expression source ownership."));
}
}
let mut work = Vec::new();
let mut root = |file, owner, field, binding: &'a super::Binding| -> Result<(), Error> {
if let super::Binding::Expression(expression) = binding {
budget.lowering_entries(work.len() + 1, expression.span)?;
work.push((file, owner, field, expression, None));
}
Ok(())
};
for unit in units {
for declaration in &unit.declarations {
let owner = declaration.header.identity.value.expect("validated owner");
match &declaration.body {
declarations::Body::Calculation { body, .. } => {
root(unit.file, owner, Field::Body, &body.value)?
}
declarations::Body::Property { base, .. } => {
root(unit.file, owner, Field::Base, &base.value)?
}
declarations::Body::Check { predicate, .. } => {
root(unit.file, owner, Field::Predicate, &predicate.value)?
}
declarations::Body::Case { overrides, .. } => {
for entry in &overrides.value {
let target =
*globals.get(entry.target.name.as_str()).ok_or_else(|| {
error(entry.target.span, "Unknown syntax override target.")
})?;
root(unit.file, owner, Field::Override(target), &entry.binding)?;
}
}
_ => {}
}
}
}
let mut visited = 0usize;
while let Some((file, owner, field, expression, parent)) = work.pop() {
budget.lowering_entries(visited + 1, expression.span)?;
visited += 1;
let site = *sites
.get(&(
file,
owner,
field,
expression.span.start,
expression.span.end,
))
.ok_or_else(|| {
error(
expression.span,
"Parsed expression is absent from the source arena.",
)
})?;
if lowered.expressions[site].parent != parent {
return Err(error(
expression.span,
"Arena parent/child edge differs from original syntax.",
));
}
let mut child = |value: &'a super::Expression, edge| -> Result<(), Error> {
budget.lowering_entries(work.len() + 1, value.span)?;
work.push((file, owner, field, value, Some((site, edge))));
Ok(())
};
use super::ExpressionKind as E;
match &expression.kind {
E::List(items) => {
for (index, item) in items.iter().enumerate() {
child(item, Child::Item(index))?;
}
}
E::Unary { operand, .. } => child(operand, Child::Operand)?,
E::Binary { left, right, .. } => {
child(left, Child::Left)?;
child(right, Child::Right)?;
}
E::If {
condition,
when_true,
when_false,
} => {
child(condition, Child::Condition)?;
child(when_true, Child::WhenTrue)?;
child(when_false, Child::WhenFalse)?;
}
E::Call {
definition,
arguments,
} => {
let calculation = *globals
.get(definition.name.as_str())
.ok_or_else(|| error(definition.span, "Unknown syntax call target."))?;
for argument in arguments {
let parameter = *parameters
.get(&(calculation, argument.parameter.name.as_str()))
.ok_or_else(|| {
error(argument.parameter.span, "Unknown syntax call parameter.")
})?;
child(&argument.value, Child::Argument(parameter))?;
}
}
E::Sum { values, initial } => {
child(values, Child::Values)?;
child(initial, Child::Initial)?;
}
E::Power { base, .. } => child(base, Child::Base)?,
E::AsKind { value, .. } => child(value, Child::Value)?,
_ => {}
}
}
if visited != lowered.expressions.len() {
return Err(error(
Span::default(),
"Arena contains expressions absent from parsed syntax.",
));
}
Ok(())
}
fn insert(
&mut self,
key: OccurrenceKey,
location: Location,
witnesses: &BTreeMap<usize, Vec<lex::Token>>,
physical: &mut BTreeSet<(usize, usize, usize)>,
budget: &Budget,
) -> Result<(), Error> {
budget.lowering_occurrences(self.occurrences.len() + 1, location.span)?;
let text = checked_source(self.sources, location)?;
if self.names.get(&key.target).is_none_or(|name| name != text) {
return Err(error(
location.span,
"Token differs from its qualified target name.",
));
}
let tokens = witnesses
.get(&location.file)
.ok_or_else(|| error(location.span, "Missing lexical witness."))?;
let token = tokens
.binary_search_by_key(&location.span.start, |token| token.span.start)
.ok()
.map(|at| tokens[at]);
if token.is_none_or(|token| token.span != location.span || token.class != lex::Class::Word)
{
return Err(error(
location.span,
"Occurrence is not an original whole name token.",
));
}
if self.occurrences.contains_key(&key)
|| !physical.insert((location.file, location.span.start, location.span.end))
{
return Err(error(
location.span,
"Duplicate qualified occurrence or token ownership.",
));
}
self.occurrences.insert(key, location);
Ok(())
}
}
pub(super) fn checked_source(
sources: &BTreeMap<usize, String>,
location: Location,
) -> Result<&str, Error> {
if location.span.start >= location.span.end {
return Err(error(location.span, "Empty or reversed source span."));
}
sources
.get(&location.file)
.and_then(|source| source.get(location.span.start..location.span.end))
.ok_or_else(|| {
error(
location.span,
"Source span is outside its original UTF-8 file.",
)
})
}
fn contained(parent: Location, child: Location) -> Result<(), Error> {
if parent.file != child.file
|| child.span.start < parent.span.start
|| child.span.end > parent.span.end
{
return Err(error(
child.span,
"Source span crosses its containing syntax.",
));
}
Ok(())
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,630 @@
use super::*;
use crate::language::ParseLimits;
const OA: &str = include_str!("../lower/fixtures/typed-oa.arcadia");
const ORDINARY: &str = include_str!("../lower/fixtures/ordinary-oa.arcadia");
fn budget() -> Budget {
Budget::new(super::super::budget::Limits::source8(
ParseLimits::UNLIMITED,
))
.unwrap()
}
fn ordinary() -> Project {
crate::language::parse(ORDINARY).unwrap()
}
fn sources() -> BTreeMap<usize, String> {
let split = OA.find("calculation combine").unwrap();
BTreeMap::from([
(7, OA[..split].replace('\n', "\r\n")),
(19, OA[split..].to_owned()),
])
}
fn raw(
sources: &BTreeMap<usize, String>,
budget: &mut Budget,
) -> (Vec<Unit>, BTreeMap<usize, Vec<lex::Token>>, Lowered) {
let mut units = Vec::new();
let mut witnesses = BTreeMap::new();
for (&file, source) in sources {
let tokens = lex::tokens(source, budget).unwrap();
let mut cursor = Cursor::new(source, &tokens, budget);
let mut declarations = Vec::new();
while cursor.cursor < tokens.len() {
declarations.push(declarations::parse(&mut cursor, 8, None).unwrap());
}
units.push(Unit { file, declarations });
witnesses.insert(file, tokens);
}
let lowered = lower::lower(&ordinary(), &units, budget).unwrap();
(units, witnesses, lowered)
}
#[test]
fn staged_index_preserves_complete_oa_sa_models_and_original_file_spans() {
for (layer, ordinary_source, typed) in [
(crate::Layer::Oa, ORDINARY, OA),
(
crate::Layer::Sa,
include_str!("../lower/fixtures/ordinary-sa.arcadia"),
include_str!("../lower/fixtures/typed-sa.arcadia"),
),
] {
let sources = BTreeMap::from([(37, typed.replace('\n', "\r\n"))]);
let skeleton = crate::language::parse(ordinary_source).unwrap();
let mut work = budget();
let (index, lowered) = Index::parse(&skeleton, &sources, &mut work).unwrap();
let mut actual = skeleton;
actual.definitions.extend(lowered.definitions);
actual.elements.extend(lowered.elements);
assert_eq!(actual, super::super::model_oracle::project(layer));
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
assert_eq!(work.usage.bytes, sources[&37].len(), "one lexical pass");
for (key, at) in index.occurrences() {
assert_eq!(at.file, 37);
assert_eq!(
&sources[&37][at.span.start..at.span.end],
index.names[&key.target]
);
}
assert!(!index.expressions().is_empty());
}
}
#[test]
fn repeated_reordered_calls_have_stable_full_paths_and_four_labels() {
let expression = "@combine(b: @dispatch_time, a: @receive_time) // first 雪\n + @combine(a: @receive_time, b: @dispatch_time)";
let original = OA.replace("@combine(a: @receive_time, b: @dispatch_time)", expression);
let reordered = original.replace(
"@combine(b: @dispatch_time, a: @receive_time)",
"@combine(a: @receive_time, b: @dispatch_time)",
);
let first = BTreeMap::from([(7, original)]);
let second = BTreeMap::from([(7, reordered)]);
let (left, _) = Index::parse(&ordinary(), &first, &mut budget()).unwrap();
let (right, _) = Index::parse(&ordinary(), &second, &mut budget()).unwrap();
assert_eq!(
left.occurrences.keys().collect::<Vec<_>>(),
right.occurrences.keys().collect::<Vec<_>>()
);
assert_eq!(
left.expressions.keys().collect::<Vec<_>>(),
right.expressions.keys().collect::<Vec<_>>()
);
let labels: Vec<_> = left
.occurrences
.iter()
.filter(|(key, _)| key.role == TokenRole::Use(Role::ArgumentLabel))
.collect();
assert_eq!(labels.len(), 4);
let paths: BTreeSet<_> = labels
.iter()
.map(|(key, _)| key.path.as_ref().unwrap().edges())
.collect();
assert_eq!(
paths,
BTreeSet::from([vec![Child::Left], vec![Child::Right]])
);
let spans: BTreeSet<_> = labels
.iter()
.map(|(_, at)| (at.span.start, at.span.end))
.collect();
assert_eq!(spans.len(), 4);
}
#[test]
fn index_checks_every_expression_edge_and_keeps_override_fields_distinct() {
let source = OA
.replace("body $a + $b;", "body if(if(true, 1 < 2, false), sum([$a, abs(-$b)], 0 [@second]), as_kind(@duration, power(1 [@second], 2) / 1 [@second]));")
.replace("@receive_time = 1250 [@millisecond];", "@receive_time = @combine(a: 1 [@second], b: 2 [@second]); @dispatch_time = @combine(b: 2 [@second], a: 1 [@second]);");
let sources = BTreeMap::from([(17, source)]);
let (index, lowered) = Index::parse(&ordinary(), &sources, &mut budget()).unwrap();
let mut project = ordinary();
project.definitions.extend(lowered.definitions);
project.elements.extend(lowered.elements);
a::admit(&project, a::AdmissionLimits::REFERENCE).unwrap();
let edges: BTreeSet<_> = index
.expressions
.keys()
.flat_map(|key| key.path.edges())
.collect();
for edge in [
Child::Item(0),
Child::Item(1),
Child::Operand,
Child::Left,
Child::Right,
Child::Condition,
Child::WhenTrue,
Child::WhenFalse,
Child::Values,
Child::Initial,
Child::Base,
Child::Value,
] {
assert!(edges.contains(&edge), "missing {edge:?}");
}
assert!(edges.iter().any(|edge| matches!(edge, Child::Argument(_))));
let labels: Vec<_> = index
.occurrences
.keys()
.filter(|key| {
key.role == TokenRole::Use(Role::ArgumentLabel)
&& matches!(key.field, Some(Field::Override(_)))
})
.collect();
assert_eq!(labels.len(), 4);
assert_eq!(
labels
.iter()
.map(|key| key.field)
.collect::<BTreeSet<_>>()
.len(),
2
);
assert!(
labels
.iter()
.all(|key| key.path.as_ref().unwrap().edges().is_empty())
);
}
#[test]
fn arena_rejects_forward_cycles_and_cross_owner_field_file_parents() {
for damage in 0..6 {
let sources = sources();
let mut work = budget();
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
let child = lowered
.expressions
.iter()
.position(|site| site.parent.is_some())
.unwrap();
let parent = lowered.expressions[child].parent.unwrap().0;
match damage {
0 => lowered.expressions[child].parent = Some((child, Child::Left)),
1 => lowered.expressions[parent].parent = Some((child, Child::Right)),
2 => lowered.expressions[child].owner = ordinary().id,
3 => lowered.expressions[child].field = Field::Predicate,
4 => lowered.expressions[child].file = 12345,
_ => lowered.expressions[child].parent = Some((parent, Child::Operand)),
}
assert!(
Index::build(
&ordinary(),
&sources,
&units,
&witnesses,
&lowered,
&mut work
)
.is_err(),
"damage {damage}"
);
}
}
#[test]
fn arena_rejects_duplicate_sites_occurrences_and_wrong_callee_labels() {
for damage in 0..4 {
let mut sources = sources();
// The other callee is fully valid and reuses both parameter identities
// and spellings. Catalog/token validation must not explain rejection.
sources.insert(
31,
r#"calculation other "Other" id "00000000-0000-0000-0000-00000000cafe" {
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Quantity(@duration);
input b id "7a47cc1e-8ae5-4551-b2d8-8a68108e0976" : Quantity(@duration);
result Quantity(@duration); body $a + $b;
}"#
.into(),
);
let mut work = budget();
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
let valid = Index::build(
&ordinary(),
&sources,
&units,
&witnesses,
&lowered,
&mut work,
)
.unwrap();
let mut expected_mismatch_span = None;
match damage {
0 => {
let site = &lowered.expressions[0];
lowered.expressions.push(lower::ExpressionSite {
file: site.file,
owner: site.owner,
field: site.field,
parent: site.parent,
span: site.span,
});
}
1 => {
let row = &lowered.occurrences[0];
lowered.occurrences.push(lower::Occurrence {
file: row.file,
owner: row.owner,
field: row.field,
expression: row.expression,
role: row.role,
target: row.target,
span: row.span,
});
}
2 => {
let label = lowered
.occurrences
.iter_mut()
.find(|row| row.role == Role::ArgumentLabel)
.unwrap();
let original = label.target;
let Target::Parameter {
calculation,
parameter,
} = &mut label.target
else {
panic!("argument label must target a parameter");
};
let other = Target::Parameter {
calculation: Id::from_u128(0xcafe),
parameter: *parameter,
};
assert_eq!(valid.names[&original], valid.names[&other]);
*calculation = Id::from_u128(0xcafe);
expected_mismatch_span = Some(label.span);
}
_ => {
let row = lowered
.occurrences
.iter_mut()
.find(|row| row.role == Role::OverrideTarget)
.unwrap();
row.field = Field::Override(ordinary().id);
}
}
let failure = Index::build(
&ordinary(),
&sources,
&units,
&witnesses,
&lowered,
&mut work,
)
.unwrap_err();
if let Some(span) = expected_mismatch_span {
assert_eq!(
failure.message,
"Argument label crosses its called calculation."
);
assert_eq!(failure.span, span);
}
}
}
#[test]
fn index_rejects_comment_partial_and_non_utf8_occurrence_spans() {
for damage in 0..3 {
let mut sources = sources();
let with_comment =
sources[&7].replace("subject @receive;", "subject @receive; // duration 雪\r\n");
sources.insert(7, with_comment);
let mut work = budget();
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
let occurrence = lowered
.occurrences
.iter_mut()
.find(|row| row.file == 7 && &sources[&7][row.span.start..row.span.end] == "duration")
.unwrap();
match damage {
0 => {
let start = sources[&7].find("// duration").unwrap() + 3;
occurrence.span = Span {
start,
end: start + 8,
};
}
1 => occurrence.span.end -= 1,
_ => {
let start = sources[&7].find('雪').unwrap() + 1;
occurrence.span = Span {
start,
end: start + 1,
};
}
}
assert!(
Index::build(
&ordinary(),
&sources,
&units,
&witnesses,
&lowered,
&mut work
)
.is_err()
);
}
}
#[test]
fn index_has_shared_budget_and_never_stabilizes_missing_ids() {
let source = "calculation f \"F\" { result Number; body unbound; }";
let files = BTreeMap::from([(91, source.into())]);
let missing = Index::parse(&ordinary(), &files, &mut budget()).unwrap_err();
assert_eq!(missing.file, Some(91));
assert!(missing.error.message.contains("stabilization"));
let mut limits = super::super::budget::Limits::source8(ParseLimits::UNLIMITED);
limits.bytes = 1;
let mut work = Budget::new(limits).unwrap();
assert!(Index::parse(&ordinary(), &sources(), &mut work).is_err());
assert!(work.active().is_err());
}
fn on_small_stack(work: impl FnOnce() + Send + 'static) {
// Match the established core language_nesting_tests worker-stack request.
// This is a requested platform stack size, not a measured peak-byte claim.
std::thread::Builder::new()
.name("analysis-index-128k".into())
.stack_size(128 * 1024)
.spawn(work)
.unwrap()
.join()
.unwrap();
}
fn chain(first: Child, edges: usize) -> Path {
let mut path = Path::default();
for position in 0..edges {
path = Path(Some(Arc::new(Step {
parent: path,
edge: if position == 0 { first } else { Child::Operand },
})));
}
path
}
#[test]
fn maximum_index_paths_compare_lookup_debug_and_drop_on_128k_stack() {
on_small_stack(|| {
let frames = super::super::budget::Limits::source8(ParseLimits::UNLIMITED).frames;
assert_eq!(frames, 256);
let edges = frames - 1; // root has frame depth one
let mut work = budget();
work.frame(frames, Span::default()).unwrap();
let left = chain(Child::Left, edges);
let equal = chain(Child::Left, edges); // independent allocations
let right = chain(Child::Right, edges);
assert!(!Arc::ptr_eq(
left.0.as_ref().unwrap(),
equal.0.as_ref().unwrap()
));
assert_eq!(left, equal);
assert_eq!(left.partial_cmp(&equal), Some(Ordering::Equal));
// The difference is at the root-most edge: leaf-first walks every link.
assert_eq!(left.cmp(&right), Child::Left.cmp(&Child::Right));
assert_eq!(right.cmp(&left), left.cmp(&right).reverse());
assert!(Path::default() < left);
assert!(chain(Child::Operand, edges - 1) < chain(Child::Operand, edges));
let shared = left.clone();
assert!(Arc::ptr_eq(
left.0.as_ref().unwrap(),
shared.0.as_ref().unwrap()
));
let leaf = Arc::downgrade(left.0.as_ref().unwrap());
let key = |path| ExpressionKey {
owner: Id::from_u128(1),
field: Field::Body,
path,
};
let mut expressions = BTreeMap::new();
assert_eq!(expressions.insert(key(left.clone()), 1), None);
assert_eq!(expressions.insert(key(equal.clone()), 2), Some(1));
assert_eq!(expressions.insert(key(right.clone()), 3), None);
assert_eq!(expressions.get(&key(equal.clone())), Some(&2));
assert_eq!(expressions.get(&key(chain(Child::Right, edges))), Some(&3));
assert_eq!(expressions.len(), 2);
let occurrence = |path| OccurrenceKey {
owner: Id::from_u128(1),
field: Some(Field::Body),
path: Some(path),
role: TokenRole::Use(Role::ParameterUse),
target: Target::Parameter {
calculation: Id::from_u128(1),
parameter: a::ParameterId(Id::from_u128(2)),
},
};
let mut occurrences = BTreeSet::new();
assert!(occurrences.insert(occurrence(left.clone())));
assert!(!occurrences.insert(occurrence(equal.clone())));
assert!(occurrences.contains(&occurrence(chain(Child::Left, edges))));
let debug = format!("{left:?}");
assert_eq!(debug.matches("Operand").count(), edges - 1);
assert!(debug.ends_with("Left]"));
assert!(!format!("{expressions:?} {occurrences:?}").is_empty());
drop(expressions);
drop(occurrences);
drop(shared);
drop(left);
assert!(leaf.upgrade().is_none(), "maps/clones release their chain");
drop(equal);
drop(right);
// Dropping a child stops at a shared ancestor, which remains usable.
let parent = chain(Child::Left, edges - 1);
let ancestor = Arc::downgrade(parent.0.as_ref().unwrap());
let child = Path(Some(Arc::new(Step {
parent: parent.clone(),
edge: Child::Right,
})));
drop(child);
assert_eq!(parent.edges().len(), edges - 1);
drop(parent);
assert!(ancestor.upgrade().is_none());
// No other strong owners: final destruction must peel the full chain.
let unique = chain(Child::Left, edges);
let final_leaf = Arc::downgrade(unique.0.as_ref().unwrap());
drop(unique);
assert!(final_leaf.upgrade().is_none());
});
}
#[test]
fn deep_parse_index_plan_and_failed_build_cleanup_on_128k_stack() {
on_small_stack(|| {
use super::super::update::Plan;
let deep = format!("{}$a{}", "abs(".repeat(127), ")".repeat(127));
let source = format!(
r#"calculation f "Deep 雪" id "00000000-0000-0000-0000-000000000081" {{
input a id "00000000-0000-0000-0000-000000000083" : Number;
result Number; body {deep};
}}
calculation caller "Caller" id "00000000-0000-0000-0000-000000000082" {{
result Number; body @f(a: 1);
}}
"#
);
let sources = BTreeMap::from([(47, source.clone())]);
let skeleton = ordinary();
let mut work = budget();
let (index, lowered) = Index::parse(&skeleton, &sources, &mut work).unwrap();
assert_eq!(work.usage.bytes, source.len());
assert_eq!(index.expressions.len(), 130); // 128 unary/leaf + call/argument
assert_eq!(
index.expressions.keys().map(|k| k.path.edges().len()).max(),
Some(127)
);
let renames = BTreeMap::from([
(Target::Global(Id::from_u128(0x81)), "renamed_deep".into()),
(
Target::Parameter {
calculation: Id::from_u128(0x81),
parameter: a::ParameterId(Id::from_u128(0x83)),
},
"renamed_arg".into(),
),
]);
let usage = work.usage;
let plan = Plan::renames(&index, &renames, &work).unwrap();
assert_eq!(plan.edit_count(), 5); // declaration/call + input/use/label
let output = plan.apply(&sources, &work).unwrap();
assert_eq!(work.usage, usage);
// Independent complete original-token oracle; no index-derived offsets.
let expected_source = source
.replace("calculation f ", "calculation renamed_deep ")
.replace("input a id", "input renamed_arg id")
.replace("$a", "$renamed_arg")
.replace("@f(a: 1)", "@renamed_deep(renamed_arg: 1)");
assert_eq!(output, BTreeMap::from([(47, expected_source)]));
let (renamed_index, renamed) = Index::parse(&skeleton, &output, &mut budget()).unwrap();
assert_eq!(
index.occurrences.keys().collect::<Vec<_>>(),
renamed_index.occurrences.keys().collect::<Vec<_>>()
);
// Compare complete independently constructed models, including deepest
// parameter identity and the call's parameter-keyed argument map.
for (lowered, renamed) in [(lowered, false), (renamed, true)] {
let mut expected = skeleton.clone();
let parameter = a::ParameterId(Id::from_u128(0x83));
let mut expression = a::Expr::Parameter(parameter);
for _ in 0..127 {
expression = a::Expr::Unary {
op: a::UnaryOp::Absolute,
operand: Box::new(expression),
};
}
for (id, alias, name, parameters, expression) in [
(
0x81,
if renamed { "renamed_deep" } else { "f" },
"Deep 雪",
vec![a::Parameter {
id: parameter,
name: if renamed { "renamed_arg" } else { "a" }.into(),
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
}],
expression,
),
(
0x82,
"caller",
"Caller",
Vec::new(),
a::Expr::Call {
definition: a::DefinitionRef::Local(Id::from_u128(0x81)),
arguments: BTreeMap::from([(
parameter,
a::Expr::Number(crate::analysis_numeric::Exact::from_integer(1)),
)]),
},
),
] {
let id = Id::from_u128(id);
expected.definitions.insert(
id,
a::Definition {
id,
alias: alias.into(),
name: name.into(),
description: String::new(),
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
parameters,
result: a::AnalysisType::Scalar(a::ScalarType::Number),
body: a::Binding::Expression(expression),
}),
},
);
}
let mut actual = skeleton.clone();
actual.definitions.extend(lowered.definitions);
actual.elements.extend(lowered.elements);
assert_eq!(actual, expected);
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
drop(actual);
drop(expected);
}
drop(plan);
drop(renamed_index);
drop(index);
// Failure after the deepest keys have been allocated must clean up on
// the same stack; no modified Lowered escapes this private test seam.
let mut work = budget();
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
let deepest = lowered
.expressions
.iter()
.find(|site| &sources[&site.file][site.span.start..site.span.end] == "$a")
.unwrap();
lowered.expressions.push(lower::ExpressionSite {
file: deepest.file,
owner: deepest.owner,
field: deepest.field,
parent: deepest.parent,
span: deepest.span,
});
let failure =
Index::build(&skeleton, &sources, &units, &witnesses, &lowered, &mut work).unwrap_err();
assert_eq!(failure.message, "Duplicate qualified expression path.");
lowered.expressions.pop();
let mut limits = super::super::budget::Limits::source8(ParseLimits::UNLIMITED);
limits.frames = 127;
let mut bounded = Budget::new(limits).unwrap();
let failure = Index::build(
&skeleton,
&sources,
&units,
&witnesses,
&lowered,
&mut bounded,
)
.unwrap_err();
assert!(failure.message.contains("syntax frames"));
assert!(bounded.active().is_err());
drop(lowered);
drop(witnesses);
drop(units);
let rejected = BTreeMap::from([(47, source.replace(&deep, &format!("abs({deep})")))]);
let failure = Index::parse(&skeleton, &rejected, &mut budget()).unwrap_err();
assert!(failure.error.message.contains("logical depth"));
});
}
@@ -34,7 +34,7 @@ pub(super) enum Category {
Calculation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Target {
Global(Id),
Parameter {
@@ -43,7 +43,7 @@ pub(super) enum Target {
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Field {
Algebra,
Kind,
@@ -59,7 +59,7 @@ pub(super) enum Field {
Override(Id),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Role {
Reference,
ParameterDeclaration,
@@ -70,7 +70,7 @@ pub(super) enum Role {
}
/// Stable structural edges, independent of alias spelling and argument order.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Child {
Item(usize),
Operand,
@@ -349,6 +349,11 @@ impl Cursor<'_, '_> {
_ => 0,
};
if child_depth >= crate::analysis::MAX_EXPRESSION_DEPTH {
// The rejected parent still owns accepted, potentially depth-128
// children. Recursive derived cleanup here runs beneath the parser
// frames and can exhaust the bounded native stack before returning
// the depth error. Consume those children with an explicit worklist.
drop_rejected_kind(kind);
return Err(error(span, "Expression logical depth exceeds 128."));
}
Ok(Expression {
@@ -399,6 +404,42 @@ impl Cursor<'_, '_> {
self.node(kind, span)
}
}
fn drop_rejected_kind(kind: ExpressionKind) {
let mut pending = vec![kind];
while let Some(kind) = pending.pop() {
match kind {
ExpressionKind::Unary { operand, .. } => pending.push(operand.kind),
ExpressionKind::Binary { left, right, .. } => {
pending.push(left.kind);
pending.push(right.kind);
}
ExpressionKind::If {
condition,
when_true,
when_false,
} => {
pending.push(condition.kind);
pending.push(when_true.kind);
pending.push(when_false.kind);
}
ExpressionKind::List(values) => pending.extend(values.into_iter().map(|v| v.kind)),
ExpressionKind::Call { arguments, .. } => {
pending.extend(arguments.into_iter().map(|a| a.value.kind));
}
ExpressionKind::Sum { values, initial } => {
pending.push(values.kind);
pending.push(initial.kind);
}
ExpressionKind::Power { base, .. } => pending.push(base.kind),
ExpressionKind::AsKind { value, .. } => pending.push(value.kind),
ExpressionKind::Boolean(_)
| ExpressionKind::Number(_)
| ExpressionKind::Quantity { .. }
| ExpressionKind::Property(_)
| ExpressionKind::Parameter(_) => {}
}
}
}
fn comparison(op: BinaryOp) -> bool {
matches!(
op,
@@ -108,6 +108,49 @@ fn flat_binary_depth_and_malformed_constructor_retain_bounded_completed_children
assert_eq!(number(&expression("1")), "1");
}
#[test]
fn every_parent_shape_rejects_depth129_and_cleans_children_on_128k_stack() {
std::thread::Builder::new()
.name("analysis-syntax-rejection-128k".into())
.stack_size(128 * 1024)
.spawn(|| {
// Rotate through every recursive syntax shape, so the rejected
// parent's complete child tree also exercises every cleanup arm.
let wrap = |shape, child: &str| match shape {
0 => format!("abs({child})"),
1 => format!("({child}) + 1"),
2 => format!("if(true, {child}, 0)"),
3 => format!("[{child}]"),
4 => format!("@f(a: {child})"),
5 => format!("sum({child}, 0)"),
6 => format!("power({child}, 1)"),
7 => format!("as_kind(@k, {child})"),
_ => unreachable!(),
};
let mut deep = "1".to_owned();
for layer in 0..127 {
deep = wrap(layer % 8, &deep);
}
assert_eq!(expression(&deep).depth, 128);
for shape in 0..8 {
let source = wrap(shape, &deep);
let failure = parse(&source, &mut budget()).unwrap_err();
assert_eq!(failure.message, "Expression logical depth exceeds 128.");
assert_eq!(
failure.span,
Span {
start: 0,
end: source.len()
}
);
assert_eq!(number(&expression("1")), "1");
}
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn significant_ratio_and_quantity_trivia_preserve_original_spans() {
let source = "ratio // explanation café\n ( - // sign\n 3, // denominator\n 4 ) // measurement\n [@millisecond]";
@@ -0,0 +1,316 @@
//! Simultaneous typed token edits. This is a staged plan, not source acceptance:
//! the bundle owner must merge ordinary edits, reparse/recompile the exact bytes,
//! admit once and compare the complete intended Project before installation.
use super::index::{Index, Location, OccurrenceKey, checked_source};
use super::lower::Target;
use super::{Budget, Error, Span, error};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq)]
struct Edit {
key: OccurrenceKey,
location: Location,
}
#[derive(Debug)]
pub(super) struct Plan<'i, 's> {
index: &'i Index<'s>,
names: BTreeMap<Target, String>,
edits: Vec<Edit>,
}
impl<'i, 's> Plan<'i, 's> {
/// All names are the desired final names. Validate the final namespaces,
/// rather than applying one rename at a time: alias/parameter swaps are legal.
pub fn renames(
index: &'i Index<'s>,
renames: &BTreeMap<Target, String>,
budget: &Budget,
) -> Result<Self, Error> {
let plan = Self::prepare(index, renames, budget)?;
plan.validate(index.sources(), budget)?;
plan.output_lengths(index.sources(), budget)?;
Ok(plan)
}
fn validate_names(
index: &Index<'_>,
renames: &BTreeMap<Target, String>,
budget: &Budget,
) -> Result<(), Error> {
budget.active()?;
budget.lowering_entries(renames.len(), Span::default())?;
let mut bytes = 0usize;
for (target, name) in renames {
if !index.names().contains_key(target) {
return Err(error(
Span::default(),
"Rename has no qualified source target.",
));
}
if !crate::valid_alias(name) {
return Err(error(
Span::default(),
"Invalid alias or scoped parameter name.",
));
}
bytes = bytes
.checked_add(name.len())
.ok_or_else(|| error(Span::default(), "Rename size overflow."))?;
budget.index_bytes(bytes, Span::default())?;
}
let mut final_names = BTreeSet::new();
for (target, original) in index.names() {
let name = renames.get(target).unwrap_or(original);
let scope = match target {
Target::Global(_) => None,
Target::Parameter { calculation, .. } => Some(*calculation),
};
budget.lowering_entries(final_names.len() + 1, Span::default())?;
if !final_names.insert((scope, name.as_str())) {
return Err(error(
Span::default(),
"Rename creates a duplicate name in its namespace.",
));
}
}
Ok(())
}
fn prepare(
index: &'i Index<'s>,
renames: &BTreeMap<Target, String>,
budget: &Budget,
) -> Result<Self, Error> {
// Validate record/name storage before copying any owned final names.
Self::validate_names(index, renames, budget)?;
let mut edits = Vec::new();
for (key, location) in index.occurrences() {
if renames
.get(&key.target)
.is_some_and(|name| index.names().get(&key.target) != Some(name))
{
budget.lowering_occurrences(edits.len() + 1, location.span)?;
edits.push(Edit {
key: key.clone(),
location: *location,
});
}
}
edits.sort_by_key(|edit| {
(
edit.location.file,
edit.location.span.start,
edit.location.span.end,
)
});
Ok(Self {
index,
names: renames.clone(),
edits,
})
}
pub fn edit_count(&self) -> usize {
self.edits.len()
}
/// Recheck the complete original witness and complete rename intent.
/// A changed unrelated file is stale too. This does not construct output.
fn validate(&self, current: &BTreeMap<usize, String>, budget: &Budget) -> Result<(), Error> {
budget.active()?;
if current != self.index.sources() {
return Err(error(
Span::default(),
"Source index is stale: original file set or bytes changed.",
));
}
budget.lowering_entries(current.len(), Span::default())?;
budget.lowering_occurrences(self.edits.len(), Span::default())?;
let mut previous: Option<Location> = None;
let mut seen = BTreeSet::new();
for edit in &self.edits {
let at = edit.location;
if self.index.occurrences().get(&edit.key) != Some(&at) || !seen.insert(&edit.key) {
return Err(error(at.span, "Forged or duplicate edit occurrence."));
}
let old = checked_source(current, at)?;
if self
.index
.names()
.get(&edit.key.target)
.is_none_or(|name| name != old)
{
return Err(error(at.span, "Stale original token spelling."));
}
self.names
.get(&edit.key.target)
.ok_or_else(|| error(at.span, "Edit has no final target name."))?;
if previous.is_some_and(|prior| {
prior.file > at.file || (prior.file == at.file && prior.span.end > at.span.start)
}) {
return Err(error(at.span, "Overlapping or unordered source edits."));
}
previous = Some(at);
}
for (key, at) in self.index.occurrences() {
let required = self
.names
.get(&key.target)
.is_some_and(|name| self.index.names().get(&key.target) != Some(name));
if required != seen.contains(key) {
return Err(error(
at.span,
"Edit set differs from the complete qualified rename plan.",
));
}
}
Ok(())
}
/// Standalone output preflight, after ownership validation. Composition
/// defers this hypothetical typed-only size to its future combined emitter.
fn output_lengths(
&self,
current: &BTreeMap<usize, String>,
budget: &Budget,
) -> Result<BTreeMap<usize, usize>, Error> {
let mut lengths: BTreeMap<_, _> = current
.iter()
.map(|(&file, source)| (file, source.len()))
.collect();
for edit in &self.edits {
let at = edit.location;
let length = lengths.get_mut(&at.file).expect("validated edit file");
*length = length
.checked_sub(at.span.end - at.span.start)
.and_then(|size| size.checked_add(self.names[&edit.key.target].len()))
.ok_or_else(|| error(at.span, "Updated source size overflow."))?;
}
let mut total = 0usize;
for length in lengths.values() {
total = total
.checked_add(*length)
.ok_or_else(|| error(Span::default(), "Updated bundle size overflow."))?;
budget.index_bytes(total, Span::default())?;
}
Ok(lengths)
}
pub fn apply(
&self,
current: &BTreeMap<usize, String>,
budget: &Budget,
) -> Result<BTreeMap<usize, String>, Error> {
self.validate(current, budget)?;
let lengths = self.output_lengths(current, budget)?;
let mut result = BTreeMap::new();
let mut edits = self.edits.iter().peekable();
for (&file, source) in current {
let mut output = String::with_capacity(lengths[&file]);
let mut start = 0;
while edits.peek().is_some_and(|edit| edit.location.file == file) {
let edit = edits.next().expect("peeked edit");
output.push_str(&source[start..edit.location.span.start]);
output.push_str(&self.names[&edit.key.target]);
start = edit.location.span.end;
}
output.push_str(&source[start..]);
result.insert(file, output);
}
Ok(result)
}
}
/// Private typed-only composition planning. The caller must supply its inherited
/// parse Budget; this type keeps that same immutable budget through export.
/// It does not establish the future complete bundle/file-table/parse-session
/// witness. In particular, an Index currently does not store its parse profile.
pub(super) struct Composition<'b, 'i, 's> {
plan: Plan<'i, 's>,
budget: &'b Budget,
}
impl std::fmt::Debug for Composition<'_, '_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Composition")
.field("plan", &self.plan)
.finish_non_exhaustive()
}
}
impl<'b, 'i, 's> Composition<'b, 'i, 's> {
pub fn renames(
index: &'i Index<'s>,
renames: &BTreeMap<Target, String>,
inherited: &'b Budget,
) -> Result<Self, Error> {
let plan = Plan::prepare(index, renames, inherited)?;
plan.validate(index.sources(), inherited)?;
Ok(Self {
plan,
budget: inherited,
})
}
/// Export the entire validated plan against exact original numeric file keys
/// and bytes. There is no output allocation, application or subset-accept API.
/// A future collector must check this actual index witness and the complete
/// combined output bounds before applying either producer's edits once.
pub fn original_edits(
&self,
current: &BTreeMap<usize, String>,
) -> Result<OriginalEdits<'_, 'i, 's>, Error> {
Plan::validate_names(self.plan.index, &self.plan.names, self.budget)?;
self.plan.validate(current, self.budget)?;
Ok(OriginalEdits { plan: &self.plan })
}
}
#[derive(Debug)]
pub(super) struct OriginalEdits<'p, 'i, 's> {
plan: &'p Plan<'i, 's>,
}
impl<'s> OriginalEdits<'_, '_, 's> {
pub fn index(&self) -> &Index<'s> {
self.plan.index
}
pub fn len(&self) -> usize {
self.plan.edits.len()
}
pub fn is_empty(&self) -> bool {
self.plan.edits.is_empty()
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = OriginalEdit<'_>> {
self.plan.edits.iter().map(|edit| OriginalEdit {
key: &edit.key,
location: edit.location,
original: &self.plan.index.sources()[&edit.location.file]
[edit.location.span.start..edit.location.span.end],
replacement: &self.plan.names[&edit.key.target],
})
}
}
/// A borrowed inspection record, never independent edit authority. Only an
/// intact OriginalEdits batch can carry this producer's complete rename intent.
#[derive(Debug)]
pub(super) struct OriginalEdit<'p> {
key: &'p OccurrenceKey,
location: Location,
original: &'p str,
replacement: &'p str,
}
impl OriginalEdit<'_> {
pub fn key(&self) -> &OccurrenceKey {
self.key
}
pub fn location(&self) -> Location {
self.location
}
pub fn original(&self) -> &str {
self.original
}
pub fn replacement(&self) -> &str {
self.replacement
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,744 @@
use super::super::budget::Limits;
use super::*;
use crate::analysis as a;
use crate::analysis_numeric::Exact;
use crate::language::ParseLimits;
use crate::{Id, Layer, Project};
const DEFINITIONS: &str = "// 雪 @f(a:) $a: comment spellings stay exact\r\n\
calculation f \"f a b\" id \"00000000-0000-0000-0000-000000000100\" {\r\n\
input a id \"00000000-0000-0000-0000-000000000200\" : Number;\r\n\
input b id \"00000000-0000-0000-0000-000000000201\" : Number;\r\n\
result Number; body $a + $b;\r\n}\r\n\
calculation g \"g a b\" id \"00000000-0000-0000-0000-000000000200\" {\r\n\
input a id \"00000000-0000-0000-0000-000000000200\" : Number;\r\n\
input b id \"00000000-0000-0000-0000-000000000201\" : Number;\r\n\
result Number; body $a + $b;\r\n}\r\n";
const CALLER: &str = "calculation caller \"Caller\" id \"00000000-0000-0000-0000-000000000300\" {\n\
result Number; body @f(b: 2, a: 1) // first 雪 @f(a:)\n\
+ @f(a: 3, b: 4) // second\n\
+ @g(a: 5, b: 6);\n}\n";
fn budget() -> Budget {
Budget::new(Limits::source8(ParseLimits::UNLIMITED)).unwrap()
}
fn global(id: u128) -> Target {
Target::Global(Id::from_u128(id))
}
fn parameter(calculation: u128, parameter: u128) -> Target {
Target::Parameter {
calculation: Id::from_u128(calculation),
parameter: a::ParameterId(Id::from_u128(parameter)),
}
}
fn sources() -> BTreeMap<usize, String> {
BTreeMap::from([(11, DEFINITIONS.into()), (23, CALLER.into())])
}
fn skeleton() -> Project {
let mut project = super::super::model_oracle::project(Layer::Oa);
project.definitions.clear();
project
.elements
.retain(|_, element| !element.kind().is_analysis());
project
}
fn binary(left: a::Expr, right: a::Expr) -> a::Expr {
a::Expr::Binary {
op: a::BinaryOp::Add,
left: Box::new(left),
right: Box::new(right),
}
}
fn expected(renamed: bool) -> Project {
// Independent model construction: no parsed AST, source index, writer or
// generated edit participates in this expected complete Project.
let mut project = skeleton();
let p = |id| a::ParameterId(Id::from_u128(id));
for (id, alias, name) in [
(0x100, if renamed { "g" } else { "f" }, "f a b"),
(0x200, if renamed { "f" } else { "g" }, "g a b"),
] {
let names = if renamed && id == 0x100 {
["b", "a"]
} else {
["a", "b"]
};
let body = a::CalculationDefinition {
parameters: [(0x200, names[0]), (0x201, names[1])]
.into_iter()
.map(|(id, name)| a::Parameter {
id: p(id),
name: name.into(),
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
})
.collect(),
result: a::AnalysisType::Scalar(a::ScalarType::Number),
body: a::Binding::Expression(binary(
a::Expr::Parameter(p(0x200)),
a::Expr::Parameter(p(0x201)),
)),
};
let id = Id::from_u128(id);
project.definitions.insert(
id,
a::Definition {
id,
alias: alias.into(),
name: name.into(),
description: String::new(),
body: a::DefinitionBody::Calculation(body),
},
);
}
let call = |id, x, y| a::Expr::Call {
definition: a::DefinitionRef::Local(Id::from_u128(id)),
arguments: BTreeMap::from([
(p(0x200), a::Expr::Number(Exact::from_integer(x))),
(p(0x201), a::Expr::Number(Exact::from_integer(y))),
]),
};
let id = Id::from_u128(0x300);
project.definitions.insert(
id,
a::Definition {
id,
alias: "caller".into(),
name: "Caller".into(),
description: String::new(),
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
parameters: Vec::new(),
result: a::AnalysisType::Scalar(a::ScalarType::Number),
body: a::Binding::Expression(binary(
binary(call(0x100, 1, 2), call(0x100, 3, 4)),
call(0x200, 5, 6),
)),
}),
},
);
project
}
fn merge(mut project: Project, lowered: super::super::lower::Lowered) -> Project {
project.definitions.extend(lowered.definitions);
project.elements.extend(lowered.elements);
project
}
fn rename_map() -> BTreeMap<Target, String> {
BTreeMap::from([
(global(0x100), "g".into()),
(global(0x200), "f".into()),
(parameter(0x100, 0x200), "b".into()),
(parameter(0x100, 0x201), "a".into()),
])
}
#[test]
fn simultaneous_global_and_scoped_swaps_match_original_token_and_whole_model_oracles() {
let sources = sources();
let mut work = budget();
let (index, lowered) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
assert_eq!(merge(skeleton(), lowered), expected(false));
let plan = Plan::renames(&index, &rename_map(), &work).unwrap();
// List expected edits against the original authored bytes independently of
// the index. Full contexts deliberately exclude equal strings/comments.
let mut edits = Vec::new();
let mut add = |file, context: &str, token: &str, replacement: &str| {
let source = &sources[&file];
let context_start = source.find(context).unwrap();
let start = context_start + context.find(token).unwrap();
edits.push((file, start, start + token.len(), replacement.to_owned()));
};
add(11, "calculation f ", "f", "g");
add(11, "calculation g ", "g", "f");
add(11, "input a id", "a", "b");
add(11, "input b id", "b", "a");
add(11, "body $a + $b;", "a", "b");
add(11, "body $a + $b;", "b;", "a;");
// The last oracle edit above includes ';' to disambiguate 'b' in 'body';
// restrict it back to the exact name token for the span oracle.
edits.last_mut().unwrap().2 -= 1;
edits.last_mut().unwrap().3.pop();
for call in ["@f(b: 2, a: 1)", "@f(a: 3, b: 4)"] {
add_call_edits(&sources, &mut edits, call);
}
let start = CALLER.find("@g(a: 5, b: 6)").unwrap() + 1;
edits.push((23, start, start + 1, "f".into()));
edits.sort_by_key(|edit| (edit.0, edit.1));
assert_eq!(plan.edit_count(), 13);
assert_eq!(
plan.edits
.iter()
.map(|edit| (
edit.location.file,
edit.location.span.start,
edit.location.span.end,
plan.names[&edit.key.target].clone()
))
.collect::<Vec<_>>(),
edits
);
let mut exact = BTreeMap::new();
for (&file, source) in &sources {
let mut output = source.clone();
for (_, start, end, value) in edits.iter().filter(|edit| edit.0 == file).rev() {
output.replace_range(*start..*end, value);
}
exact.insert(file, output);
}
let output = plan.apply(&sources, &work).unwrap();
assert_eq!(output, exact);
assert!(output[&11].contains("// 雪 @f(a:) $a: comment spellings stay exact\r\n"));
assert!(output[&23].contains("// first 雪 @f(a:)\n"));
assert!(output[&11].contains("\"f a b\""));
let (_, lowered) = Index::parse(&skeleton(), &output, &mut budget()).unwrap();
let actual = merge(skeleton(), lowered);
assert_eq!(actual, expected(true));
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
}
fn add_call_edits(
sources: &BTreeMap<usize, String>,
edits: &mut Vec<(usize, usize, usize, String)>,
call: &str,
) {
let start = sources[&23].find(call).unwrap();
for (offset, replacement) in [
(1, "g"),
(call.find("a:").unwrap(), "b"),
(call.find("b:").unwrap(), "a"),
] {
edits.push((23, start + offset, start + offset + 1, replacement.into()));
}
}
#[test]
fn alias_edits_preserve_unit_type_override_tokens_and_unrelated_authored_bytes() {
let source = include_str!("../lower/fixtures/typed-oa.arcadia")
.replace("750 [@millisecond]", "750 // exact 雪\r\n [@millisecond]");
let files = BTreeMap::from([(59, source.clone())]);
let mut work = budget();
let (index, _) = Index::parse(&skeleton(), &files, &mut work).unwrap();
let mut expected_project = super::super::model_oracle::project(Layer::Oa);
let id = |name: &str| {
index
.names()
.iter()
.find(|(target, alias)| matches!(target, Target::Global(_)) && alias.as_str() == name)
.map(|(target, _)| *target)
.unwrap()
};
let renames = BTreeMap::from([
(id("duration"), "time_kind".into()),
(id("millisecond"), "ms_unit".into()),
(id("receive_time"), "arrival".into()),
]);
let plan = Plan::renames(&index, &renames, &work).unwrap();
let output = plan.apply(&files, &work).unwrap();
let expected_source = source
.replace("quantity_kind duration ", "quantity_kind time_kind ")
.replace("@duration", "@time_kind")
.replace("unit millisecond ", "unit ms_unit ")
.replace("@millisecond", "@ms_unit")
.replace("oa_property receive_time ", "oa_property arrival ")
.replace("@receive_time", "@arrival");
assert_eq!(output[&59], expected_source);
for definition in expected_project.definitions.values_mut() {
if let Some(name) = renames.get(&Target::Global(definition.id)) {
definition.alias = name.clone();
}
}
for element in expected_project.elements.values_mut() {
if let Some(name) = renames.get(&Target::Global(element.id)) {
element.alias = name.clone();
}
}
let (_, lowered) = Index::parse(&skeleton(), &output, &mut budget()).unwrap();
assert_eq!(merge(skeleton(), lowered), expected_project);
}
#[test]
fn rename_rejects_unknown_invalid_and_colliding_final_names_but_allows_noop() {
let sources = sources();
let work = &mut budget();
let (index, _) = Index::parse(&skeleton(), &sources, work).unwrap();
for renames in [
BTreeMap::from([(global(999), "new".into())]),
BTreeMap::from([(global(0x100), "bad name".into())]),
BTreeMap::from([(global(0x100), "g".into())]),
BTreeMap::from([(parameter(0x100, 0x200), "b".into())]),
] {
assert!(Plan::renames(&index, &renames, work).is_err());
}
let plan = Plan::renames(&index, &BTreeMap::from([(global(0x100), "f".into())]), work).unwrap();
assert_eq!(plan.edit_count(), 0);
assert_eq!(plan.apply(&sources, work).unwrap(), sources);
}
#[test]
fn stale_file_sets_bytes_forged_spans_and_duplicate_edits_fail_before_output() {
let sources = sources();
let mut work = budget();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
for damage in 0..6 {
let mut current = sources.clone();
let mut plan = Plan::renames(&index, &rename_map(), &work).unwrap();
match damage {
0 => current
.get_mut(&23)
.unwrap()
.push_str("// unrelated change"),
1 => {
current.remove(&23);
}
2 => {
current.insert(99, String::new());
}
3 => plan.edits[0].location.span.start += 1,
4 => plan.edits.insert(1, plan.edits[0].clone()),
_ => {
plan.edits.pop();
}
}
assert!(plan.apply(&current, &work).is_err(), "damage {damage}");
}
}
#[test]
fn output_and_plan_limits_are_aggregate_and_do_not_reset_parse_work() {
let sources = sources();
let mut work = budget();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
let usage = work.usage;
let numeric = work.numeric_units();
let plan = Plan::renames(&index, &rename_map(), &work).unwrap();
let output = plan.apply(&sources, &work).unwrap();
assert_eq!(work.usage, usage);
assert_eq!(work.numeric_units(), numeric);
let bytes = output.values().map(String::len).sum();
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
limits.bytes = bytes;
assert!(plan.apply(&sources, &Budget::new(limits).unwrap()).is_ok());
limits.bytes -= 1;
assert!(plan.apply(&sources, &Budget::new(limits).unwrap()).is_err());
limits = Limits::source8(ParseLimits::UNLIMITED);
limits.references = 0;
limits.entries = 0;
assert!(Plan::renames(&index, &rename_map(), &Budget::new(limits).unwrap()).is_err());
}
#[test]
fn composition_exports_complete_borrowed_swap_witnesses_and_empty_noops() {
use super::super::index::TokenRole;
use super::super::lower::{Child, Field, Role};
type Key = (Id, Option<Field>, Option<Vec<Child>>, TokenRole, Target);
type Row = (Location, String, String, Key);
let sources = sources();
let mut work = budget();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
let usage = work.usage;
let numeric = work.numeric_units();
let composition = Composition::renames(&index, &rename_map(), &work).unwrap();
assert!(std::ptr::eq(composition.budget, &work));
// A byte-identical snapshot is valid; records still borrow the actual index.
let batch = composition.original_edits(&sources.clone()).unwrap();
assert!(std::ptr::eq(batch.index(), &index));
assert_eq!(batch.len(), 13);
assert!(!batch.is_empty());
assert_eq!(batch.iter().len(), 13);
// Independently authored occurrence identities and full source contexts.
// No index offset/path or generated output defines this oracle.
let mut expected: Vec<Row> = Vec::new();
let mut add = |file, context: &str, prefix: &str, old: &str, new: &str, key: Key| {
let start = sources[&file].find(context).unwrap() + prefix.len();
assert_eq!(&sources[&file][start..start + old.len()], old);
expected.push((
Location {
file,
span: Span {
start,
end: start + old.len(),
},
},
old.into(),
new.into(),
key,
));
};
for (id, old, new, context) in [
(0x100, "f", "g", "calculation f "),
(0x200, "g", "f", "calculation g "),
] {
add(
11,
context,
"calculation ",
old,
new,
(
Id::from_u128(id),
None,
None,
TokenRole::Declaration,
global(id),
),
);
}
for (id, old, new, context, edge, prefix) in [
(0x200, "a", "b", "input a id", Child::Left, "body $"),
(0x201, "b", "a", "input b id", Child::Right, "body $a + $"),
] {
let target = parameter(0x100, id);
add(
11,
context,
"input ",
old,
new,
(
Id::from_u128(0x100),
Some(Field::Input(a::ParameterId(Id::from_u128(id)))),
None,
TokenRole::Use(Role::ParameterDeclaration),
target,
),
);
add(
11,
"body $a + $b;",
prefix,
old,
new,
(
Id::from_u128(0x100),
Some(Field::Body),
Some(vec![edge]),
TokenRole::Use(Role::ParameterUse),
target,
),
);
}
for (context, path) in [
("@f(b: 2, a: 1)", vec![Child::Left, Child::Left]),
("@f(a: 3, b: 4)", vec![Child::Left, Child::Right]),
] {
add(
23,
context,
"@",
"f",
"g",
(
Id::from_u128(0x300),
Some(Field::Body),
Some(path.clone()),
TokenRole::Use(Role::CallTarget),
global(0x100),
),
);
for (id, old, new) in [(0x200, "a", "b"), (0x201, "b", "a")] {
let offset = context.find(&format!("{old}:")).unwrap();
add(
23,
context,
&context[..offset],
old,
new,
(
Id::from_u128(0x300),
Some(Field::Body),
Some(path.clone()),
TokenRole::Use(Role::ArgumentLabel),
parameter(0x100, id),
),
);
}
}
add(
23,
"@g(a: 5, b: 6)",
"@",
"g",
"f",
(
Id::from_u128(0x300),
Some(Field::Body),
Some(vec![Child::Right]),
TokenRole::Use(Role::CallTarget),
global(0x200),
),
);
expected.sort_by_key(|row| (row.0.file, row.0.span.start, row.0.span.end));
let actual: Vec<Row> = batch
.iter()
.map(|edit| {
let key = edit.key();
(
edit.location(),
edit.original().into(),
edit.replacement().into(),
(
key.owner,
key.field,
key.path.as_ref().map(|path| path.edges()),
key.role,
key.target,
),
)
})
.collect();
assert_eq!(actual, expected);
let noop = Composition::renames(
&index,
&BTreeMap::from([
(global(0x100), "f".into()),
(parameter(0x200, 0x200), "a".into()),
]),
&work,
)
.unwrap();
let empty = noop.original_edits(&sources).unwrap();
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert_eq!(empty.iter().count(), 0);
assert_eq!(work.usage, usage);
assert_eq!(work.numeric_units(), numeric);
}
#[test]
fn composition_rejects_stale_or_damaged_complete_batches_before_export() {
let mut sources = sources();
sources.insert(99, "// unrelated 雪\r\n".into());
let original = sources.clone();
let mut work = budget();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
for damage in 0..10 {
let mut current = sources.clone();
let mut composition = Composition::renames(&index, &rename_map(), &work).unwrap();
match damage {
0 => current.get_mut(&99).unwrap().push_str("changed"),
1 => {
current.remove(&23);
}
2 => {
current.insert(101, String::new());
}
3 => composition.plan.edits[0].location.span.start += 1,
4 => composition
.plan
.edits
.insert(1, composition.plan.edits[0].clone()),
5 => {
composition.plan.edits.pop();
}
6 => composition.plan.edits.swap(0, 1),
7 => {
composition
.plan
.names
.insert(global(0x100), "bad name".into());
}
8 => {
composition.plan.names.insert(global(0x100), "f".into());
}
_ => {
composition.plan.names.insert(global(999), "missing".into());
}
}
let before = current.clone();
assert!(
composition.original_edits(&current).is_err(),
"damage {damage}"
);
assert_eq!(current, before);
}
assert_eq!(sources, original);
let failure = work.frame(257, Span::default()).unwrap_err();
assert_eq!(
Composition::renames(&index, &rename_map(), &work).unwrap_err(),
failure
);
}
#[test]
fn composition_defers_only_typed_output_size_with_independent_shrink_arithmetic() {
let sources = sources();
let cap = sources.values().map(String::len).sum();
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
limits.bytes = cap;
let mut work = Budget::new(limits).unwrap();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
assert_eq!(work.usage.bytes, cap);
let usage = work.usage;
let numeric = work.numeric_units();
let names = BTreeMap::from([(global(0x100), "ff".into())]);
assert_eq!(
Plan::renames(&index, &names, &work).unwrap_err().message,
"Source8 index/output byte limit exceeded."
);
let composition = Composition::renames(&index, &names, &work).unwrap();
let batch = composition.original_edits(&sources).unwrap();
assert_eq!(batch.len(), 3); // declaration and two distinct call targets
let growth: usize = batch
.iter()
.map(|edit| edit.replacement().len() - edit.original().len())
.sum();
assert_eq!(growth, 3);
assert_eq!(
composition.plan.apply(&sources, &work).unwrap_err().message,
"Source8 index/output byte limit exceeded."
);
// Independent future ordinary/scalar producer witness on untouched display
// name bytes. This is size/offset arithmetic only: this typed-only harness
// has no ordinary collector, mixed ownership proof or accepted output.
let start = DEFINITIONS.find("\"f a b\"").unwrap() + 1;
let shrink = Location {
file: 11,
span: Span {
start,
end: start + "f a b".len(),
},
};
assert_eq!(
&sources[&shrink.file][shrink.span.start..shrink.span.end],
"f a b"
);
for edit in batch.iter() {
let at = edit.location();
assert!(
at.file != shrink.file
|| at.span.end <= shrink.span.start
|| shrink.span.end <= at.span.start
);
}
let original_len = shrink.span.end - shrink.span.start;
assert_eq!(cap + growth - original_len + "fa".len(), cap);
assert_eq!(cap + growth - original_len + "fab".len(), cap + 1);
assert_eq!(work.usage, usage);
assert_eq!(work.numeric_units(), numeric);
}
#[test]
fn composition_keeps_name_storage_and_rename_record_limits_before_allocation() {
let sources = sources();
let cap = sources.values().map(String::len).sum();
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
limits.bytes = cap;
let mut work = Budget::new(limits).unwrap();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
let exact = BTreeMap::from([(global(0x100), "x".repeat(cap))]);
assert_eq!(
Composition::renames(&index, &exact, &work)
.unwrap()
.original_edits(&sources)
.unwrap()
.len(),
3
);
let over = BTreeMap::from([(global(0x100), "x".repeat(cap + 1))]);
assert_eq!(
Composition::renames(&index, &over, &work)
.unwrap_err()
.message,
"Source8 index/output byte limit exceeded."
);
for invalid in [
BTreeMap::from([(global(999), "unknown".into())]),
BTreeMap::from([(global(0x100), "bad name".into())]),
BTreeMap::from([(global(0x100), "g".into())]),
BTreeMap::from([(parameter(0x100, 0x200), "b".into())]),
] {
assert!(Composition::renames(&index, &invalid, &work).is_err());
}
// A typed-empty fixture isolates the rename-map record bound without
// syntax entry usage masking it. No ordinary edits are authorized here.
let ordinary = skeleton();
let count = ordinary.identities().len();
let files = BTreeMap::from([(1, "// empty typed unit\n".into())]);
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
limits.entries = count;
limits.references = 0;
let mut inherited = Budget::new(limits).unwrap();
let (index, _) = Index::parse(&ordinary, &files, &mut inherited).unwrap();
let mut names: BTreeMap<_, _> = index
.names()
.keys()
.enumerate()
.map(|(i, target)| (*target, format!("renamed_{i}")))
.collect();
assert_eq!(names.len(), count);
assert!(
Composition::renames(&index, &names, &inherited)
.unwrap()
.original_edits(&files)
.unwrap()
.is_empty()
);
names.insert(global(999), "additional".into());
assert_eq!(
Composition::renames(&index, &names, &inherited)
.unwrap_err()
.message,
"Source8 lowering entry limit exceeded."
);
}
#[test]
fn composition_checks_edit_record_ceiling_before_duplicate_membership() {
let sources = sources();
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
limits.entries = 128;
limits.references = 32;
let ceiling = limits.entries + limits.references;
let mut work = Budget::new(limits).unwrap();
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
let mut composition = Composition::renames(&index, &rename_map(), &work).unwrap();
assert_eq!(composition.original_edits(&sources).unwrap().len(), 13);
let duplicate = composition.plan.edits[0].clone();
composition.plan.edits = vec![duplicate.clone(); ceiling];
// At the exact count bound validation proceeds to the independent ownership
// error. The damaged plan never becomes a successful/authoritative batch.
assert_eq!(
composition.original_edits(&sources).unwrap_err().message,
"Forged or duplicate edit occurrence."
);
composition.plan.edits.push(duplicate);
assert_eq!(
composition.original_edits(&sources).unwrap_err().message,
"Source8 lowering occurrence limit exceeded."
);
}
#[test]
fn standalone_noop_apply_keeps_tighter_file_only_budget_and_output_boundary() {
let sources = sources();
let mut inherited = budget();
let (index, _) = Index::parse(&skeleton(), &sources, &mut inherited).unwrap();
let plan = Plan::renames(
&index,
&BTreeMap::from([(global(0x100), "f".into())]),
&inherited,
)
.unwrap();
assert_eq!(plan.edit_count(), 0);
assert!(index.names().len() > sources.len());
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
limits.entries = sources.len();
limits.references = 0;
limits.bytes = sources.values().map(String::len).sum();
// Standalone apply checks file/edit records, not the already validated
// catalog/name-storage allocation from construction. Rechecking the catalog
// under this intentionally tighter probe budget would reject this no-op.
assert_eq!(
plan.apply(&sources, &Budget::new(limits).unwrap()).unwrap(),
sources
);
limits.bytes -= 1;
assert_eq!(
plan.apply(&sources, &Budget::new(limits).unwrap())
.unwrap_err()
.message,
"Source8 index/output byte limit exceeded."
);
}