Compare commits

...
6 changed files with 887 additions and 8 deletions
@@ -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.",
);
}
@@ -28,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
}
@@ -438,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,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.