Retain mixed parse policy and check aggregate budgets before traversal

This commit is contained in:
2026-09-23 04:42:32 -07:00
parent 0fc0826435
commit 754a9b4b7c
2 changed files with 223 additions and 1 deletions
@@ -36,6 +36,9 @@ struct ModelCounts {
pub(super) struct ParsedMixedSyntax<'s> {
bundle: &'s SourceBundle,
// Preserve caller policy for the later semantic pass. Syntax storage uses
// separate ceilings and cannot stand in for these graph/record/step limits.
limits: ProjectLimits,
files: Vec<FileSyntax<'s>>,
budget: Budget,
}
@@ -260,7 +263,6 @@ fn parse_with_profile(
)));
}
let mut budget = Budget::new(profile).map_err(at_entry)?;
let mut traversal = Traversal::new(bundle, limits)?;
let bytes = bundle
.files
.values()
@@ -273,6 +275,9 @@ fn parse_with_profile(
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)?;
let mut parsed = BTreeMap::new();
let mut model = ModelCounts {
limits,
@@ -361,6 +366,7 @@ fn parse_with_profile(
budget.active().map_err(at_entry)?;
Ok(ParsedMixedSyntax {
bundle,
limits,
files: parsed.into_values().collect(),
budget,
})
@@ -919,3 +919,219 @@ fn mixed_deep_typed_success_and_rejection_cleanup_on_128k_stack() {
.join()
.unwrap();
}
#[test]
fn witness_retains_exact_caller_policy_separately_from_syntax_storage() {
let bundle = single(&ordinary("a", FIRST_ID, "description \"retained\";"));
let tight = ProjectLimits {
max_files: 1,
max_include_depth: 0,
max_include_edges: 0,
model: ParseLimits {
max_source_bytes: bundle.files["main.arcadia"].len(),
max_graph_objects: 1,
max_records: 2,
max_step_references: 0,
},
};
let loose = ProjectLimits {
model: ParseLimits {
max_graph_objects: 5,
max_records: 20,
..tight.model
},
..tight
};
let policy = |p: ProjectLimits| {
(
p.max_files,
p.max_include_depth,
p.max_include_edges,
p.model.max_source_bytes,
p.model.max_graph_objects,
p.model.max_records,
p.model.max_step_references,
)
};
let first = parse(&bundle, tight).unwrap();
let second = parse(&bundle, loose).unwrap();
assert_eq!(policy(first.limits), policy(tight));
assert_eq!(policy(second.limits), policy(loose));
assert_ne!(policy(first.limits), policy(second.limits));
assert!(std::ptr::eq(first.bundle, second.bundle));
assert_eq!(first.budget.usage, second.budget.usage);
assert_eq!(first.budget.numeric_units(), second.budget.numeric_units());
// Neither identical syntax nor its storage profile is allowed to erase the
// distinct policy needed for later full nested-record/model-step admission.
}
#[test]
fn syntax_entries_accumulate_across_both_ordinary_and_typed_producer_orders() {
let ordinary = ordinary("a", FIRST_ID, "description \"second charge\";");
let typed = calculation("c", CALC_ID, "1");
for (body, failure_text, token_offset) in [
(format!("{ordinary}\n{typed}"), "body 1;", 5),
(format!("{typed}\n{ordinary}"), "\"second charge\"", 0),
] {
let bundle = single(&body);
let source = &bundle.files["main.arcadia"];
// Independently count project1 + ordinary declaration/field/value3 +
// typed header/result-field/type/body-field/literal5 =9 syntax entries.
let positive = parse(&bundle, ProjectLimits::default()).unwrap();
assert_eq!(positive.budget.usage.entries, 9);
let profile = Limits {
entries: 8,
..Limits::source8(ParseLimits::UNLIMITED)
};
let errors = match parse_with_profile(&bundle, ProjectLimits::default(), profile) {
Ok(_) => panic!("reset syntax budget accepted both producers"),
Err(errors) => errors,
};
assert_eq!(
errors[0].diagnostic.message,
"Source8 syntax entries limit exceeded."
);
assert_eq!(errors[0].path, "main.arcadia");
assert_eq!(
errors[0].diagnostic.span.start,
source.find(failure_text).unwrap() + token_offset
);
// A mistakenly reset counter might instead fail the later metadata cap;
// that different phase/location must not satisfy this oracle.
}
}
#[test]
fn exact_work_and_reference_occurrences_accumulate_across_files() {
let split = |expression: &str| SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([
(
"main.arcadia".into(),
document("include \"first.arcadia\"; include \"second.arcadia\";"),
),
(
"first.arcadia".into(),
calculation("first", FIRST_ID, expression),
),
(
"second.arcadia".into(),
calculation("second", SECOND_ID, expression),
),
]),
};
// Literal1's specified meter weights: parse2 + power construction2 +
// mantissa product2 + one-word normalization67 =73. Two conversions cost146.
// These thresholds are not measured from a mixed witness or parse helper.
let numeric = split("1");
let profile = Limits {
numeric_units: 146,
..Limits::source8(ParseLimits::UNLIMITED)
};
let accepted = parse_with_profile(&numeric, ProjectLimits::default(), profile).unwrap();
assert_eq!(accepted.budget.numeric_units(), 146);
for alias in ["first", "second"] {
let alone = single(&calculation(alias, FIRST_ID, "1"));
let accepted = parse_with_profile(
&alone,
ProjectLimits::default(),
Limits {
numeric_units: 73,
..profile
},
)
.unwrap();
assert_eq!(accepted.budget.numeric_units(), 73);
}
let errors = match parse_with_profile(
&numeric,
ProjectLimits::default(),
Limits {
numeric_units: 145,
..profile
},
) {
Ok(_) => panic!("per-file reset lost earlier numeric work"),
Err(errors) => errors,
};
assert_eq!(errors[0].path, "second.arcadia");
assert_eq!(
errors[0].diagnostic.span.start,
numeric.files["second.arcadia"].find("body 1;").unwrap() + 5
);
assert!(
errors[0]
.diagnostic
.message
.contains("NumericUnits: used 73, requested 73, limit 145")
);
let references = split("@missing");
let profile = Limits {
references: 1,
..Limits::source8(ParseLimits::UNLIMITED)
};
for alias in ["first", "second"] {
let alone = single(&calculation(alias, FIRST_ID, "@missing"));
assert_eq!(
parse_with_profile(&alone, ProjectLimits::default(), profile)
.unwrap()
.budget
.usage
.references,
1
);
}
let accepted = parse_with_profile(
&references,
ProjectLimits::default(),
Limits {
references: 2,
..profile
},
)
.unwrap();
assert_eq!(accepted.budget.usage.references, 2);
let errors = match parse_with_profile(&references, ProjectLimits::default(), profile) {
Ok(_) => panic!("per-file reset lost earlier reference occurrence"),
Err(errors) => errors,
};
assert_eq!(errors[0].path, "second.arcadia");
assert_eq!(
errors[0].diagnostic.message,
"Source8 references limit exceeded."
);
assert_eq!(
errors[0].diagnostic.span.start,
references.files["second.arcadia"].find("@missing").unwrap() + 1
);
}
#[test]
fn private_aggregate_preflights_run_before_traversal_path_allocation() {
let mut bundle = single("");
// Normal private policy reaches traversal and rejects the non-root entry.
// Under tighter preflight policy, no traversal authority is constructed.
bundle.entry = "nested/main.arcadia".into();
assert_eq!(rejects(&bundle)[0].diagnostic.code, "invalid_path");
let normal = Limits::source8(ParseLimits::UNLIMITED);
for (profile, message) in [
(
Limits { bytes: 0, ..normal },
"Source8 index/output byte limit exceeded.",
),
(
Limits {
entries: 0,
..normal
},
"Source8 lowering entry limit exceeded.",
),
] {
let errors = match parse_with_profile(&bundle, ProjectLimits::default(), profile) {
Ok(_) => panic!("exhausted private aggregate returned a witness"),
Err(errors) => errors,
};
assert_eq!(errors[0].diagnostic.message, message);
}
}