Retain mixed parse policy and check aggregate budgets before traversal
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user