Carry retained source ownership through private parser phase checks
This commit is contained in:
@@ -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::CarriedInput<'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::CarriedInput<'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();
|
||||
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.")))?;
|
||||
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,121 @@
|
||||
//! 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>,
|
||||
}
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn policy(&self) -> &SnapshotPolicy {
|
||||
match &self.backing {
|
||||
SnapshotBacking::Borrowed { policy, .. } => policy,
|
||||
SnapshotBacking::Retained(owner) => &owner.policy,
|
||||
}
|
||||
}
|
||||
fn bundle(&self) -> &'s SourceBundle {
|
||||
match &self.backing {
|
||||
SnapshotBacking::Borrowed { bundle, .. } => bundle,
|
||||
SnapshotBacking::Retained(owner) => &owner.bundle,
|
||||
}
|
||||
}
|
||||
pub(super) fn slots(&self) -> Slots {
|
||||
match &self.backing {
|
||||
SnapshotBacking::Borrowed { .. } => Slots::default(),
|
||||
SnapshotBacking::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 = input.bundle();
|
||||
let policy = input.policy();
|
||||
parse_impl(bundle, policy.project_limits, policy.limits, Some(input))
|
||||
}
|
||||
|
||||
#[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,655 @@
|
||||
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,SnapshotBacking::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().check(&budget, 0, 1).unwrap_err().message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
held.retained = Slots::default();
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.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,21 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user