Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa7a980418 | ||
|
|
d11b99bb4a | ||
|
|
18073a6f2e | ||
|
|
f1c095654a | ||
|
|
70c20c39de | ||
|
|
8159a17dd7 | ||
|
|
9e8e77e1d8 | ||
|
|
9388fcf2f0 | ||
|
|
ee500908e3 | ||
|
|
147b247398 | ||
|
|
50a5918fd6 |
@@ -64,6 +64,10 @@ impl Budget {
|
||||
failed: None,
|
||||
})
|
||||
}
|
||||
/// Immutable exact policy snapshot for private deliberate-snapshot continuation.
|
||||
pub(super) fn limits(&self) -> Limits {
|
||||
self.limits
|
||||
}
|
||||
pub fn active(&self) -> Result<(), Error> {
|
||||
self.failed.clone().map_or(Ok(()), Err)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ use crate::language::{
|
||||
use crate::source_project::{ProjectLimits, SourceBundle, SourceDiagnostic, traversal::Traversal};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod carried;
|
||||
|
||||
struct ModelCounts {
|
||||
limits: ProjectLimits,
|
||||
graph: usize,
|
||||
@@ -44,6 +46,8 @@ pub(super) struct ParsedMixedSyntax<'s> {
|
||||
// Includes the conservative dropped value-end subtotal. The semantic
|
||||
// phase adds syntax entries to this once, not tokens/positions again.
|
||||
metadata: usize,
|
||||
// Kept through consumed resolution/indexing; fixes the live backing owner.
|
||||
carried: Option<carried::CarriedGuard<'s>>,
|
||||
}
|
||||
|
||||
struct FileSyntax<'s> {
|
||||
@@ -246,6 +250,15 @@ fn parse_with_profile(
|
||||
limits: ProjectLimits,
|
||||
profile: Limits,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
parse_impl(bundle, limits, profile, None)
|
||||
}
|
||||
|
||||
fn parse_impl<'s>(
|
||||
bundle: &'s SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
profile: Limits,
|
||||
carried: Option<carried::CarriedGuard<'s>>,
|
||||
) -> Result<ParsedMixedSyntax<'s>, Vec<SourceDiagnostic>> {
|
||||
let entry_source = bundle.files.get(&bundle.entry).map_or("", String::as_str);
|
||||
let at_entry = |error| located(&bundle.entry, entry_source, ordinary_error(error));
|
||||
let maximum = Limits::source8(ParseLimits {
|
||||
@@ -266,6 +279,9 @@ fn parse_with_profile(
|
||||
)));
|
||||
}
|
||||
let mut budget = Budget::new(profile).map_err(at_entry)?;
|
||||
if let Some(input) = &carried {
|
||||
input.check(&budget, 0, 0).map_err(at_entry)?;
|
||||
}
|
||||
let bytes = bundle
|
||||
.files
|
||||
.values()
|
||||
@@ -275,9 +291,13 @@ fn parse_with_profile(
|
||||
.index_bytes(bytes, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
let mut metadata = bundle.files.len();
|
||||
budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
if let Some(input) = &carried {
|
||||
input.check(&budget, 0, metadata).map_err(at_entry)?;
|
||||
} else {
|
||||
budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
// Tighter private byte/file-slot limits precede folded-path table and
|
||||
// traversal allocations. Public Traversal's own validation order is intact.
|
||||
let mut traversal = Traversal::new(bundle, limits)?;
|
||||
@@ -325,10 +345,16 @@ fn parse_with_profile(
|
||||
.and_then(|n| n.checked_add(projection_slots))
|
||||
.and_then(|n| n.checked_add(state.lexical_owners.len()))
|
||||
.ok_or_else(|| failure(super::error(Span::default(), "Metadata size overflow.")))?;
|
||||
state
|
||||
.budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(failure)?;
|
||||
if let Some(input) = &carried {
|
||||
input
|
||||
.check(state.budget, state.budget.usage.entries, metadata)
|
||||
.map_err(failure)?;
|
||||
} else {
|
||||
state
|
||||
.budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(failure)?;
|
||||
}
|
||||
let positions = language::edit_positions_from_tokens(
|
||||
source,
|
||||
&ordinary,
|
||||
@@ -367,12 +393,18 @@ fn parse_with_profile(
|
||||
}
|
||||
traversal.finish()?;
|
||||
budget.active().map_err(at_entry)?;
|
||||
if let Some(input) = &carried {
|
||||
input
|
||||
.check(&budget, budget.usage.entries, metadata)
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
Ok(ParsedMixedSyntax {
|
||||
bundle,
|
||||
limits,
|
||||
files: parsed.into_values().collect(),
|
||||
budget,
|
||||
metadata,
|
||||
carried,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Fixture-constructible carried phase authority, not an emitted-output result.
|
||||
//! Checks existing logical phase totals; does not meter every parser allocation.
|
||||
use super::*;
|
||||
use project::accounting::{Ledger, Slots};
|
||||
|
||||
struct SnapshotPolicy {
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
}
|
||||
|
||||
/// The allocation-owning bundle guard. No production constructor exists yet.
|
||||
struct RetainedSnapshot {
|
||||
bundle: SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
retained: Slots,
|
||||
}
|
||||
enum SnapshotBacking<'s> {
|
||||
Borrowed {
|
||||
bundle: &'s SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
},
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
/// Neither Clone nor Copy: fixes the bundle, policy and retained allocation owner.
|
||||
pub(super) struct CarriedInput<'s> {
|
||||
backing: SnapshotBacking<'s>,
|
||||
}
|
||||
enum GuardBacking<'s> {
|
||||
Borrowed,
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
/// Policy has transferred into Budget/ParsedMixedSyntax. Only backing ownership
|
||||
/// remains here, avoiding a duplicate inline policy in every later witness.
|
||||
pub(super) struct CarriedGuard<'s> {
|
||||
backing: GuardBacking<'s>,
|
||||
}
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
|
||||
match self.backing {
|
||||
SnapshotBacking::Borrowed { bundle, policy } => (
|
||||
bundle,
|
||||
policy,
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Borrowed,
|
||||
},
|
||||
),
|
||||
SnapshotBacking::Retained(owner) => (
|
||||
&owner.bundle,
|
||||
SnapshotPolicy {
|
||||
limits: owner.policy.limits,
|
||||
project_limits: owner.policy.project_limits,
|
||||
},
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Retained(owner),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CarriedGuard<'_> {
|
||||
pub(super) fn slots(&self) -> Slots {
|
||||
match &self.backing {
|
||||
GuardBacking::Borrowed => Slots::default(),
|
||||
GuardBacking::Retained(owner) => owner.retained,
|
||||
}
|
||||
}
|
||||
pub(super) fn check(
|
||||
&self,
|
||||
budget: &Budget,
|
||||
syntax: usize,
|
||||
metadata: usize,
|
||||
) -> Result<(), super::super::Error> {
|
||||
let at = Span::default();
|
||||
let total = self
|
||||
.slots()
|
||||
.add(Slots::entries(syntax), at)?
|
||||
.add(Slots::entries(metadata), at)?;
|
||||
// A local check, not another reservation retained in the syntax owner.
|
||||
Ledger::default().reserve(budget, total, at)
|
||||
}
|
||||
}
|
||||
|
||||
/// Input already binds the actual bundle; callers cannot supply a replacement.
|
||||
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let (bundle, policy, guard) = input.into_parts();
|
||||
parse_impl(bundle, policy.project_limits, policy.limits, Some(guard))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SnapshotPolicy {
|
||||
fn captured(syntax: &ParsedMixedSyntax<'_>) -> Self {
|
||||
Self {
|
||||
limits: syntax.budget.limits(),
|
||||
project_limits: syntax.limits,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl RetainedSnapshot {
|
||||
fn fixture(bundle: SourceBundle, policy: SnapshotPolicy) -> Result<Self, super::super::Error> {
|
||||
let at = Span::default();
|
||||
let mut text = bundle.entry.len();
|
||||
for (path, source) in &bundle.files {
|
||||
text = Slots::text(text)
|
||||
.add(Slots::text(path.len()), at)?
|
||||
.add(Slots::text(source.len()), at)?
|
||||
.text;
|
||||
}
|
||||
let retained = Slots {
|
||||
entries: bundle.files.len(),
|
||||
occurrences: 0,
|
||||
text,
|
||||
};
|
||||
Ok(Self {
|
||||
bundle,
|
||||
policy,
|
||||
retained,
|
||||
})
|
||||
}
|
||||
fn input(&self) -> CarriedInput<'_> {
|
||||
CarriedInput {
|
||||
backing: SnapshotBacking::Retained(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn borrowed(bundle: &'s SourceBundle, syntax: &ParsedMixedSyntax<'_>) -> Self {
|
||||
Self {
|
||||
backing: SnapshotBacking::Borrowed {
|
||||
bundle,
|
||||
policy: SnapshotPolicy::captured(syntax),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,662 @@
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Usage;
|
||||
fn doc(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
)
|
||||
}
|
||||
fn empty() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(""))]),
|
||||
}
|
||||
}
|
||||
fn chain() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), doc("include \"part.arcadia\";")),
|
||||
(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
),
|
||||
("last.arcadia".into(), "//é\r\n".into()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
// Fixture-only policies may deliberately be too tight to admit the target.
|
||||
// These are validated profiles, not a production old-owner transition.
|
||||
fn fixture_policy(limits: Limits, project_limits: ProjectLimits) -> SnapshotPolicy {
|
||||
SnapshotPolicy {
|
||||
limits: Budget::new(limits).unwrap().limits(),
|
||||
project_limits,
|
||||
}
|
||||
}
|
||||
fn owner(bundle: SourceBundle, limits: Limits) -> RetainedSnapshot {
|
||||
RetainedSnapshot::fixture(bundle, fixture_policy(limits, ProjectLimits::default())).unwrap()
|
||||
}
|
||||
fn parse_failure(result: Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>) -> SourceDiagnostic {
|
||||
let errors = match result {
|
||||
Ok(_) => panic!("unexpected syntax success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(errors.len(), 1);
|
||||
errors.into_iter().next().unwrap()
|
||||
}
|
||||
fn resolve_failure(syntax: ParsedMixedSyntax<'_>) -> SourceDiagnostic {
|
||||
let failure = match project::resolve(syntax) {
|
||||
Ok(_) => panic!("unexpected resolution success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
failure.diagnostics.into_iter().next().unwrap()
|
||||
}
|
||||
fn diagnostic(d: &SourceDiagnostic, path: &str, start: usize, end: usize, message: &str) {
|
||||
assert_eq!(d.path, path);
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert_eq!(d.diagnostic.message, message);
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
fn expected_project() -> crate::Project {
|
||||
crate::Project {
|
||||
schema_version: 8,
|
||||
id: Id::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
alias: "p".into(),
|
||||
name: "Project".into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn real_retained_bundles_keep_exact_guard_work_and_once_only_resolved_baseline() {
|
||||
for (bundle, c, u, m, live, entries, bytes) in [
|
||||
(
|
||||
empty(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 102,
|
||||
},
|
||||
Usage {
|
||||
bytes: 78,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0,
|
||||
},
|
||||
11,
|
||||
Slots {
|
||||
entries: 13,
|
||||
occurrences: 0,
|
||||
text: 110,
|
||||
},
|
||||
17,
|
||||
112,
|
||||
),
|
||||
(
|
||||
chain(),
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: 180,
|
||||
},
|
||||
Usage {
|
||||
bytes: 132,
|
||||
tokens: 31,
|
||||
entries: 3,
|
||||
references: 0,
|
||||
deepest_frame: 0,
|
||||
},
|
||||
19,
|
||||
Slots {
|
||||
entries: 25,
|
||||
occurrences: 0,
|
||||
text: 212,
|
||||
},
|
||||
29,
|
||||
214,
|
||||
),
|
||||
] {
|
||||
let owner = owner(bundle, profile(entries, bytes));
|
||||
assert_eq!(owner.retained, c);
|
||||
let original = owner.bundle.clone();
|
||||
let parsed = parse_carried(owner.input()).unwrap();
|
||||
assert_eq!(parsed.metadata, m);
|
||||
assert_eq!(parsed.budget.usage, u);
|
||||
assert_eq!(parsed.budget.numeric_units(), 0);
|
||||
assert!(std::ptr::eq(parsed.bundle, &owner.bundle));
|
||||
assert!(
|
||||
matches!(&parsed.carried.as_ref().unwrap().backing,GuardBacking::Retained(retained) if std::ptr::eq(*retained,&owner))
|
||||
);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
assert_eq!(resolved.project(), &expected_project());
|
||||
assert!(resolved.warnings().is_empty());
|
||||
let (syntax, stored) = resolved.phase_state();
|
||||
assert_eq!(stored, live);
|
||||
assert_eq!(syntax.budget.usage, u);
|
||||
assert_eq!(syntax.carried.as_ref().unwrap().slots(), c);
|
||||
assert_eq!(
|
||||
syntax.files.iter().map(|f| f.path).collect::<Vec<_>>(),
|
||||
original
|
||||
.files
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
drop(resolved);
|
||||
assert_eq!(owner.bundle, original);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_combined_syntax_caps_and_canonical_failure_files() {
|
||||
for (bundle, cap, path) in [(empty(), 13, "main.arcadia"), (chain(), 25, "part.arcadia")] {
|
||||
let pass = owner(bundle.clone(), profile(cap, 1024));
|
||||
drop(parse_carried(pass.input()).unwrap());
|
||||
let fail = owner(bundle, profile(cap - 1, 1024));
|
||||
let d = parse_failure(parse_carried(fail.input()));
|
||||
diagnostic(&d, path, 0, 1, "Source8 lowering entry limit exceeded.");
|
||||
assert_eq!((d.diagnostic.line, d.diagnostic.column), (1, 1));
|
||||
}
|
||||
let owner = owner(chain(), profile(20, 1024));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(owner.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn legacy_zero_timing_is_preserved_but_explicit_zero_carry_still_combines() {
|
||||
let bundle = empty();
|
||||
let original =
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), profile(11, 1024)).unwrap();
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(CarriedInput::borrowed(&bundle, &original))),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
diagnostic(
|
||||
&resolve_failure(original),
|
||||
"main.arcadia",
|
||||
0,
|
||||
0,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
let original =
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), profile(12, 1024)).unwrap();
|
||||
let carried = parse_carried(CarriedInput::borrowed(&bundle, &original)).unwrap();
|
||||
assert_eq!(carried.budget.usage, original.budget.usage);
|
||||
assert_eq!(carried.metadata, 11);
|
||||
}
|
||||
#[test]
|
||||
fn resolver_entry_and_catalog_text_caps_include_c_once_after_parser_success() {
|
||||
for (bundle, entries, bytes) in [(empty(), 17, 112), (chain(), 29, 214)] {
|
||||
for limits in [profile(entries - 1, bytes), profile(entries, bytes - 1)] {
|
||||
let held = owner(bundle.clone(), limits);
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
"main.arcadia",
|
||||
11,
|
||||
73,
|
||||
if limits.entries == entries - 1 {
|
||||
"Source8 lowering entry limit exceeded."
|
||||
} else {
|
||||
"Source8 index/output byte limit exceeded."
|
||||
},
|
||||
);
|
||||
drop(
|
||||
project::resolve(
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), limits).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn syntax_text_accounting_is_after_decoding_and_has_precise_original_location() {
|
||||
for (bundle, bytes, path, start, end) in [
|
||||
(empty(), 109, "main.arcadia", 11, 73),
|
||||
(chain(), 211, "part.arcadia", 0, 23),
|
||||
] {
|
||||
let held = owner(bundle, profile(100, bytes));
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
assert_eq!(
|
||||
parsed
|
||||
.files
|
||||
.iter()
|
||||
.find_map(|f| f.ordinary.header.as_ref())
|
||||
.unwrap()
|
||||
.project
|
||||
.name,
|
||||
"Project"
|
||||
);
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
path,
|
||||
start,
|
||||
end,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn initial_c_and_file_slots_are_checked_before_traversal() {
|
||||
for (bundle, cap) in [(empty(), 1), (chain(), 5)] {
|
||||
let held = owner(bundle, profile(cap, 1024));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
}
|
||||
for (cap, code) in [(1, "invalid_source"), (2, "project_limit")] {
|
||||
let held = RetainedSnapshot::fixture(
|
||||
empty(),
|
||||
fixture_policy(
|
||||
profile(cap, 1024),
|
||||
ProjectLimits {
|
||||
max_files: 0,
|
||||
..ProjectLimits::default()
|
||||
},
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parse_failure(parse_carried(held.input())).diagnostic.code,
|
||||
code
|
||||
);
|
||||
}
|
||||
let held = owner(empty(), profile(100, 101));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
let held = owner(empty(), profile(100, 102));
|
||||
drop(parse_carried(held.input()).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn explicit_fault_injections_cover_occurrence_caps_and_unreachable_overflows() {
|
||||
let mut held = owner(empty(), profile(100, 1024));
|
||||
let maximum = held.policy.limits.entries + held.policy.limits.references;
|
||||
// Not real bundle ownership: only a fault-injected dimension check.
|
||||
held.retained.occurrences = maximum;
|
||||
drop(parse_carried(held.input()).unwrap());
|
||||
held.retained.occurrences = maximum + 1;
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering occurrence limit exceeded.",
|
||||
);
|
||||
held.retained = Slots {
|
||||
entries: usize::MAX,
|
||||
occurrences: 0,
|
||||
text: 0,
|
||||
};
|
||||
let budget = Budget::new(profile(100, 1024)).unwrap();
|
||||
// Valid hard limits reject C first: direct injection reaches arithmetic.
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.into_parts()
|
||||
.2
|
||||
.check(&budget, 0, 1)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
held.retained = Slots::default();
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.into_parts()
|
||||
.2
|
||||
.check(&budget, usize::MAX, 1)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
assert_eq!(
|
||||
Slots::text(usize::MAX)
|
||||
.add(Slots::text(1), Span::default())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
let mut ledger = Ledger {
|
||||
live: Slots::entries(usize::MAX),
|
||||
};
|
||||
assert_eq!(
|
||||
ledger
|
||||
.reserve(&budget, Slots::entries(1), Span::default())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
}
|
||||
|
||||
fn same_policy(syntax: &ParsedMixedSyntax<'_>, policy: &SnapshotPolicy) {
|
||||
let a = syntax.budget.limits();
|
||||
let b = policy.limits;
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[b.bytes, b.tokens, b.entries, b.references, b.frames]
|
||||
);
|
||||
assert_eq!(a.numeric_units, b.numeric_units);
|
||||
let a = syntax.limits;
|
||||
let b = policy.project_limits;
|
||||
assert_eq!(
|
||||
[
|
||||
a.max_files,
|
||||
a.max_include_depth,
|
||||
a.max_include_edges,
|
||||
a.model.max_source_bytes,
|
||||
a.model.max_graph_objects,
|
||||
a.model.max_records,
|
||||
a.model.max_step_references
|
||||
],
|
||||
[
|
||||
b.max_files,
|
||||
b.max_include_depth,
|
||||
b.max_include_edges,
|
||||
b.model.max_source_bytes,
|
||||
b.model.max_graph_objects,
|
||||
b.model.max_records,
|
||||
b.model.max_step_references
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn exact_policy_capture_preserves_every_field_and_starts_fresh_snapshot_work() {
|
||||
let base = empty();
|
||||
let next = chain();
|
||||
let limits = Limits {
|
||||
bytes: 4096,
|
||||
tokens: 2048,
|
||||
entries: 1024,
|
||||
references: 128,
|
||||
frames: 64,
|
||||
numeric_units: 4096,
|
||||
};
|
||||
let caller = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 5,
|
||||
max_include_edges: 9,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 8192,
|
||||
max_graph_objects: 10,
|
||||
max_records: 30,
|
||||
max_step_references: 256,
|
||||
},
|
||||
};
|
||||
let seed = parse_with_profile(&base, caller, limits).unwrap();
|
||||
assert_eq!(seed.budget.usage.bytes, 78);
|
||||
let input = CarriedInput::borrowed(&next, &seed);
|
||||
let captured = SnapshotPolicy::captured(&seed);
|
||||
drop(seed);
|
||||
let parsed = parse_carried(input).unwrap();
|
||||
same_policy(&parsed, &captured);
|
||||
assert_eq!(
|
||||
parsed.budget.usage,
|
||||
Usage {
|
||||
bytes: 132,
|
||||
tokens: 31,
|
||||
entries: 3,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(parsed.budget.numeric_units(), 0);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
same_policy(resolved.phase_state().0, &captured);
|
||||
}
|
||||
#[test]
|
||||
fn inherited_byte_token_and_traversal_limits_reach_the_real_parser() {
|
||||
let seed_bundle = empty();
|
||||
let bundle = chain();
|
||||
for (bytes, tokens, success) in [(132, 31, true), (131, 31, false), (132, 30, false)] {
|
||||
let limits = Limits {
|
||||
tokens,
|
||||
..profile(100, bytes)
|
||||
};
|
||||
let seed = parse_with_profile(&seed_bundle, ProjectLimits::default(), limits).unwrap();
|
||||
let result = parse_carried(CarriedInput::borrowed(&bundle, &seed));
|
||||
if success {
|
||||
drop(project::resolve(result.unwrap()).unwrap());
|
||||
} else {
|
||||
let d = parse_failure(result);
|
||||
if bytes == 131 {
|
||||
diagnostic(
|
||||
&d,
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
} else {
|
||||
diagnostic(&d, "last.arcadia", 5, 6, "Source8 tokens limit exceeded.");
|
||||
assert_eq!((d.diagnostic.line, d.diagnostic.column), (1, 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (files, depth, edges, success) in [
|
||||
(3, 2, 2, true),
|
||||
(2, 2, 2, false),
|
||||
(3, 1, 2, false),
|
||||
(3, 2, 1, false),
|
||||
] {
|
||||
let caller = ProjectLimits {
|
||||
max_files: files,
|
||||
max_include_depth: depth,
|
||||
max_include_edges: edges,
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let seed = parse_with_profile(&seed_bundle, caller, profile(100, 1024)).unwrap();
|
||||
let result = parse_carried(CarriedInput::borrowed(&bundle, &seed));
|
||||
if success {
|
||||
drop(project::resolve(result.unwrap()).unwrap());
|
||||
} else {
|
||||
let d = parse_failure(result);
|
||||
assert_eq!(d.diagnostic.code, "project_limit");
|
||||
if files == 2 {
|
||||
assert_eq!(d.path, "main.arcadia");
|
||||
} else {
|
||||
assert_eq!(d.path, "part.arcadia");
|
||||
assert_eq!(
|
||||
d.diagnostic.span,
|
||||
Span {
|
||||
start: 0,
|
||||
end: if edges == 1 { 1 } else { 23 }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn typed(expression: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
doc(&format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000002\" {{ input x id \"00000000-0000-0000-0000-000000000003\" : Number; result Number; body {expression}; }}"
|
||||
)),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn captured_numeric_reference_frame_and_model_policies_are_behavioral_not_just_copied() {
|
||||
let base = empty();
|
||||
for (target, limits, expected) in [
|
||||
(
|
||||
typed("1"),
|
||||
Limits {
|
||||
numeric_units: 72,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"NumericUnits",
|
||||
),
|
||||
(
|
||||
typed("$x"),
|
||||
Limits {
|
||||
references: 0,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"Source8 references limit exceeded.",
|
||||
),
|
||||
(
|
||||
typed("abs(1)"),
|
||||
Limits {
|
||||
frames: 0,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"Source8 syntax frames limit exceeded.",
|
||||
),
|
||||
] {
|
||||
let seed = parse_with_profile(&base, ProjectLimits::default(), limits).unwrap();
|
||||
let d = parse_failure(parse_carried(CarriedInput::borrowed(&target, &seed)));
|
||||
assert!(d.diagnostic.message.contains(expected), "{:?}", d);
|
||||
}
|
||||
for expression in ["1", "$x", "abs(1)"] {
|
||||
let target = typed(expression);
|
||||
let seed =
|
||||
parse_with_profile(&base, ProjectLimits::default(), profile(1000, 4096)).unwrap();
|
||||
drop(
|
||||
project::resolve(parse_carried(CarriedInput::borrowed(&target, &seed)).unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
// Nonzero old numeric work must not leak into the second snapshot.
|
||||
let one = typed("1");
|
||||
let limits = Limits {
|
||||
numeric_units: 73,
|
||||
..profile(1000, 4096)
|
||||
};
|
||||
let seed = parse_with_profile(&one, ProjectLimits::default(), limits).unwrap();
|
||||
assert_eq!(seed.budget.numeric_units(), 73);
|
||||
let parsed = parse_carried(CarriedInput::borrowed(&one, &seed)).unwrap();
|
||||
assert_eq!(parsed.budget.numeric_units(), 73);
|
||||
assert_eq!(parsed.budget.usage, seed.budget.usage);
|
||||
for (graph, records, steps) in [(0, 10, 10), (10, 1, 10), (10, 10, 0)] {
|
||||
let caller = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_graph_objects: graph,
|
||||
max_records: records,
|
||||
max_step_references: steps,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let seed = parse_with_profile(
|
||||
&base,
|
||||
caller,
|
||||
Limits::source8(ParseLimits {
|
||||
max_step_references: steps,
|
||||
..ParseLimits::UNLIMITED
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let target = if steps == 0 { typed("$x") } else { typed("1") };
|
||||
let errors = parse_carried(CarriedInput::borrowed(&target, &seed));
|
||||
if steps == 0 {
|
||||
assert!(
|
||||
parse_failure(errors)
|
||||
.diagnostic
|
||||
.message
|
||||
.contains("references")
|
||||
);
|
||||
} else {
|
||||
assert!(parse_failure(errors).diagnostic.message.contains("limit"));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn zero_carried_mixed_oa_sa_unicode_results_and_index_ownership_match_legacy() {
|
||||
for (activity, property, layer) in [
|
||||
("oa_activity", "oa_property", "Oa"),
|
||||
("sa_function", "sa_property", "Sa"),
|
||||
] {
|
||||
let body = format!(
|
||||
"node n id \"00000000-0000-0000-0000-000000000004\" {{ diagram @d; element @v; }}\n{property} v \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @w; type Number; base 1; }}\n{activity} w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram d \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ layer {layer}; }}"
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
format!("// 雨\r\n{}", doc(&body).replace('\n', "\r\n")),
|
||||
)]),
|
||||
};
|
||||
let legacy = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
let carried = parse_carried(CarriedInput::borrowed(&bundle, &legacy)).unwrap();
|
||||
assert_eq!(legacy.budget.usage, carried.budget.usage);
|
||||
assert_eq!(
|
||||
legacy.budget.numeric_units(),
|
||||
carried.budget.numeric_units()
|
||||
);
|
||||
assert_eq!(legacy.metadata, carried.metadata);
|
||||
let legacy = project::resolve(legacy).unwrap();
|
||||
let carried = project::resolve(carried).unwrap();
|
||||
assert_eq!(legacy.project(), carried.project());
|
||||
assert_eq!(legacy.warnings(), carried.warnings());
|
||||
assert_eq!(legacy.phase_state().1, carried.phase_state().1);
|
||||
assert!(carried.phase_state().0.carried.is_some());
|
||||
let legacy = legacy.index().unwrap().index_ordinary().unwrap();
|
||||
let carried = carried.index().unwrap().index_ordinary().unwrap();
|
||||
assert_eq!(legacy.names(), carried.names());
|
||||
assert_eq!(legacy.typed_occurrences(), carried.typed_occurrences());
|
||||
assert_eq!(
|
||||
legacy.ordinary_occurrences(),
|
||||
carried.ordinary_occurrences()
|
||||
);
|
||||
assert!(std::ptr::eq(carried.bundle(), &bundle));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_position_key_copy_is_counted_at_existing_postparse_text_phase() {
|
||||
let source = doc(
|
||||
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"é\"; }",
|
||||
);
|
||||
let start = source.find("description \"é\";").unwrap();
|
||||
let end = start + "description \"é\";".len();
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), source)]),
|
||||
};
|
||||
let retained_text = bundle.files["main.arcadia"].len() + 24;
|
||||
// Existing text metric: project8 + alias/name2 + field-span key11 +
|
||||
// AST field key11 + decoded string2 + projected field key11 =45.
|
||||
// The final11-byte key is copied before resolve accounts this phase.
|
||||
let held = owner(bundle, profile(1000, retained_text + 44));
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
assert_eq!(parsed.files[0].positions.len(), 1);
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
"main.arcadia",
|
||||
start,
|
||||
end,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
|
||||
use std::collections::BTreeMap;
|
||||
pub(in crate::language::analysis_source) mod accounting;
|
||||
use accounting::{Ledger, Slots, add};
|
||||
pub(in crate::language::analysis_source) mod ordinary_index;
|
||||
|
||||
/// Read-only authority: every component originates in the consumed syntax.
|
||||
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
|
||||
@@ -27,6 +28,10 @@ impl ResolvedMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn phase_state(&self) -> (&ParsedMixedSyntax<'_>, Slots) {
|
||||
(&self.syntax, self.storage.live)
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
@@ -49,20 +54,7 @@ impl<'s> ResolvedMixedProject<'s> {
|
||||
};
|
||||
let data =
|
||||
super::super::index::mixed_data(input, &mut self.syntax.budget, &mut self.storage)
|
||||
.map_err(|mut failure| {
|
||||
// An invariant without a canonical file has no defensible source
|
||||
// offset. Use the entry start explicitly, never a foreign span.
|
||||
failure.file = failure.file.filter(|&f| f < self.syntax.files.len());
|
||||
if failure.file.is_none_or(|file| {
|
||||
self.syntax.files[file]
|
||||
.source
|
||||
.get(failure.error.span.start..failure.error.span.end)
|
||||
.is_none()
|
||||
}) {
|
||||
failure.error.span = Span::default();
|
||||
}
|
||||
Failure::typed(&self.syntax, failure.file, failure.error)
|
||||
})?;
|
||||
.map_err(|failure| Failure::index(&self.syntax, failure))?;
|
||||
Ok(IndexedMixedProject {
|
||||
resolved: self,
|
||||
data,
|
||||
@@ -136,6 +128,20 @@ pub(in crate::language::analysis_source) struct Failure {
|
||||
pub diagnostics: Vec<SourceDiagnostic>,
|
||||
}
|
||||
impl Failure {
|
||||
/// Canonical attribution when justified; otherwise a safe explicit start fallback.
|
||||
fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self {
|
||||
failure.file = failure.file.filter(|&f| f < syntax.files.len());
|
||||
if failure.file.is_none_or(|file| {
|
||||
syntax.files[file]
|
||||
.source
|
||||
.get(failure.error.span.start..failure.error.span.end)
|
||||
.is_none()
|
||||
}) {
|
||||
failure.error.span = Span::default();
|
||||
}
|
||||
Self::typed(syntax, failure.file, failure.error)
|
||||
}
|
||||
|
||||
// Rejection transport is not retained in a successful witness. In particular
|
||||
// a storage refusal needs a small diagnostic even when its allowance is zero.
|
||||
fn typed(syntax: &ParsedMixedSyntax<'_>, file: Option<usize>, e: TypedError) -> Self {
|
||||
@@ -436,7 +442,12 @@ fn active_references(
|
||||
pub(in crate::language::analysis_source) fn resolve(
|
||||
mut syntax: ParsedMixedSyntax<'_>,
|
||||
) -> Result<ResolvedMixedProject<'_>, Failure> {
|
||||
let mut storage = Ledger::default();
|
||||
let mut storage = Ledger {
|
||||
live: syntax
|
||||
.carried
|
||||
.as_ref()
|
||||
.map_or_else(Slots::default, |input| input.slots()),
|
||||
};
|
||||
let lower_storage = accounting::syntax(&syntax, &mut storage)?;
|
||||
let catalog = catalog(&syntax, &mut storage)?;
|
||||
let entry = entry(&syntax);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
//! Complete authored-name ownership from the sealed ordinary position projection.
|
||||
//! Physical keys belong only to this immutable snapshot. This is not edit authority.
|
||||
use super::super::super::{index, lex};
|
||||
use super::*;
|
||||
use crate::language::{EditField, EditKind, EditValue};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Canonical file key and the whole alias Word's half-open byte range.
|
||||
pub(in crate::language::analysis_source) type OrdinaryKey = (usize, usize, usize);
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(in crate::language::analysis_source) enum OrdinaryRole {
|
||||
Header,
|
||||
Reference,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(in crate::language::analysis_source) struct OrdinaryOccurrence {
|
||||
pub owner: Id,
|
||||
pub target: Id,
|
||||
pub role: OrdinaryRole,
|
||||
/// Alias token for Header; marker, intervening trivia and alias for Reference.
|
||||
pub extent: Span,
|
||||
}
|
||||
|
||||
/// Owns both occurrence families and their complete original admission witness.
|
||||
pub(in crate::language::analysis_source) struct AuthoredMixedProject<'s> {
|
||||
indexed: IndexedMixedProject<'s>,
|
||||
ordinary: BTreeMap<OrdinaryKey, OrdinaryOccurrence>,
|
||||
}
|
||||
impl<'s> IndexedMixedProject<'s> {
|
||||
pub fn index_ordinary(mut self) -> Result<AuthoredMixedProject<'s>, Failure> {
|
||||
let ordinary = build(
|
||||
&self.resolved.syntax,
|
||||
&self.data,
|
||||
&mut self.resolved.storage,
|
||||
)
|
||||
.map_err(|failure| Failure::index(&self.resolved.syntax, failure))?;
|
||||
Ok(AuthoredMixedProject {
|
||||
indexed: self,
|
||||
ordinary,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl AuthoredMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
self.indexed.project()
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
self.indexed.warnings()
|
||||
}
|
||||
pub fn bundle(&self) -> &crate::source_project::SourceBundle {
|
||||
self.indexed.bundle()
|
||||
}
|
||||
pub fn files(&self) -> impl Iterator<Item = (usize, &str, &str)> {
|
||||
self.indexed.files()
|
||||
}
|
||||
pub fn names(&self) -> &BTreeMap<lower::Target, String> {
|
||||
self.indexed.names()
|
||||
}
|
||||
pub fn typed_occurrences(&self) -> &BTreeMap<index::OccurrenceKey, index::Location> {
|
||||
self.indexed.occurrences()
|
||||
}
|
||||
pub fn expressions(&self) -> &BTreeMap<index::ExpressionKey, index::Location> {
|
||||
self.indexed.expressions()
|
||||
}
|
||||
pub fn ordinary_occurrences(&self) -> &BTreeMap<OrdinaryKey, OrdinaryOccurrence> {
|
||||
&self.ordinary
|
||||
}
|
||||
}
|
||||
|
||||
struct Builder<'a, 'b> {
|
||||
budget: &'a Budget,
|
||||
storage: &'b mut Ledger,
|
||||
globals: BTreeMap<&'a str, (Id, bool)>,
|
||||
typed: BTreeSet<OrdinaryKey>,
|
||||
ordinary: BTreeMap<OrdinaryKey, OrdinaryOccurrence>,
|
||||
}
|
||||
fn build(
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
data: &index::IndexData,
|
||||
storage: &mut Ledger,
|
||||
) -> Result<BTreeMap<OrdinaryKey, OrdinaryOccurrence>, lower::Failure> {
|
||||
let mut file = None;
|
||||
let mut b = Builder {
|
||||
budget: &syntax.budget,
|
||||
storage,
|
||||
globals: BTreeMap::new(),
|
||||
typed: BTreeSet::new(),
|
||||
ordinary: BTreeMap::new(),
|
||||
};
|
||||
let result = (|| {
|
||||
b.budget.active()?;
|
||||
for (target, name) in &data.names {
|
||||
if let lower::Target::Global(id) = target {
|
||||
b.reserve(Span::default())?;
|
||||
if b.globals.insert(name, (*id, false)).is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Duplicate ordinary global spelling.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, at) in &data.occurrences {
|
||||
file = Some(at.file);
|
||||
let unit = syntax
|
||||
.files
|
||||
.get(at.file)
|
||||
.ok_or_else(|| error(at.span, "Ordinary index has no canonical file."))?;
|
||||
source(unit, at.span)?;
|
||||
b.reserve(at.span)?;
|
||||
if !b.typed.insert((at.file, at.span.start, at.span.end)) {
|
||||
return Err(error(at.span, "Duplicate typed physical name token."));
|
||||
}
|
||||
if key.role == index::TokenRole::Declaration {
|
||||
if key.target != lower::Target::Global(key.owner) {
|
||||
return Err(error(at.span, "Typed global header has a different owner."));
|
||||
}
|
||||
b.header(key.owner, source(unit, at.span)?, at.span)?;
|
||||
}
|
||||
}
|
||||
for (number, unit) in syntax.files.iter().enumerate() {
|
||||
file = Some(number);
|
||||
if let Some(header) = &unit.ordinary.header {
|
||||
if unit.path != syntax.bundle.entry {
|
||||
return Err(error(
|
||||
header.span,
|
||||
"Project header is outside the canonical entry.",
|
||||
));
|
||||
}
|
||||
source(unit, header.span)?;
|
||||
let start = token_at(unit, header.span.start, header.span)?;
|
||||
let keyword = unit.tokens[start];
|
||||
if keyword.class != lex::Class::Word || source(unit, keyword.span)? != "project" {
|
||||
return Err(error(
|
||||
header.span,
|
||||
"Project header lacks its original project token.",
|
||||
));
|
||||
}
|
||||
let alias = *unit
|
||||
.tokens
|
||||
.get(start + 1)
|
||||
.ok_or_else(|| error(header.span, "Project header lacks an alias token."))?;
|
||||
contains(header.span, alias.span)?;
|
||||
word(unit, alias.span, &header.project.alias)?;
|
||||
b.header(header.project.id, &header.project.alias, alias.span)?;
|
||||
b.insert(
|
||||
number,
|
||||
alias.span,
|
||||
OrdinaryOccurrence {
|
||||
owner: header.project.id,
|
||||
target: header.project.id,
|
||||
role: OrdinaryRole::Header,
|
||||
extent: alias.span,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
if unit.ordinary.declarations.len() != unit.positions.len() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Ordinary declaration position count differs from syntax.",
|
||||
));
|
||||
}
|
||||
for (declaration, (owner, position)) in
|
||||
unit.ordinary.declarations.iter().zip(&unit.positions)
|
||||
{
|
||||
if *owner != declaration.id
|
||||
|| position.span != declaration.span
|
||||
|| position.body != declaration.body
|
||||
|| position.lexical_owner != declaration.lexical_owner
|
||||
{
|
||||
return Err(error(
|
||||
position.span,
|
||||
"Ordinary position differs from its declaration owner.",
|
||||
));
|
||||
}
|
||||
source(unit, declaration.span)?;
|
||||
source(unit, declaration.body)?;
|
||||
contains(declaration.span, declaration.body)?;
|
||||
let alias = *position.header.get("alias").ok_or_else(|| {
|
||||
error(
|
||||
position.span,
|
||||
"Ordinary declaration lacks an alias position.",
|
||||
)
|
||||
})?;
|
||||
let start = token_at(unit, declaration.span.start, declaration.span)?;
|
||||
if unit.tokens.get(start + 1).map(|t| t.span) != Some(alias)
|
||||
|| alias.end > declaration.body.start
|
||||
{
|
||||
return Err(error(
|
||||
alias,
|
||||
"Ordinary alias is not its declaration header token.",
|
||||
));
|
||||
}
|
||||
contains(declaration.span, alias)?;
|
||||
word(unit, alias, &declaration.alias)?;
|
||||
b.header(*owner, &declaration.alias, alias)?;
|
||||
b.insert(
|
||||
number,
|
||||
alias,
|
||||
OrdinaryOccurrence {
|
||||
owner: *owner,
|
||||
target: *owner,
|
||||
role: OrdinaryRole::Header,
|
||||
extent: alias,
|
||||
},
|
||||
)?;
|
||||
// The synthetic root includes braces; actual fields lie in the interior.
|
||||
let root_start = declaration.body.start.checked_sub(1).ok_or_else(|| {
|
||||
error(position.fields.span, "Invalid ordinary body boundary.")
|
||||
})?;
|
||||
let root_end = declaration.body.end.checked_add(1).ok_or_else(|| {
|
||||
error(position.fields.span, "Invalid ordinary body boundary.")
|
||||
})?;
|
||||
if position.fields.span
|
||||
!= (Span {
|
||||
start: root_start,
|
||||
end: root_end,
|
||||
})
|
||||
{
|
||||
return Err(error(
|
||||
position.fields.span,
|
||||
"Ordinary synthetic root differs from its body braces.",
|
||||
));
|
||||
}
|
||||
source(unit, position.fields.span)?;
|
||||
contains(declaration.span, position.fields.span)?;
|
||||
let EditKind::Record(fields) = &position.fields.kind else {
|
||||
return Err(error(
|
||||
position.fields.span,
|
||||
"Ordinary synthetic root must be a record.",
|
||||
));
|
||||
};
|
||||
b.fields(
|
||||
number,
|
||||
unit,
|
||||
*owner,
|
||||
declaration.span,
|
||||
declaration.body,
|
||||
fields,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
file = None;
|
||||
if b.globals.values().any(|(_, seen)| !seen) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Global name is missing its authored declaration header.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
result.map_err(|error| lower::Failure { file, error })?;
|
||||
let globals = b.globals.len();
|
||||
let typed = b.typed.len();
|
||||
drop(b.globals);
|
||||
drop(b.typed);
|
||||
b.storage.release(Slots::entries(
|
||||
add(globals, typed, Span::default())
|
||||
.map_err(|error| lower::Failure { file: None, error })?,
|
||||
));
|
||||
Ok(b.ordinary)
|
||||
}
|
||||
impl Builder<'_, '_> {
|
||||
fn reserve(&mut self, at: Span) -> Result<(), TypedError> {
|
||||
self.storage.reserve(self.budget, Slots::entries(1), at)
|
||||
}
|
||||
fn header(&mut self, id: Id, alias: &str, at: Span) -> Result<(), TypedError> {
|
||||
let Some((target, seen)) = self.globals.get_mut(alias) else {
|
||||
return Err(error(at, "Ordinary header has no global target."));
|
||||
};
|
||||
if *target != id {
|
||||
return Err(error(
|
||||
at,
|
||||
"Ordinary header differs from its global identity.",
|
||||
));
|
||||
}
|
||||
if *seen {
|
||||
return Err(error(
|
||||
at,
|
||||
"Global declaration header appears more than once.",
|
||||
));
|
||||
}
|
||||
*seen = true;
|
||||
Ok(())
|
||||
}
|
||||
fn insert(
|
||||
&mut self,
|
||||
file: usize,
|
||||
alias: Span,
|
||||
occurrence: OrdinaryOccurrence,
|
||||
) -> Result<(), TypedError> {
|
||||
let key = (file, alias.start, alias.end);
|
||||
if self.typed.contains(&key) || self.ordinary.contains_key(&key) {
|
||||
return Err(error(
|
||||
alias,
|
||||
"Duplicate ordinary or typed physical name token.",
|
||||
));
|
||||
}
|
||||
self.reserve(alias)?;
|
||||
self.ordinary.insert(key, occurrence);
|
||||
Ok(())
|
||||
}
|
||||
fn fields(
|
||||
&mut self,
|
||||
file: usize,
|
||||
unit: &FileSyntax<'_>,
|
||||
owner: Id,
|
||||
declaration: Span,
|
||||
body: Span,
|
||||
fields: &BTreeMap<String, EditField>,
|
||||
) -> Result<(), TypedError> {
|
||||
let mut work = Vec::new();
|
||||
for field in fields.values() {
|
||||
self.push_field(unit, &mut work, field, body)?;
|
||||
}
|
||||
while let Some((value, parent)) = work.pop() {
|
||||
self.storage.release(Slots::entries(1));
|
||||
source(unit, value.span)?;
|
||||
contains(parent, value.span)?;
|
||||
contains(declaration, value.span)?;
|
||||
match &value.kind {
|
||||
EditKind::Reference(alias) => {
|
||||
let start = token_at(unit, value.span.start, value.span)?;
|
||||
let marker = unit.tokens[start];
|
||||
if marker.class != lex::Class::Punctuation || source(unit, marker.span)? != "@"
|
||||
{
|
||||
return Err(error(
|
||||
value.span,
|
||||
"Ordinary reference lacks its original marker token.",
|
||||
));
|
||||
}
|
||||
let name = *unit.tokens.get(start + 1).ok_or_else(|| {
|
||||
error(value.span, "Ordinary reference lacks its alias token.")
|
||||
})?;
|
||||
if name.span.end != value.span.end {
|
||||
return Err(error(
|
||||
value.span,
|
||||
"Ordinary reference extent differs from its complete alias token.",
|
||||
));
|
||||
}
|
||||
word(unit, name.span, alias)?;
|
||||
let target = self
|
||||
.globals
|
||||
.get(alias.as_str())
|
||||
.ok_or_else(|| {
|
||||
error(name.span, "Ordinary reference has no global target.")
|
||||
})?
|
||||
.0;
|
||||
self.insert(
|
||||
file,
|
||||
name.span,
|
||||
OrdinaryOccurrence {
|
||||
owner,
|
||||
target,
|
||||
role: OrdinaryRole::Reference,
|
||||
extent: value.span,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
EditKind::Record(fields) => {
|
||||
for field in fields.values() {
|
||||
self.push_field(unit, &mut work, field, value.span)?;
|
||||
}
|
||||
}
|
||||
EditKind::List(items) => {
|
||||
for item in items {
|
||||
self.reserve(item.span)?;
|
||||
work.push((item, value.span));
|
||||
}
|
||||
}
|
||||
EditKind::Atom | EditKind::HistoricalUuid(_) => {}
|
||||
}
|
||||
}
|
||||
drop(work);
|
||||
Ok(())
|
||||
}
|
||||
fn push_field<'a>(
|
||||
&mut self,
|
||||
unit: &FileSyntax<'_>,
|
||||
work: &mut Vec<(&'a EditValue, Span)>,
|
||||
field: &'a EditField,
|
||||
parent: Span,
|
||||
) -> Result<(), TypedError> {
|
||||
source(unit, field.span)?;
|
||||
contains(parent, field.span)?;
|
||||
contains(field.span, field.value.span)?;
|
||||
self.reserve(field.value.span)?;
|
||||
work.push((&field.value, field.span));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn source<'a>(unit: &'a FileSyntax<'_>, span: Span) -> Result<&'a str, TypedError> {
|
||||
unit.source
|
||||
.get(span.start..span.end)
|
||||
.ok_or_else(|| error(span, "Ordinary span is outside its original UTF-8 file."))
|
||||
}
|
||||
fn contains(parent: Span, child: Span) -> Result<(), TypedError> {
|
||||
if child.start < parent.start || child.end > parent.end || child.start > child.end {
|
||||
return Err(error(child, "Ordinary span crosses its containing syntax."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn token_at(unit: &FileSyntax<'_>, start: usize, at: Span) -> Result<usize, TypedError> {
|
||||
unit.tokens
|
||||
.binary_search_by_key(&start, |t| t.span.start)
|
||||
.map_err(|_| {
|
||||
error(
|
||||
at,
|
||||
"Ordinary occurrence does not start at an original token.",
|
||||
)
|
||||
})
|
||||
}
|
||||
fn word(unit: &FileSyntax<'_>, at: Span, expected: &str) -> Result<(), TypedError> {
|
||||
let token = unit.tokens[token_at(unit, at.start, at)?];
|
||||
if token.class != lex::Class::Word || token.span != at {
|
||||
return Err(error(
|
||||
at,
|
||||
"Ordinary alias is not an original whole Word token.",
|
||||
));
|
||||
}
|
||||
if source(unit, at)? != expected {
|
||||
return Err(error(
|
||||
at,
|
||||
"Ordinary alias differs from its retained spelling.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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,694 @@
|
||||
use super::super::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
use super::*;
|
||||
use crate::Layer;
|
||||
use crate::source_project::{ProjectLimits, SourceBundle};
|
||||
|
||||
fn indexed(bundle: &SourceBundle) -> IndexedMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap()
|
||||
}
|
||||
fn span(start: usize, end: usize) -> Span {
|
||||
Span { start, end }
|
||||
}
|
||||
fn record(
|
||||
owner: usize,
|
||||
target: usize,
|
||||
role: OrdinaryRole,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> OrdinaryOccurrence {
|
||||
OrdinaryOccurrence {
|
||||
owner: id(owner),
|
||||
target: id(target),
|
||||
role,
|
||||
extent: span(start, end),
|
||||
}
|
||||
}
|
||||
fn fail(indexed: IndexedMixedProject<'_>, message: &str, path: &str) -> SourceDiagnostic {
|
||||
let failure = match indexed.index_ordinary() {
|
||||
Ok(_) => panic!("damaged ordinary index accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
let diagnostic = failure.diagnostics.into_iter().next().unwrap();
|
||||
assert_eq!(diagnostic.path, path);
|
||||
assert_eq!(diagnostic.diagnostic.message, message);
|
||||
diagnostic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entire_rich_oa_sa_and_unicode_crlf_maps_preserve_complete_authority() {
|
||||
use OrdinaryRole::{Header as H, Reference as R};
|
||||
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 indexed = indexed(&bundle);
|
||||
let names = indexed.names().clone();
|
||||
let typed = indexed.occurrences().clone();
|
||||
let expressions = indexed.expressions().clone();
|
||||
let warnings = indexed.warnings().to_vec();
|
||||
let limits = indexed.resolved.static_limits;
|
||||
let static_usage = indexed.resolved.static_usage;
|
||||
let usage = indexed.resolved.syntax.budget.usage;
|
||||
let numeric = indexed.resolved.syntax.budget.numeric_units();
|
||||
let before = indexed.resolved.storage.live;
|
||||
let complete = indexed.index_ordinary().unwrap();
|
||||
let endpoints = if crlf {
|
||||
[28, 133, 188, 200, 363, 426, 505]
|
||||
} else {
|
||||
[19, 122, 177, 189, 350, 412, 490]
|
||||
};
|
||||
let mut expected = BTreeMap::new();
|
||||
for (at, owner, target, role) in [
|
||||
(endpoints[0], 1, 1, H),
|
||||
(endpoints[1], 4, 4, H),
|
||||
(endpoints[2], 4, 5, R),
|
||||
(endpoints[3], 4, 3, R),
|
||||
(endpoints[4], 2, 2, H),
|
||||
(endpoints[5], 5, 5, H),
|
||||
] {
|
||||
expected.insert(
|
||||
(2, at, at + 1),
|
||||
record(
|
||||
owner,
|
||||
target,
|
||||
role,
|
||||
if role == R { at - 1 } else { at },
|
||||
at + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
let at = endpoints[6];
|
||||
expected.insert((2, at, at + 1), record(6, 6, H, at, at + 1));
|
||||
}
|
||||
assert_eq!(complete.ordinary_occurrences(), &expected);
|
||||
assert_eq!(complete.project(), &rich_model(layer));
|
||||
assert!(std::ptr::eq(complete.bundle(), &bundle));
|
||||
assert_eq!(
|
||||
complete.files().collect::<Vec<_>>(),
|
||||
bundle
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (p, s))| (i, p.as_str(), s.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(complete.names(), &names);
|
||||
assert_eq!(complete.typed_occurrences(), &typed);
|
||||
assert_eq!(complete.expressions(), &expressions);
|
||||
assert_eq!(complete.warnings(), &warnings);
|
||||
assert_eq!(complete.indexed.resolved.static_limits, limits);
|
||||
assert_eq!(complete.indexed.resolved.static_usage, static_usage);
|
||||
assert_eq!(complete.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.storage.live,
|
||||
before
|
||||
.add(Slots::entries(expected.len()), Span::default())
|
||||
.unwrap()
|
||||
);
|
||||
let mut headers: BTreeSet<_> = complete
|
||||
.ordinary
|
||||
.values()
|
||||
.filter(|o| o.role == H)
|
||||
.map(|o| o.target)
|
||||
.collect();
|
||||
headers.extend(
|
||||
complete
|
||||
.typed_occurrences()
|
||||
.keys()
|
||||
.filter(|k| k.role == index::TokenRole::Declaration)
|
||||
.map(|k| k.owner),
|
||||
);
|
||||
assert_eq!(
|
||||
headers,
|
||||
complete
|
||||
.names()
|
||||
.keys()
|
||||
.filter_map(|t| if let lower::Target::Global(id) = t {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_file_keys_distinguish_equal_header_offsets_and_exclude_include_prose() {
|
||||
let mut bundle = single("include \"b.arcadia\"; include \"a.arcadia\";");
|
||||
for (path, alias, n) in [("a.arcadia", "aa", 2), ("b.arcadia", "bb", 3)] {
|
||||
bundle.files.insert(
|
||||
path.into(),
|
||||
format!(
|
||||
"oa_activity {alias} \"Same @aa @bb\" id \"{}\" {{ {} }}",
|
||||
id(n),
|
||||
if n == 3 { "parent @aa;" } else { "" }
|
||||
),
|
||||
);
|
||||
}
|
||||
let complete = indexed(&bundle).index_ordinary().unwrap();
|
||||
assert_eq!(
|
||||
complete.ordinary,
|
||||
BTreeMap::from([
|
||||
((0, 12, 14), record(2, 2, OrdinaryRole::Header, 12, 14)),
|
||||
((1, 12, 14), record(3, 3, OrdinaryRole::Header, 12, 14)),
|
||||
((1, 82, 84), record(3, 2, OrdinaryRole::Reference, 81, 84)),
|
||||
((2, 19, 20), record(1, 1, OrdinaryRole::Header, 19, 20)),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
fn rich_with_extra_headers() -> SourceBundle {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let source = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let at = source.rfind('}').unwrap();
|
||||
let extra = (20..26)
|
||||
.map(|n| format!("oa_activity extra{n} \"Extra\" id \"{}\" {{}}\n", id(n)))
|
||||
.collect::<String>();
|
||||
source.insert_str(at, &extra);
|
||||
bundle
|
||||
}
|
||||
#[test]
|
||||
fn exact_ordinary_peak_after_typed_peak_preserves_the_inherited_ledger() {
|
||||
let bundle = rich_with_extra_headers();
|
||||
let baseline = indexed(&bundle).resolved.storage.live;
|
||||
// G13 + T21 + N12 =46, final ordinary header. Six extra leaf activities
|
||||
// add six retained warnings and eighteen lower-catalog slots. The prior
|
||||
// passing resolver cap97 therefore needs at most109; this stage needs111.
|
||||
for (extra, fits) in [(45, false), (46, true)] {
|
||||
let profile = super::super::super::super::budget::Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed =
|
||||
super::super::super::parse_with_profile(&bundle, ProjectLimits::default(), profile)
|
||||
.unwrap();
|
||||
let indexed = resolve(parsed).unwrap().index().unwrap();
|
||||
assert_eq!(indexed.resolved.storage.live, baseline);
|
||||
if fits {
|
||||
assert_eq!(
|
||||
indexed
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
.indexed
|
||||
.resolved
|
||||
.storage
|
||||
.live,
|
||||
baseline.add(Slots::entries(12), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let diagnostic = fail(
|
||||
indexed,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let at = bundle.files["main.arcadia"].find("extra25").unwrap();
|
||||
assert_eq!(diagnostic.diagnostic.span, span(at, at + 7));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn node_field<'a>(
|
||||
indexed: &'a mut IndexedMixedProject<'_>,
|
||||
name: &str,
|
||||
) -> &'a mut language::EditField {
|
||||
let EditKind::Record(fields) = &mut indexed.resolved.syntax.files[2].positions[0].1.fields.kind
|
||||
else {
|
||||
panic!("node fields")
|
||||
};
|
||||
fields.get_mut(name).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_corruptions_reach_owner_header_marker_extent_and_spelling_checks() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
for case in 0..12 {
|
||||
let mut indexed = indexed(&bundle);
|
||||
let message = match case {
|
||||
0 => {
|
||||
indexed.resolved.syntax.files[2].positions[0].0 = id(3);
|
||||
"Ordinary position differs from its declaration owner."
|
||||
}
|
||||
1 => {
|
||||
indexed.resolved.syntax.files[2].positions[0]
|
||||
.1
|
||||
.lexical_owner = Some(id(2));
|
||||
"Ordinary position differs from its declaration owner."
|
||||
}
|
||||
2 => {
|
||||
indexed.resolved.syntax.files[2].positions[0]
|
||||
.1
|
||||
.header
|
||||
.insert("alias", span(177, 178));
|
||||
"Ordinary alias is not its declaration header token."
|
||||
}
|
||||
3 => {
|
||||
indexed.resolved.syntax.files[2]
|
||||
.ordinary
|
||||
.header
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.project
|
||||
.id = id(2);
|
||||
"Ordinary header differs from its global identity."
|
||||
}
|
||||
4 => {
|
||||
indexed.resolved.syntax.files[2].positions[0]
|
||||
.1
|
||||
.fields
|
||||
.span
|
||||
.start += 1;
|
||||
"Ordinary synthetic root differs from its body braces."
|
||||
}
|
||||
5 => {
|
||||
node_field(&mut indexed, "diagram").value.span.start = 177;
|
||||
"Ordinary reference lacks its original marker token."
|
||||
}
|
||||
6 => {
|
||||
node_field(&mut indexed, "diagram").value.span.end = 177;
|
||||
"Ordinary reference extent differs from its complete alias token."
|
||||
}
|
||||
7 => {
|
||||
node_field(&mut indexed, "diagram").value.span.end = 179;
|
||||
"Ordinary reference extent differs from its complete alias token."
|
||||
}
|
||||
8 => {
|
||||
node_field(&mut indexed, "diagram").value.kind =
|
||||
EditKind::Reference("other".into());
|
||||
"Ordinary alias differs from its retained spelling."
|
||||
}
|
||||
9 => {
|
||||
let token = indexed.resolved.syntax.files[2]
|
||||
.tokens
|
||||
.iter_mut()
|
||||
.find(|t| t.span.start == 177)
|
||||
.unwrap();
|
||||
token.class = lex::Class::String;
|
||||
"Ordinary alias is not an original whole Word token."
|
||||
}
|
||||
10 => {
|
||||
node_field(&mut indexed, "diagram").span.start = 0;
|
||||
"Ordinary span crosses its containing syntax."
|
||||
}
|
||||
_ => {
|
||||
indexed.data.names.remove(&lower::Target::Global(id(5)));
|
||||
"Ordinary reference has no global target."
|
||||
}
|
||||
};
|
||||
fail(indexed, message, "main.arcadia");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_header_and_duplicate_physical_ownership_are_rejected() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut missing = indexed(&bundle);
|
||||
missing.resolved.syntax.files[2]
|
||||
.ordinary
|
||||
.declarations
|
||||
.remove(1);
|
||||
missing.resolved.syntax.files[2].positions.remove(1);
|
||||
let diagnostic = fail(
|
||||
missing,
|
||||
"Global name is missing its authored declaration header.",
|
||||
"main.arcadia",
|
||||
);
|
||||
assert_eq!(diagnostic.diagnostic.span, Span::default());
|
||||
let mut duplicate = indexed(&bundle);
|
||||
// A second field position points at the same full reference. Parser emission
|
||||
// cannot produce this; the control isolates ordinary physical ownership.
|
||||
let field = node_field(&mut duplicate, "element");
|
||||
field.span = span(168, 179);
|
||||
field.value.span = span(176, 178);
|
||||
field.value.kind = EditKind::Reference("d".into());
|
||||
fail(
|
||||
duplicate,
|
||||
"Duplicate ordinary or typed physical name token.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let mut cross = indexed(&bundle);
|
||||
let key = cross
|
||||
.data
|
||||
.occurrences
|
||||
.keys()
|
||||
.find(|k| k.role == index::TokenRole::Use(lower::Role::ParameterUse))
|
||||
.unwrap()
|
||||
.clone();
|
||||
cross.data.occurrences.insert(
|
||||
key,
|
||||
index::Location {
|
||||
file: 2,
|
||||
span: span(122, 123),
|
||||
},
|
||||
);
|
||||
fail(
|
||||
cross,
|
||||
"Duplicate ordinary or typed physical name token.",
|
||||
"main.arcadia",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file_and_utf8_damage_use_safe_explicit_diagnostic_locations() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("main.arcadia").unwrap() =
|
||||
bundle.files["main.arcadia"].replace("node n", "// 雨\nnode n");
|
||||
let mut bad = indexed(&bundle);
|
||||
let at = bad.data.occurrences.values_mut().next().unwrap();
|
||||
at.file = usize::MAX;
|
||||
assert_eq!(
|
||||
fail(bad, "Ordinary index has no canonical file.", "main.arcadia")
|
||||
.diagnostic
|
||||
.span,
|
||||
Span::default()
|
||||
);
|
||||
let mut bad = indexed(&bundle);
|
||||
let byte = bundle.files["main.arcadia"].find('雨').unwrap() + 1;
|
||||
bad.resolved.syntax.files[2].positions[0].1.fields.span = span(byte, byte + 1);
|
||||
assert_eq!(
|
||||
fail(
|
||||
bad,
|
||||
"Ordinary synthetic root differs from its body braces.",
|
||||
"main.arcadia"
|
||||
)
|
||||
.diagnostic
|
||||
.span,
|
||||
Span::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_resolution_ignores_same_spelled_scoped_parameter_names() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main
|
||||
.replace("oa_activity w", "oa_activity x")
|
||||
.replace("subject @w", "subject @x");
|
||||
let end = main.rfind('}').unwrap();
|
||||
main.insert_str(
|
||||
end,
|
||||
&format!(
|
||||
"artifact attach \"Attachment\" id \"{}\" {{ owner @x; uri \"x\"; }}\n",
|
||||
id(20)
|
||||
),
|
||||
);
|
||||
let complete = indexed(&bundle).index_ordinary().unwrap();
|
||||
let ordinary = complete
|
||||
.ordinary
|
||||
.values()
|
||||
.find(|r| r.owner == id(20) && r.role == OrdinaryRole::Reference)
|
||||
.unwrap();
|
||||
assert_eq!(ordinary.target, id(2));
|
||||
assert!(complete.names().contains_key(&lower::Target::Parameter {
|
||||
calculation: id(10),
|
||||
parameter: analysis::ParameterId(id(1))
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_stack_success_and_late_header_failure_leave_no_partial_owner() {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
drop(indexed(&bundle).index_ordinary().unwrap());
|
||||
let mut bad = indexed(&bundle);
|
||||
bad.data
|
||||
.names
|
||||
.insert(lower::Target::Global(id(999)), "unowned".into());
|
||||
fail(
|
||||
bad,
|
||||
"Global name is missing its authored declaration header.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let expression = format!("{}1{}", "abs(".repeat(127), ")".repeat(127));
|
||||
let deep = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body {expression}; }}",
|
||||
id(2)
|
||||
));
|
||||
drop(indexed(&deep).index_ordinary().unwrap());
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn duplicate_global_header_and_cross_file_projection_damage_are_distinct() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut duplicate = indexed(&bundle);
|
||||
let mut key = duplicate
|
||||
.data
|
||||
.occurrences
|
||||
.keys()
|
||||
.find(|k| k.role == index::TokenRole::Declaration && k.owner == id(10))
|
||||
.unwrap()
|
||||
.clone();
|
||||
key.field = Some(lower::Field::Body);
|
||||
// Reclassify an existing whole call token as a second header; remove its
|
||||
// former role so physical uniqueness cannot mask the header-coverage check.
|
||||
let call = duplicate
|
||||
.data
|
||||
.occurrences
|
||||
.iter()
|
||||
.find(|(_, at)| at.file == 2 && at.span == span(291, 292))
|
||||
.unwrap()
|
||||
.0
|
||||
.clone();
|
||||
duplicate.data.occurrences.remove(&call);
|
||||
duplicate.data.occurrences.insert(
|
||||
key,
|
||||
index::Location {
|
||||
file: 2,
|
||||
span: span(291, 292),
|
||||
},
|
||||
);
|
||||
fail(
|
||||
duplicate,
|
||||
"Global declaration header appears more than once.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let mut bundle = single("include \"a.arcadia\"; include \"b.arcadia\";");
|
||||
for (file, alias, n) in [("a.arcadia", "aa", 2), ("b.arcadia", "bb", 3)] {
|
||||
bundle.files.insert(
|
||||
file.into(),
|
||||
format!("oa_activity {alias} \"Same\" id \"{}\" {{}}", id(n)),
|
||||
);
|
||||
}
|
||||
let mut damaged = indexed(&bundle);
|
||||
let other = damaged.resolved.syntax.files[1].positions.remove(0);
|
||||
damaged.resolved.syntax.files[0].positions[0] = other;
|
||||
fail(
|
||||
damaged,
|
||||
"Ordinary position differs from its declaration owner.",
|
||||
"a.arcadia",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_ordinary_declarations_use_no_typed_frame_policy() {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
let mut body = String::new();
|
||||
for n in 2..34 {
|
||||
body.push_str(&format!("oa_activity a{n} \"A\" id \"{}\" {{\n", id(n)));
|
||||
}
|
||||
body.push_str(&"}\n".repeat(32));
|
||||
let bundle = single(&body);
|
||||
let indexed = indexed(&bundle);
|
||||
let usage = indexed.resolved.syntax.budget.usage;
|
||||
let before = indexed.resolved.storage.live;
|
||||
let complete = indexed.index_ordinary().unwrap();
|
||||
assert_eq!(complete.ordinary.len(), 33);
|
||||
assert!(
|
||||
complete
|
||||
.ordinary
|
||||
.values()
|
||||
.all(|o| o.role == OrdinaryRole::Header)
|
||||
);
|
||||
assert_eq!(complete.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(33), Span::default()).unwrap()
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn malformed_reference_utf8_start_is_reported_without_slicing_panic() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("main.arcadia").unwrap() =
|
||||
bundle.files["main.arcadia"].replace("diagram @d", "diagram @ // 雨\r\n d");
|
||||
let mut damaged = indexed(&bundle);
|
||||
let byte = bundle.files["main.arcadia"].find('雨').unwrap() + 1;
|
||||
node_field(&mut damaged, "diagram").value.span.start = byte;
|
||||
let diagnostic = fail(
|
||||
damaged,
|
||||
"Ordinary span is outside its original UTF-8 file.",
|
||||
"main.arcadia",
|
||||
);
|
||||
assert_eq!(diagnostic.diagnostic.span, Span::default());
|
||||
}
|
||||
const CATEGORIES: &str = include_str!("tests/fixtures/categories.arcadia");
|
||||
fn categories() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), CATEGORIES.into())]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_authored_category_reference_excludes_trivia_prose_and_historical_uuid() {
|
||||
use OrdinaryRole::{Header as H, Reference as R};
|
||||
let bundle = categories();
|
||||
assert_eq!(CATEGORIES.len(), 2152);
|
||||
let indexed = indexed(&bundle);
|
||||
let model = indexed.project().clone();
|
||||
let warnings = indexed.warnings().to_vec();
|
||||
let before = indexed.resolved.storage.live;
|
||||
let usage = indexed.resolved.syntax.budget.usage;
|
||||
let numeric = indexed.resolved.syntax.budget.numeric_units();
|
||||
let complete = indexed.index_ordinary().unwrap();
|
||||
// Entire independently authored36-row map, checked by literal UTF-8 arithmetic.
|
||||
let rows = [
|
||||
(19, 20, 1, 1, H, 19),
|
||||
(168, 169, 2, 2, H, 168),
|
||||
(249, 254, 3, 3, H, 249),
|
||||
(344, 345, 3, 2, R, 323),
|
||||
(369, 377, 4, 4, H, 369),
|
||||
(487, 488, 5, 5, H, 487),
|
||||
(561, 565, 6, 6, H, 561),
|
||||
(625, 630, 6, 3, R, 624),
|
||||
(640, 648, 6, 4, R, 639),
|
||||
(664, 665, 7, 7, H, 664),
|
||||
(741, 745, 7, 6, R, 740),
|
||||
(789, 793, 7, 6, R, 788),
|
||||
(818, 822, 8, 8, H, 818),
|
||||
(885, 889, 8, 6, R, 884),
|
||||
(925, 926, 9, 9, H, 925),
|
||||
(992, 997, 9, 3, R, 991),
|
||||
(1003, 1004, 9, 5, R, 1002),
|
||||
(1328, 1329, 10, 10, H, 1328),
|
||||
(1391, 1394, 10, 12, R, 1390),
|
||||
(1397, 1402, 10, 14, R, 1396),
|
||||
(1422, 1425, 11, 11, H, 1422),
|
||||
(1487, 1491, 11, 13, R, 1486),
|
||||
(1514, 1517, 12, 12, H, 1514),
|
||||
(1577, 1578, 12, 10, R, 1576),
|
||||
(1617, 1620, 12, 11, R, 1616),
|
||||
(1648, 1652, 13, 13, H, 1648),
|
||||
(1712, 1715, 13, 11, R, 1711),
|
||||
(1741, 1746, 13, 3, R, 1740),
|
||||
(1789, 1794, 14, 14, H, 1789),
|
||||
(1855, 1856, 14, 10, R, 1854),
|
||||
(1882, 1890, 14, 4, R, 1881),
|
||||
(1935, 1942, 15, 15, H, 1935),
|
||||
(2004, 2005, 15, 10, R, 2003),
|
||||
(2029, 2032, 15, 12, R, 2028),
|
||||
(2047, 2051, 15, 13, R, 2046),
|
||||
(2092, 2097, 15, 14, R, 2091),
|
||||
];
|
||||
let expected = rows
|
||||
.into_iter()
|
||||
.map(|(start, end, owner, target, role, extent)| {
|
||||
((0, start, end), record(owner, target, role, extent, end))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(complete.ordinary_occurrences(), &expected);
|
||||
assert_eq!(complete.project(), &model);
|
||||
assert_eq!(complete.warnings(), warnings);
|
||||
assert!(std::ptr::eq(complete.bundle(), &bundle));
|
||||
assert!(complete.typed_occurrences().is_empty());
|
||||
assert!(complete.expressions().is_empty());
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(36), Span::default()).unwrap()
|
||||
);
|
||||
assert_eq!(complete.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(complete.project().elements[&id(3)].parent, Some(id(2)));
|
||||
assert_eq!(complete.project().elements[&id(4)].parent, Some(id(2)));
|
||||
assert!(
|
||||
!complete
|
||||
.ordinary
|
||||
.values()
|
||||
.any(|o| o.owner == id(4) && o.role == R),
|
||||
"implicit parent has no authored token"
|
||||
);
|
||||
assert_eq!(&CATEGORIES[323..345], "@ // @noise child\r\n a");
|
||||
let Some(semantic) = complete.project().traces[&id(9)]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
else {
|
||||
panic!("historical semantic baseline")
|
||||
};
|
||||
assert!(
|
||||
matches!(&semantic.facets[0],crate::semantic::FacetReview::Field(crate::semantic::FieldReview::Type(review)) if review.oa==crate::semantic::Observed::Value(crate::TypeRef::Named(id(3))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_scratch_peak_reserves_live_iterative_slots_before_growth() {
|
||||
let mut bundle = categories();
|
||||
let points = ["{ x 0; y 0; }"; 64].join(", ");
|
||||
let source = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*source = source.replace(
|
||||
"routes [{ edge @link; }];",
|
||||
&format!("routes [{{ edge @link; waypoints [{points}]; }}];"),
|
||||
);
|
||||
let baseline = indexed(&bundle).resolved.storage.live;
|
||||
// Unexpanded category peak52 is below the earlier resolver catalog/Vec
|
||||
// overlap. Sixty-four valid route points isolate this stage: N11 + G15,
|
||||
// pending edge1 + point values64, then pop one and push x/y =>W66.
|
||||
// Exactly92 slots are live; earlier catalog overlap is only74.
|
||||
for (extra, fits) in [(91, false), (92, true)] {
|
||||
let profile = super::super::super::super::budget::Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed =
|
||||
super::super::super::parse_with_profile(&bundle, ProjectLimits::default(), profile)
|
||||
.unwrap();
|
||||
let indexed = resolve(parsed).unwrap().index().unwrap();
|
||||
assert_eq!(indexed.resolved.storage.live, baseline);
|
||||
if fits {
|
||||
assert_eq!(
|
||||
indexed
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
.indexed
|
||||
.resolved
|
||||
.storage
|
||||
.live,
|
||||
baseline.add(Slots::entries(36), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let diagnostic = fail(
|
||||
indexed,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let start = bundle.files["main.arcadia"].rfind("y 0").unwrap() + 2;
|
||||
assert_eq!(diagnostic.diagnostic.span, span(start, start + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# This exact-byte source fixture deliberately retains its authored CRLF.
|
||||
categories.arcadia -text whitespace=cr-at-eol
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
arcadia 8;
|
||||
project p "Authored @p child" id "00000000-0000-0000-0000-000000000001" {
|
||||
description "Do not index @child, @link or uuid in prose 雨";
|
||||
oa_activity a "Parent child" id "00000000-0000-0000-0000-000000000002" {
|
||||
oa_activity child "Explicit child" id "00000000-0000-0000-0000-000000000003" { parent @ // @noise child
|
||||
a; }
|
||||
oa_activity implicit "Implicit child" id "00000000-0000-0000-0000-000000000004" { description "child @a"; }
|
||||
}
|
||||
sa_function s "Selected" id "00000000-0000-0000-0000-000000000005" {}
|
||||
interaction link "Link" id "00000000-0000-0000-0000-000000000006" { source @child; target @implicit; }
|
||||
diagram d "View" id "00000000-0000-0000-0000-000000000007" {
|
||||
routes [{ edge @link; }];
|
||||
view_options { hidden_edges [@link]; };
|
||||
}
|
||||
artifact note "Evidence" id "00000000-0000-0000-0000-000000000008" { owner @link; uri "docs/@child.md"; }
|
||||
trace t "Review" id "00000000-0000-0000-0000-000000000009" {
|
||||
oa @child; sa @s; kind Manual;
|
||||
baseline {
|
||||
oa { name "child"; description "@child"; };
|
||||
sa { name "s"; description "@s"; };
|
||||
semantic { facets [{ Field { Type { oa { Value { Named uuid "00000000-0000-0000-0000-000000000003"; }; }; sa { Value Boolean; }; }; }; }]; };
|
||||
};
|
||||
}
|
||||
oa_process q "Caller" id "00000000-0000-0000-0000-000000000010" { steps [@use, @right]; }
|
||||
oa_process sub "Callee" id "00000000-0000-0000-0000-000000000011" { steps [@left]; }
|
||||
oa_occurrence use "Call" id "00000000-0000-0000-0000-000000000012" { parent @q; body { SequenceReference { sequence @sub; }; }; }
|
||||
oa_occurrence left "Left" id "00000000-0000-0000-0000-000000000013" { parent @sub; body { Action { action @child; lifeline none; }; }; }
|
||||
oa_occurrence right "Right" id "00000000-0000-0000-0000-000000000014" { parent @q; body { Action { action @implicit; lifeline none; }; }; }
|
||||
contextual_link context "Context" id "00000000-0000-0000-0000-000000000015" { owner @q; source { references [@use]; occurrence @left; }; target { references []; occurrence @right; }; purpose { Precedence { guard "@child"; }; }; }
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::analysis_numeric::Exact;
|
||||
use crate::source_project::{ProjectLimits, SourceBundle};
|
||||
use crate::*;
|
||||
|
||||
fn id(n: usize) -> Id {
|
||||
pub(super) fn id(n: usize) -> Id {
|
||||
Id::parse_str(&format!("00000000-0000-0000-0000-{n:012}")).unwrap()
|
||||
}
|
||||
fn doc(body: &str) -> String {
|
||||
@@ -15,7 +15,7 @@ fn doc(body: &str) -> String {
|
||||
id(1)
|
||||
)
|
||||
}
|
||||
fn single(body: &str) -> SourceBundle {
|
||||
pub(super) fn single(body: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(body))]),
|
||||
@@ -144,7 +144,7 @@ fn expected(layer: Layer) -> Project {
|
||||
)]),
|
||||
}
|
||||
}
|
||||
fn accepted(bundle: &SourceBundle) -> ResolvedMixedProject<'_> {
|
||||
pub(super) fn accepted(bundle: &SourceBundle) -> ResolvedMixedProject<'_> {
|
||||
resolve(parse(bundle, ProjectLimits::default()).unwrap()).unwrap_or_else(|e| panic!("{e:?}"))
|
||||
}
|
||||
fn rejected(bundle: &SourceBundle) -> Failure {
|
||||
@@ -250,7 +250,7 @@ fn complete_oa_sa_forward_model_warning_and_original_authority() {
|
||||
}
|
||||
}
|
||||
|
||||
fn rich_bundle(layer: Layer) -> SourceBundle {
|
||||
pub(super) fn rich_bundle(layer: Layer) -> SourceBundle {
|
||||
let mut bundle = bundle(layer);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main
|
||||
@@ -270,7 +270,7 @@ fn rich_bundle(layer: Layer) -> SourceBundle {
|
||||
}
|
||||
bundle
|
||||
}
|
||||
fn rich_model(layer: Layer) -> Project {
|
||||
pub(super) fn rich_model(layer: Layer) -> Project {
|
||||
let mut model = expected(layer);
|
||||
for (alias, name, n, op) in [
|
||||
("f", "F", 10, a::BinaryOp::Add),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Private carried-baseline phase-check implementation
|
||||
|
||||
2026-09-25. Based on `18073a6f2eabce83fc83997f094e331eec835a16`; implements the limited prerequisite selected in `harness/analysis-carried-phase-contract-20260925.md` (parent evidence commit `a2c781773454ee932a9e292e6dc93f551f9f86c5`). Source and tests are authored; compilation/runtime acceptance belong to parent gates and are not claimed here.
|
||||
|
||||
`mixed/carried.rs` introduces an opaque non-Clone input backed by either a fixed borrowed bundle with captured policy or a borrowed allocation-owning RetainedSnapshot. Every new constructor is `cfg(test)`. The retained fixture owns the actual complete SourceBundle and computes exactly F map slots, zero lower occurrence slots and all source/key/entry UTF-8 bytes. The parser cannot substitute a different bundle at invocation. There is no production emitter/old-owner transition, accepted-output result, public source8 activation or editing authority.
|
||||
|
||||
The shared parser implementation has explicit legacy versus carried mode. Existing entry functions choose legacy and preserve their separate metadata checkpoints. Carried mode checks C initially, C+file metadata before traversal, C+U+M before every position projection and at complete syntax return, even when C is zero. Checks use existing logical Slots arithmetic and inherited ceilings; they are not additional retained reservations. A narrow read-only Budget policy getter preserves all Limits values. Every snapshot still has one fresh Budget and normal independent Usage/Exact work, with exact caller ProjectLimits.
|
||||
|
||||
ParsedMixedSyntax retains the guard. Consumed resolution seeds its Ledger from that guard once, then runs the existing syntax accounting and subsequent reservation/release sequence unchanged. Successful ownership naturally retains the guard through syntax inside resolved/indexed owners. A cfg(test) immutable `phase_state` observer exposes syntax/Slots for sibling tests; it exposes no mutation or production constructor.
|
||||
|
||||
Temporal scope is deliberately limited: current-unit parsing and syntax string decoding/copying can precede aggregate phase refusal, and retained syntax text is counted during resolution. Existing grammar event checks remain before their declared allocations; this prerequisite does not add universal parser allocation admission. Model/serde/raw validator heap, allocator capacity/RSS and stack frames keep their established exclusions. Stronger before-growth obligations, output creation, whole actual-output equality and accepted edited-output ownership remain future requirements. This work neither closes nor waives them.
|
||||
|
||||
Twelve authored tests cover independent E/H bundle counts and whole Project oracles, real retained guard identity, exact new Usage, once-only resolver +C retention, parser 13/12 and 25/24 boundaries, resolver 17/16 and 29/28 entry peaks, 112/111 and 214/213 text peaks, canonical original diagnostic paths/spans, initial C/file checks, postdecode syntax text and ordinary position-key copy accounting, unchanged legacy versus explicit carried-zero timing, all captured Limits/ProjectLimits fields, real byte/token/reference/frame/Exact/model/file/depth/include policies, and OA/SA Unicode/CRLF compatibility through both index families. Nonzero occurrence and impossible arithmetic cases are explicitly labeled fault injection; the actual retained bundle owner has no occurrence storage.
|
||||
|
||||
E is 78 source bytes with C=(1,0,102), U 1 / M 11 and final (13,0,110). H is 132 source bytes with C=(3,0,180), U 3 / M 19 and final (25,0,212); its final comment contains actual é and is 6 UTF-8 bytes. These expectations come from the independent parent oracle, not application outputs. Syntax-only exact caps are not reported as full-resolve successes. Parser default resource spans remain 0..1; resolver restores 0..0 or the original header/include extent.
|
||||
|
||||
Zero carried mode intentionally refuses E cap 11 at preprojection, while legacy cap 11 still produces syntax and then refuses resolution at U+M 12. This distinction preserves the old API timing without numerically bypassing the new carried contract. Borrowed policy capture from a still-live fixture witness is test-only propagation evidence, not proof old auxiliary B was consumed or dropped.
|
||||
|
||||
No Cargo/build/test execution was performed by the implementation agent. Targeted rustfmt on owned files and raw-LF normalization are authoring checks only. Parent owns source review integration, exact-source commits, compiler/focused/full-suite gates and publication. Inherited full-suite failures and formatting issues are not addressed by this patch; parent gates will compare actual outcomes. The documented environment-sensitive upstream stack observation remains a limitation. Existing small-stack controls were not edited or waived.
|
||||
|
||||
Independent test-source review corrected two pre-execution fixture mistakes: use the accepted oa_property/sa_property keywords, and distinguish the parser include-edge error span 0..1 from the traversal include-depth span 0..23. These are source-review corrections, not recorded runtime failures.
|
||||
|
||||
## Recorded regression and slim-guard candidate
|
||||
|
||||
Parent's `d11b99bb` focused run passed all 12 carried tests, but its full run aborted at the unchanged `ordinary_index::tests::small_stack_success_and_late_header_failure_leave_no_partial_owner` with STATUS_STACK_OVERFLOW. The predecessor `18073a6f` full run had completed with 230 passing tests and 16 inherited failures, including that control. This is a failed current full gate, not an inherited-failure waiver or an accepted repair.
|
||||
|
||||
The exact-artifact LLDB trace reproduces the overflow during Windows heap allocation under formatting/Exact::parse_decimal, then exact_literal, syntax::binding, declarations::parse, State::parse_typed, Parser::unit, new parse_impl, parse_with_profile, parse, accepted/indexed helpers and the existing worker. The diagnostic child's killed exit is not a test pass. Parent measured individual prologues of parse_impl 11,144 bytes, parse_with_profile 312, accepted 1,752, indexed 1,704 and worker 7,752; these are not the complete active stack. Raw evidence remains in the parent gate records, including `carried-debugger-d11b99b-1.log` and `carried-full-d11b99b-1.stack-frames.json`.
|
||||
|
||||
The selected narrow candidate consumes CarriedInput before entering the shared parser. Exact captured policy transfers into Budget and syntax.limits; the retained CarriedGuard holds only Borrowed or Retained(&RetainedSnapshot). This removes the duplicate inline SnapshotPolicy payload from parsed/resolved/indexed/result carriers, while retained backing still borrows the actual allocation-owning bundle. Borrowed source identity remains bound through ParsedMixedSyntax.bundle. Explicit Some mode still distinguishes carried zero from legacy None, and all checks/reservations/Usage remain unchanged. No heap allocation or new logical storage was introduced. The parser wrapper remains uninlined and unchanged.
|
||||
|
||||
Only carried tests' guard-shape inspection and direct injected-arithmetic access follow the new internal guard type; all expected counts, limits, spans, outcomes and existing stack tests remain unchanged. The candidate is not claimed to establish a sole overflow cause or a successful repair before parent exact-source gates. All original failure evidence remains retained.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Private authored ordinary-name index implementation
|
||||
|
||||
Before compilation, source checkpoint `50a5918f` exposed Git normalization of
|
||||
the CRLF category fixture. A narrow fixture attribute now preserves its exact
|
||||
2,152 bytes and authored span oracle. Test-only resource variants were also
|
||||
strengthened to reach this new stage after prior phases fit; the initial source
|
||||
was not built or runtime-tested. Production bytes are unchanged by these repairs.
|
||||
|
||||
Local implementation on the `93eb6117f95353d25d594d59780018f197c51672` private lineage, following the root accepted ordinary-index contract (2026-09-24). Runtime acceptance is pending the parent-owned compiler/test gates; this note records design and authored evaluators, not passing results.
|
||||
|
||||
`IndexedMixedProject::index_ordinary(self)` consumes the admitted typed owner and returns `ordinary_index::AuthoredMixedProject`. It retains that owner and one map keyed by canonical file and whole alias-token byte range. Inline records carry owner/global target, Header/Reference role and full original extent. Read-only methods expose both occurrence families, expression keys, names, complete Project, warnings and original bundle/files. No adapter to legacy Index/Plan/Composition or independently replaceable source/profile is added.
|
||||
|
||||
Construction borrows original significant tokens and the sealed ordinary AST/position pairs. Headers are checked against declaration occurrences, not enclosing ranges; the synthetic root Record includes body braces, while real field/value extents are checked within their justified parents. Every Reference leaf is visited, including nested records/lists and arbitrary ordinary fields. A Reference must start at the significant `@` and end exactly at the immediately following whole Word. HistoricalUuid, Atom and prose/include/identity strings are excluded. No reparse, lower, model copy, name copy, numeric work, admission or frame call occurs. Completeness trusts the sealed projection plus exhaustive authored oracles; per-record checks do not establish arbitrary coordinated AST/projection omission rejection.
|
||||
|
||||
The inherited ledger remains live. New retention is exactly N=1+D+A entry slots, zero occurrence slots, zero text bytes. Scratch is G borrowed global-name entries with inline header-seen flags, T typed physical tokens and W currently queued borrowed values. Reserve before every map/set/work growth; a pop releases one logical slot; globals and typed lookup release after their containers drop. Work drains per declaration; the synthetic root is not queued. Actual rich OA/SA stage peaks are incoming baseline+34/+36 (retained+6/+7), but earlier typed peaks prevent those profiles from isolating this later stage. Rich plus six empty activities uses a later-stage46/45 boundary (retained12), clearing the preceding lower-catalog overlap including retained warnings; unexpanded category fixture has stage peak52 with W5, but that can be below preceding resolver overlap. The scratch-cap test adds64 finite route waypoints to a separate in-memory variant and uses92/91 with W66 (retained36). These hand-counts still require runtime verification, including earlier-stage admission.
|
||||
|
||||
Authored tests cover complete OA/SA maps with literal numeric spans, independent Unicode/CRLF spans, exact rich Project/model metadata retention, canonical coincident offsets and cross-file references, global versus calculation parameter spelling, nested explicit/implicit parents, comment-separated markers, relation/context/route/view/artifact/trace categories, historical UUID exclusion, precise corruption diagnostics and safe fallback, retained/scratch caps without Budget replacement, and bounded-stack success/late failure. The category literal initially contained an invalid duplicate route; the independent oracle critic caught it before compilation and the duplicate was removed. The corrected fixture has36 independently enumerated records (15 headers, 21 references). No inherited compatibility test or known failure disposition is removed.
|
||||
|
||||
Focused runtime at `147b247` reported 9 passing and 4 failing tests (parent-owned gate). Three failures were pre-index refusals from incorrectly authored C-style block comments: the shared raw scanner only treats `//` through newline as Comment. Category trivia now uses a byte-neutral line-comment replacement, preserving all36 numeric records and the full marker/alias extents; the UTF-8 corruption fixture likewise first admits a real line comment before damaging its retained start. The fourth control accidentally duplicated an already indexed call token before it could test header coverage; it now removes that token's former call role before assigning the forged second Header role. Intended later diagnostics remain unchanged. These are test/fixture corrections; production is unchanged, and the 147b247 failures remain evidence. Retest pending.
|
||||
Reference in New Issue
Block a user