fix(analysis): bound type-checker continuations and admission work
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
//! Bounded static admission for the actual Project catalog and analysis records.
|
||||
use super::typecheck::BindingContext;
|
||||
use super::*;
|
||||
use crate::analysis_numeric::{ResolvedKind, Unit};
|
||||
use crate::{Id, Kind, Layer, Project, SharedBody};
|
||||
@@ -6,6 +7,10 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Provisional ceiling; final acceptance requires the root-owned measured gate.
|
||||
/// This is separate from runtime work limits and must be recorded by consumers.
|
||||
/// Events cover analysis record/node visits, queued child slots, scanned text
|
||||
/// bytes, parameter lookups and dependency/relation hops. Fixed-size type and
|
||||
/// exact-number operations have bounded cost within their containing event;
|
||||
/// this does not meter the existing architectural validator or wire allocation.
|
||||
pub const MAX_STATIC_EVENTS: u64 = 1_000_000;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -122,6 +127,7 @@ fn resolve_definitions(
|
||||
("name", &definition.name),
|
||||
("description", &definition.description),
|
||||
] {
|
||||
budget.charge(*id, field, value.len() as u64)?;
|
||||
if !crate::validation::valid_text(value) {
|
||||
return Err(AnalysisError::new(
|
||||
*id,
|
||||
@@ -184,6 +190,7 @@ fn resolve_definitions(
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
budget.charge(*id, "symbol", unit.symbol.len() as u64)?;
|
||||
if !crate::validation::valid_text(&unit.symbol) {
|
||||
return Err(AnalysisError::new(
|
||||
*id,
|
||||
@@ -240,11 +247,7 @@ impl Info {
|
||||
"Expression exceeds the stored closure node limit.",
|
||||
));
|
||||
}
|
||||
let mut push = |child| pending.push((child, depth + 1));
|
||||
match node {
|
||||
Expr::Property(id) => {
|
||||
info.properties.insert(*id);
|
||||
}
|
||||
let children = match node {
|
||||
Expr::List(items) => {
|
||||
if items.len() > MAX_LIST_LENGTH {
|
||||
return Err(AnalysisError::new(
|
||||
@@ -254,6 +257,45 @@ impl Info {
|
||||
"List exceeds 1024 entries.",
|
||||
));
|
||||
}
|
||||
items.len()
|
||||
}
|
||||
Expr::Call { arguments, .. } => {
|
||||
if arguments.len() > MAX_CLOSURE_NODES {
|
||||
return Err(AnalysisError::new(
|
||||
owner,
|
||||
field,
|
||||
"analysis_nodes",
|
||||
"Call argument count exceeds the stored closure limit.",
|
||||
));
|
||||
}
|
||||
arguments.len()
|
||||
}
|
||||
Expr::Unary { .. } | Expr::Power { .. } | Expr::AsKind { .. } => 1,
|
||||
Expr::Binary { .. } | Expr::Sum { .. } => 2,
|
||||
Expr::If { .. } => 3,
|
||||
_ => 0,
|
||||
};
|
||||
if info
|
||||
.nodes
|
||||
.saturating_add(pending.len())
|
||||
.saturating_add(children)
|
||||
> MAX_CLOSURE_NODES
|
||||
{
|
||||
return Err(AnalysisError::new(
|
||||
owner,
|
||||
field,
|
||||
"analysis_nodes",
|
||||
"Visited and pending expression nodes exceed the stored closure limit.",
|
||||
));
|
||||
}
|
||||
// Precharge variable-width iteration and allocation before enqueue.
|
||||
budget.charge(owner, field, children as u64)?;
|
||||
let mut push = |child| pending.push((child, depth + 1));
|
||||
match node {
|
||||
Expr::Property(id) => {
|
||||
info.properties.insert(*id);
|
||||
}
|
||||
Expr::List(items) => {
|
||||
for child in items.iter().rev() {
|
||||
push(child);
|
||||
}
|
||||
@@ -276,14 +318,6 @@ impl Info {
|
||||
definition,
|
||||
arguments,
|
||||
} => {
|
||||
if arguments.len() > MAX_CLOSURE_NODES {
|
||||
return Err(AnalysisError::new(
|
||||
owner,
|
||||
field,
|
||||
"analysis_nodes",
|
||||
"Call argument count exceeds the stored closure limit.",
|
||||
));
|
||||
}
|
||||
info.calls.insert(definition.local());
|
||||
for child in arguments.values().rev() {
|
||||
push(child);
|
||||
@@ -297,14 +331,6 @@ impl Info {
|
||||
Expr::AsKind { value, .. } => push(value),
|
||||
_ => {}
|
||||
}
|
||||
if pending.len() > MAX_CLOSURE_NODES {
|
||||
return Err(AnalysisError::new(
|
||||
owner,
|
||||
field,
|
||||
"analysis_nodes",
|
||||
"Pending expression nodes exceed the stored closure limit.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
@@ -371,15 +397,18 @@ fn subject(
|
||||
owner: Id,
|
||||
layer: Layer,
|
||||
value: AnalysisSubject,
|
||||
budget: &mut Budget,
|
||||
) -> Result<(), AnalysisError> {
|
||||
let project = environment.project;
|
||||
budget.charge(owner, "subject", 1)?;
|
||||
let valid = match value {
|
||||
AnalysisSubject::Element(id) => project
|
||||
.elements
|
||||
.get(&id)
|
||||
.is_some_and(|element| element.layer() == layer && !element.kind().is_analysis()),
|
||||
AnalysisSubject::Relation(id) => {
|
||||
project.relations.contains_key(&id) && project.layer_of(id) == Some(layer)
|
||||
project.relations.contains_key(&id)
|
||||
&& relation_layer(project, id, owner, budget)? == Some(layer)
|
||||
}
|
||||
};
|
||||
if valid {
|
||||
@@ -393,6 +422,30 @@ fn subject(
|
||||
))
|
||||
}
|
||||
}
|
||||
/// Match Project::layer_of without unmetered relation-chain traversal before
|
||||
/// architectural validation has had a chance to reject malformed endpoints.
|
||||
fn relation_layer(
|
||||
project: &Project,
|
||||
id: Id,
|
||||
owner: Id,
|
||||
budget: &mut Budget,
|
||||
) -> Result<Option<Layer>, AnalysisError> {
|
||||
let mut current = id;
|
||||
let mut seen = BTreeSet::new();
|
||||
loop {
|
||||
budget.charge(owner, "subject", 1)?;
|
||||
if !seen.insert(current) {
|
||||
return Ok(None);
|
||||
}
|
||||
if let Some(element) = project.elements.get(¤t) {
|
||||
return Ok(Some(element.layer()));
|
||||
}
|
||||
let Some(relation) = project.relations.get(¤t) else {
|
||||
return Ok(None);
|
||||
};
|
||||
current = relation.source();
|
||||
}
|
||||
}
|
||||
fn declared(
|
||||
environment: &ResolvedAnalysis<'_>,
|
||||
owner: Id,
|
||||
@@ -420,6 +473,7 @@ fn check_bindings(
|
||||
let mut names = BTreeSet::new();
|
||||
for parameter in &calculation.parameters {
|
||||
budget.charge(*id, "parameters", 1)?;
|
||||
budget.charge(*id, "parameters", parameter.name.len() as u64)?;
|
||||
if !crate::valid_alias(¶meter.name) || !names.insert(¶meter.name) {
|
||||
return Err(AnalysisError::new(
|
||||
*id,
|
||||
@@ -441,12 +495,14 @@ fn check_bindings(
|
||||
let expected = declared(environment, *id, "result", &calculation.result)?;
|
||||
super::typecheck::binding(
|
||||
environment,
|
||||
*id,
|
||||
"body",
|
||||
BindingContext {
|
||||
owner: *id,
|
||||
field: "body",
|
||||
layer: None,
|
||||
parameters: ¶meters,
|
||||
},
|
||||
&calculation.body,
|
||||
&expected,
|
||||
None,
|
||||
¶meters,
|
||||
budget,
|
||||
)?;
|
||||
}
|
||||
@@ -461,7 +517,7 @@ fn check_bindings(
|
||||
display_unit,
|
||||
base,
|
||||
}) => {
|
||||
subject(environment, *id, layer, *target)?;
|
||||
subject(environment, *id, layer, *target, budget)?;
|
||||
let expected = declared(environment, *id, "value_type", value_type)?;
|
||||
if let Some(reference) = display_unit {
|
||||
let unit = environment.unit(*reference).ok_or_else(|| {
|
||||
@@ -490,12 +546,14 @@ fn check_bindings(
|
||||
}
|
||||
super::typecheck::binding(
|
||||
environment,
|
||||
*id,
|
||||
"base",
|
||||
BindingContext {
|
||||
owner: *id,
|
||||
field: "base",
|
||||
layer: Some(layer),
|
||||
parameters: &no_parameters,
|
||||
},
|
||||
base,
|
||||
&expected,
|
||||
Some(layer),
|
||||
&no_parameters,
|
||||
budget,
|
||||
)?;
|
||||
}
|
||||
@@ -505,7 +563,7 @@ fn check_bindings(
|
||||
..
|
||||
}) => {
|
||||
if let Some(target) = target {
|
||||
subject(environment, *id, layer, *target)?;
|
||||
subject(environment, *id, layer, *target, budget)?;
|
||||
}
|
||||
for (property, binding) in overrides {
|
||||
budget.charge(*id, "overrides", 1)?;
|
||||
@@ -537,12 +595,14 @@ fn check_bindings(
|
||||
let expected = declared(environment, *id, "overrides", value_type)?;
|
||||
super::typecheck::binding(
|
||||
environment,
|
||||
*id,
|
||||
"overrides",
|
||||
BindingContext {
|
||||
owner: *id,
|
||||
field: "overrides",
|
||||
layer: Some(layer),
|
||||
parameters: &no_parameters,
|
||||
},
|
||||
binding,
|
||||
&expected,
|
||||
Some(layer),
|
||||
&no_parameters,
|
||||
budget,
|
||||
)?;
|
||||
}
|
||||
@@ -553,7 +613,7 @@ fn check_bindings(
|
||||
predicate,
|
||||
}) => {
|
||||
if let Some(target) = target {
|
||||
subject(environment, *id, layer, *target)?;
|
||||
subject(environment, *id, layer, *target, budget)?;
|
||||
}
|
||||
if let Some(constraint) = documented_constraint
|
||||
&& !environment
|
||||
@@ -573,12 +633,14 @@ fn check_bindings(
|
||||
}
|
||||
super::typecheck::binding(
|
||||
environment,
|
||||
*id,
|
||||
"predicate",
|
||||
BindingContext {
|
||||
owner: *id,
|
||||
field: "predicate",
|
||||
layer: Some(layer),
|
||||
parameters: &no_parameters,
|
||||
},
|
||||
predicate,
|
||||
&ResolvedType::boolean(),
|
||||
Some(layer),
|
||||
&no_parameters,
|
||||
budget,
|
||||
)?;
|
||||
}
|
||||
@@ -665,7 +727,11 @@ fn closure(
|
||||
budget: &mut Budget,
|
||||
) -> Result<(), AnalysisError> {
|
||||
let mut nodes = base_nodes;
|
||||
let mut pending: Vec<_> = calls.into_iter().collect();
|
||||
let mut pending = Vec::new();
|
||||
for id in calls {
|
||||
budget.charge(owner, "closure", 1)?;
|
||||
pending.push(id);
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = pending.pop() {
|
||||
budget.charge(owner, "closure", 1)?;
|
||||
@@ -730,7 +796,7 @@ fn check_graphs(
|
||||
let mut contexts = vec![EvaluationContext::ModelBaseline];
|
||||
for id in index.cases.keys() {
|
||||
budget.charge(*id, "context", 1)?;
|
||||
if project.layer_of(*id) == Some(layer) {
|
||||
if project.elements[id].layer() == layer {
|
||||
contexts.push(EvaluationContext::Case(*id));
|
||||
}
|
||||
}
|
||||
@@ -744,7 +810,7 @@ fn check_graphs(
|
||||
let mut definitions = BTreeSet::new();
|
||||
for (id, base) in &index.properties {
|
||||
budget.charge(owner, "effective_properties", 1)?;
|
||||
if project.layer_of(*id) != Some(layer) {
|
||||
if project.elements[id].layer() != layer {
|
||||
continue;
|
||||
}
|
||||
let effective = match context {
|
||||
@@ -769,7 +835,7 @@ fn check_graphs(
|
||||
}
|
||||
for (id, info) in &index.checks {
|
||||
budget.charge(owner, "effective_checks", 1)?;
|
||||
if project.layer_of(*id) == Some(layer) {
|
||||
if project.elements[id].layer() == layer {
|
||||
budget.charge(owner, "effective_checks", info.calls.len() as u64)?;
|
||||
nodes = nodes.checked_add(info.nodes).ok_or_else(|| {
|
||||
AnalysisError::new(
|
||||
@@ -793,6 +859,14 @@ fn check_graphs(
|
||||
mod meter_tests {
|
||||
use super::*;
|
||||
|
||||
fn budget(limit: u64) -> Budget {
|
||||
Budget {
|
||||
limit,
|
||||
usage: AdmissionUsage::default(),
|
||||
exhausted: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_meter_exact_boundary_zero_charge_and_exhaustion_latch() {
|
||||
let owner = Id::nil();
|
||||
@@ -818,4 +892,187 @@ mod meter_tests {
|
||||
assert!(zero.charge(owner, "test", 1).is_err());
|
||||
assert!(zero.exhausted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_expression_children_are_charged_before_they_are_queued() {
|
||||
let owner = Id::nil();
|
||||
let width = 1024;
|
||||
for value in [
|
||||
Binding::Expression(Expr::List(vec![Expr::Boolean(true); width])),
|
||||
Binding::Expression(Expr::Call {
|
||||
definition: DefinitionRef::Local(Id::from_u128(1)),
|
||||
arguments: (0..width)
|
||||
.map(|id| (ParameterId(Id::from_u128(id as u128)), Expr::Boolean(true)))
|
||||
.collect(),
|
||||
}),
|
||||
] {
|
||||
let mut short = budget(width as u64);
|
||||
let failure = match Info::read(owner, "probe", &value, &mut short) {
|
||||
Ok(_) => panic!("Child queue must be precharged"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(failure.code, "analysis_limit");
|
||||
assert_eq!(short.usage.static_events, 1);
|
||||
assert_eq!(short.usage.stored_expression_nodes, 1);
|
||||
assert!(short.exhausted);
|
||||
let mut exact = budget((2 * width + 1) as u64);
|
||||
let info = Info::read(owner, "probe", &value, &mut exact).unwrap();
|
||||
assert_eq!(info.nodes, width + 1);
|
||||
assert_eq!(exact.usage.static_events, (2 * width + 1) as u64);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_key_scan_and_argument_lookup_have_separate_precharges() {
|
||||
let mut project = Project::empty();
|
||||
let calculation = Id::from_u128(1);
|
||||
let parameters: Vec<_> = (0..64)
|
||||
.map(|n| Parameter {
|
||||
id: ParameterId(Id::from_u128(100 + n)),
|
||||
name: format!("argument_{n}"),
|
||||
value_type: AnalysisType::Scalar(ScalarType::Boolean),
|
||||
})
|
||||
.collect();
|
||||
let mut arguments: BTreeMap<_, _> = parameters
|
||||
.iter()
|
||||
.map(|parameter| (parameter.id, Expr::Boolean(true)))
|
||||
.collect();
|
||||
project.definitions.insert(
|
||||
calculation,
|
||||
Definition {
|
||||
id: calculation,
|
||||
alias: "call_probe".into(),
|
||||
name: "Call probe".into(),
|
||||
description: String::new(),
|
||||
body: DefinitionBody::Calculation(CalculationDefinition {
|
||||
parameters,
|
||||
result: AnalysisType::Scalar(ScalarType::Boolean),
|
||||
body: Binding::Unbound,
|
||||
}),
|
||||
},
|
||||
);
|
||||
let environment = admit(&project, AdmissionLimits::REFERENCE).unwrap();
|
||||
let missing = ParameterId(Id::from_u128(163));
|
||||
arguments.remove(&missing);
|
||||
arguments.insert(ParameterId(Id::from_u128(999)), Expr::Boolean(true));
|
||||
let mut value = Binding::Expression(Expr::Call {
|
||||
definition: DefinitionRef::Local(calculation),
|
||||
arguments,
|
||||
});
|
||||
let no_parameters = BTreeMap::new();
|
||||
let check = |value: &Binding, budget: &mut Budget| {
|
||||
super::super::typecheck::binding(
|
||||
&environment,
|
||||
BindingContext {
|
||||
owner: project.id,
|
||||
field: "probe",
|
||||
layer: None,
|
||||
parameters: &no_parameters,
|
||||
},
|
||||
value,
|
||||
&ResolvedType::boolean(),
|
||||
budget,
|
||||
)
|
||||
};
|
||||
let mut short = budget(64);
|
||||
assert_eq!(
|
||||
check(&value, &mut short).unwrap_err().code,
|
||||
"analysis_limit"
|
||||
);
|
||||
assert_eq!(short.usage.static_events, 1);
|
||||
let mut keys_only = budget(65);
|
||||
assert_eq!(
|
||||
check(&value, &mut keys_only).unwrap_err().code,
|
||||
"analysis_arguments"
|
||||
);
|
||||
assert_eq!(keys_only.usage.static_events, 65);
|
||||
let Binding::Expression(Expr::Call { arguments, .. }) = &mut value else {
|
||||
panic!()
|
||||
};
|
||||
arguments.remove(&ParameterId(Id::from_u128(999)));
|
||||
arguments.insert(missing, Expr::Boolean(true));
|
||||
let mut exact = budget(193); // One Call + 64 keys + 64 lookups + 64 leaf visits.
|
||||
check(&value, &mut exact).unwrap();
|
||||
assert_eq!(exact.usage.static_events, 193);
|
||||
let mut one_short = budget(192);
|
||||
assert_eq!(
|
||||
check(&value, &mut one_short).unwrap_err().code,
|
||||
"analysis_limit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relation_subject_resolution_charges_each_hop_and_bounds_cycles() {
|
||||
let mut project = Project::empty();
|
||||
let element = Id::from_u128(2000);
|
||||
project.elements.insert(
|
||||
element,
|
||||
crate::Element {
|
||||
id: element,
|
||||
alias: "subject".into(),
|
||||
name: "Subject".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: crate::ElementBody::Oa(crate::OaBody::Activity),
|
||||
},
|
||||
);
|
||||
let first = Id::from_u128(3000);
|
||||
let last = Id::from_u128(3063);
|
||||
// These deliberately malformed relation endpoints reach analysis before
|
||||
// architecture admission; no valid architectural chain is claimed.
|
||||
for n in 3000..3064 {
|
||||
let id = Id::from_u128(n);
|
||||
project.relations.insert(
|
||||
id,
|
||||
crate::Relation {
|
||||
id,
|
||||
alias: format!("relation_{n}"),
|
||||
name: "Probe".into(),
|
||||
description: String::new(),
|
||||
body: crate::RelationBody::Allocation {
|
||||
source: if id == last {
|
||||
element
|
||||
} else {
|
||||
Id::from_u128(n + 1)
|
||||
},
|
||||
target: element,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut short = budget(64);
|
||||
assert_eq!(
|
||||
relation_layer(&project, first, project.id, &mut short)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"analysis_limit"
|
||||
);
|
||||
assert_eq!(short.usage.static_events, 64);
|
||||
let mut exact = budget(65);
|
||||
assert_eq!(
|
||||
relation_layer(&project, first, project.id, &mut exact).unwrap(),
|
||||
Some(Layer::Oa)
|
||||
);
|
||||
assert_eq!(exact.usage.static_events, 65);
|
||||
project.relations.get_mut(&last).unwrap().body = crate::RelationBody::Allocation {
|
||||
source: first,
|
||||
target: element,
|
||||
};
|
||||
let mut cycle = budget(65);
|
||||
assert_eq!(
|
||||
relation_layer(&project, first, project.id, &mut cycle).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(cycle.usage.static_events, 65);
|
||||
project.relations.get_mut(&last).unwrap().body = crate::RelationBody::Allocation {
|
||||
source: Id::from_u128(9999),
|
||||
target: element,
|
||||
};
|
||||
let mut missing = budget(65);
|
||||
assert_eq!(
|
||||
relation_layer(&project, first, project.id, &mut missing).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(missing.usage.static_events, 65);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,32 @@ fn add_calculation(project: &mut Project, n: u128, result: AnalysisType, body: E
|
||||
id
|
||||
}
|
||||
|
||||
fn add_identity_calculation(
|
||||
project: &mut Project,
|
||||
n: u128,
|
||||
value_type: AnalysisType,
|
||||
) -> (Id, ParameterId) {
|
||||
let id = Id::from_u128(n);
|
||||
let parameter = ParameterId(Id::from_u128(n + 10_000));
|
||||
project.definitions.insert(
|
||||
id,
|
||||
definition(
|
||||
id,
|
||||
&format!("identity_{n}"),
|
||||
DefinitionBody::Calculation(CalculationDefinition {
|
||||
parameters: vec![Parameter {
|
||||
id: parameter,
|
||||
name: "value".into(),
|
||||
value_type: value_type.clone(),
|
||||
}],
|
||||
result: value_type,
|
||||
body: expression(Expr::Parameter(parameter)),
|
||||
}),
|
||||
),
|
||||
);
|
||||
(id, parameter)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_real_model_roundtrips_and_exposes_explicit_effective_bindings() {
|
||||
let f = fixture();
|
||||
@@ -611,6 +637,34 @@ fn calculations_are_pure_parameters_are_scoped_and_calls_bind_exactly() {
|
||||
calculation.parameters[0].name = "first_duration".into();
|
||||
renamed = execute_batch(&renamed, &[Command::EditDefinition(f.combine, revised)]).unwrap();
|
||||
admit(&renamed, AdmissionLimits::REFERENCE).unwrap();
|
||||
|
||||
// Distinct errors make argument order observable. Parameter a sorts before
|
||||
// b by UUID; reversing declaration order must reverse the first error.
|
||||
let mut ordered = f.project.clone();
|
||||
*base_mut(&mut ordered, f.total) = expression(Expr::Call {
|
||||
definition: reference(f.combine),
|
||||
arguments: BTreeMap::from([
|
||||
(f.a, Expr::Parameter(f.a)),
|
||||
(f.b, Expr::Property(f.respond)),
|
||||
]),
|
||||
});
|
||||
assert_eq!(reject(&ordered).code, "analysis_parameter");
|
||||
calculation_mut(&mut ordered, f.combine)
|
||||
.parameters
|
||||
.swap(0, 1);
|
||||
assert_eq!(reject(&ordered).code, "analysis_property");
|
||||
*base_mut(&mut ordered, f.total) = expression(binary(
|
||||
BinaryOp::Add,
|
||||
Expr::Property(f.respond),
|
||||
Expr::Parameter(f.a),
|
||||
));
|
||||
assert_eq!(reject(&ordered).code, "analysis_property");
|
||||
*base_mut(&mut ordered, f.total) = expression(binary(
|
||||
BinaryOp::Add,
|
||||
Expr::Parameter(f.a),
|
||||
Expr::Property(f.respond),
|
||||
));
|
||||
assert_eq!(reject(&ordered).code, "analysis_parameter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -851,6 +905,7 @@ fn logical_depth_and_call_depth_have_exact_boundaries() {
|
||||
};
|
||||
}
|
||||
let deep = add_calculation(&mut f.project, 7000, scalar_number(), nested);
|
||||
eprintln!("analysis depth gate: admitting 128 unary nodes");
|
||||
admit(&f.project, AdmissionLimits::REFERENCE).unwrap();
|
||||
let old = std::mem::replace(
|
||||
&mut calculation_mut(&mut f.project, deep).body,
|
||||
@@ -863,9 +918,74 @@ fn logical_depth_and_call_depth_have_exact_boundaries() {
|
||||
op: UnaryOp::Negate,
|
||||
operand: Box::new(old),
|
||||
});
|
||||
eprintln!("analysis depth gate: rejecting 129 unary nodes");
|
||||
assert_eq!(reject(&f.project).code, "analysis_depth");
|
||||
// Only 129 nodes here: no hostile unbounded recursive teardown probe.
|
||||
f.project.definitions.remove(&deep);
|
||||
|
||||
let (identity, parameter) = add_identity_calculation(&mut f.project, 7001, scalar_number());
|
||||
let mut mixed = number(1);
|
||||
for level in 1..128 {
|
||||
mixed = match level % 5 {
|
||||
0 => Expr::Unary {
|
||||
op: UnaryOp::Absolute,
|
||||
operand: Box::new(mixed),
|
||||
},
|
||||
1 => binary(BinaryOp::Add, mixed, number(0)),
|
||||
2 => Expr::If {
|
||||
condition: Box::new(Expr::Boolean(true)),
|
||||
when_true: Box::new(mixed),
|
||||
when_false: Box::new(number(0)),
|
||||
},
|
||||
3 => Expr::Call {
|
||||
definition: reference(identity),
|
||||
arguments: BTreeMap::from([(parameter, mixed)]),
|
||||
},
|
||||
_ => Expr::Power {
|
||||
base: Box::new(mixed),
|
||||
exponent: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
let mixed_id = add_calculation(&mut f.project, 7002, scalar_number(), mixed);
|
||||
eprintln!("analysis depth gate: admitting mixed Binary/If/Call/Power/Unary depth 128");
|
||||
admit(&f.project, AdmissionLimits::REFERENCE).unwrap();
|
||||
let Binding::Expression(mixed) = std::mem::replace(
|
||||
&mut calculation_mut(&mut f.project, mixed_id).body,
|
||||
Binding::Unbound,
|
||||
) else {
|
||||
panic!()
|
||||
};
|
||||
calculation_mut(&mut f.project, mixed_id).body = expression(Expr::Unary {
|
||||
op: UnaryOp::Negate,
|
||||
operand: Box::new(mixed),
|
||||
});
|
||||
eprintln!("analysis depth gate: rejecting mixed depth 129");
|
||||
assert_eq!(reject(&f.project).code, "analysis_depth");
|
||||
f.project.definitions.remove(&mixed_id);
|
||||
|
||||
let list_type = AnalysisType::List(ScalarType::Number);
|
||||
let (list_identity, list_parameter) =
|
||||
add_identity_calculation(&mut f.project, 7003, list_type.clone());
|
||||
let mut list = Expr::List(vec![]);
|
||||
for level in 1..128 {
|
||||
list = if level % 2 == 0 {
|
||||
Expr::If {
|
||||
condition: Box::new(Expr::Boolean(false)),
|
||||
when_true: Box::new(list),
|
||||
when_false: Box::new(Expr::List(vec![])),
|
||||
}
|
||||
} else {
|
||||
Expr::Call {
|
||||
definition: reference(list_identity),
|
||||
arguments: BTreeMap::from([(list_parameter, list)]),
|
||||
}
|
||||
};
|
||||
}
|
||||
add_calculation(&mut f.project, 7004, list_type, list);
|
||||
eprintln!("analysis depth gate: admitting contextual empty lists through If/Call depth 128");
|
||||
admit(&f.project, AdmissionLimits::REFERENCE).unwrap();
|
||||
|
||||
let mut previous = add_calculation(&mut f.project, 7100, scalar_number(), number(1));
|
||||
for n in 7101..7132 {
|
||||
previous = add_calculation(
|
||||
@@ -878,6 +998,7 @@ fn logical_depth_and_call_depth_have_exact_boundaries() {
|
||||
},
|
||||
);
|
||||
}
|
||||
eprintln!("analysis depth gate: admitting 32-definition call graph");
|
||||
admit(&f.project, AdmissionLimits::REFERENCE).unwrap();
|
||||
add_calculation(
|
||||
&mut f.project,
|
||||
@@ -888,6 +1009,7 @@ fn logical_depth_and_call_depth_have_exact_boundaries() {
|
||||
arguments: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
eprintln!("analysis depth gate: rejecting 33-definition call graph");
|
||||
assert_eq!(reject(&f.project).code, "analysis_call_depth");
|
||||
}
|
||||
|
||||
@@ -992,6 +1114,36 @@ fn static_work_is_lowerable_deterministic_and_aggregated_across_cases() {
|
||||
.admission_usage();
|
||||
assert_eq!(first, second);
|
||||
assert!(first.static_events > used);
|
||||
let mut described = f.project.clone();
|
||||
described
|
||||
.definitions
|
||||
.get_mut(&f.duration)
|
||||
.unwrap()
|
||||
.description = "x".repeat(1024);
|
||||
assert_eq!(
|
||||
admit(&described, AdmissionLimits::REFERENCE)
|
||||
.unwrap()
|
||||
.admission_usage()
|
||||
.static_events,
|
||||
used + 1024,
|
||||
);
|
||||
// Duration is the first definition in UUID order. The forbidden final byte
|
||||
// must not be inspected before its complete text span has been charged.
|
||||
described
|
||||
.definitions
|
||||
.get_mut(&f.duration)
|
||||
.unwrap()
|
||||
.description = format!("{}\0", "x".repeat(1023));
|
||||
let text_limit = AdmissionLimits {
|
||||
max_static_events: 2 + described.definitions[&f.duration].name.len() as u64 + 1023,
|
||||
};
|
||||
let failure = match admit(&described, text_limit) {
|
||||
Ok(_) => panic!("Text inspection must be precharged"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(failure.code, "analysis_limit");
|
||||
assert_eq!(failure.object, f.duration);
|
||||
assert_eq!(failure.field, "description");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
//! Static expression checking shares numeric operator algebra with execution.
|
||||
use super::admission::Budget;
|
||||
use super::*;
|
||||
use crate::analysis_numeric::{NumericError, NumericType};
|
||||
use crate::analysis_numeric::{NumericError, NumericType, ResolvedKind};
|
||||
use crate::{Id, Layer, SharedBody};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(super) struct BindingContext<'a> {
|
||||
pub owner: Id,
|
||||
pub field: &'a str,
|
||||
pub layer: Option<Layer>,
|
||||
pub parameters: &'a BTreeMap<ParameterId, ResolvedType>,
|
||||
}
|
||||
|
||||
pub(super) fn binding(
|
||||
environment: &ResolvedAnalysis<'_>,
|
||||
owner: Id,
|
||||
field: &str,
|
||||
context: BindingContext<'_>,
|
||||
value: &Binding,
|
||||
expected: &ResolvedType,
|
||||
layer: Option<Layer>,
|
||||
parameters: &BTreeMap<ParameterId, ResolvedType>,
|
||||
budget: &mut Budget,
|
||||
) -> Result<(), AnalysisError> {
|
||||
if let Binding::Expression(expression) = value {
|
||||
let mut checker = Checker {
|
||||
environment,
|
||||
owner,
|
||||
field,
|
||||
layer,
|
||||
parameters,
|
||||
owner: context.owner,
|
||||
field: context.field,
|
||||
layer: context.layer,
|
||||
parameters: context.parameters,
|
||||
budget,
|
||||
};
|
||||
checker.expression(expression, Some(expected), 1)?;
|
||||
checker.expression(expression, Some(expected))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -37,7 +41,42 @@ struct Checker<'a, 'p, 'b> {
|
||||
parameters: &'a BTreeMap<ParameterId, ResolvedType>,
|
||||
budget: &'b mut Budget,
|
||||
}
|
||||
impl Checker<'_, '_, '_> {
|
||||
|
||||
/// One suspended expression per logical depth. No Expr is owned or cloned by
|
||||
/// a frame, and every stored type/nominal handle has fixed, nonrecursive size.
|
||||
struct Frame<'e, 'p> {
|
||||
expression: &'e Expr,
|
||||
expected: Option<ResolvedType>,
|
||||
state: State<'p>,
|
||||
}
|
||||
|
||||
enum State<'p> {
|
||||
Enter,
|
||||
Unary,
|
||||
BinaryLeft,
|
||||
BinaryRight(ResolvedType),
|
||||
IfCondition,
|
||||
IfTrue,
|
||||
IfFalse(ResolvedType),
|
||||
List {
|
||||
next: usize,
|
||||
},
|
||||
Call {
|
||||
calculation: &'p CalculationDefinition,
|
||||
next: usize,
|
||||
},
|
||||
SumInitial,
|
||||
SumValues(ResolvedType),
|
||||
Power,
|
||||
AsKind(ResolvedKind<KindHandle>),
|
||||
}
|
||||
|
||||
enum Step<'e> {
|
||||
Child(&'e Expr, Option<ResolvedType>),
|
||||
Complete(ResolvedType),
|
||||
}
|
||||
|
||||
impl<'p> Checker<'_, 'p, '_> {
|
||||
fn error(&self, code: &str, message: impl Into<String>) -> AnalysisError {
|
||||
AnalysisError::new(self.owner, self.field, code, message)
|
||||
}
|
||||
@@ -55,21 +94,165 @@ impl Checker<'_, '_, '_> {
|
||||
.map(ResolvedType::numeric)
|
||||
.map_err(|error| self.error("analysis_type", error.to_string()))
|
||||
}
|
||||
|
||||
/// Explicit continuations replace Rust recursion. Width never expands the
|
||||
/// frame stack: lists and calls schedule only their next ordered child.
|
||||
/// At most 128 frames and one returned scalar/list type are retained. The
|
||||
/// per-node/per-argument meter charges remain identical to recursive entry;
|
||||
/// continuation steps do constant bounded work within those charges.
|
||||
fn expression(
|
||||
&mut self,
|
||||
expression: &Expr,
|
||||
expected: Option<&ResolvedType>,
|
||||
depth: usize,
|
||||
) -> Result<ResolvedType, AnalysisError> {
|
||||
self.budget.charge(self.owner, self.field, 1)?;
|
||||
if depth > MAX_EXPRESSION_DEPTH {
|
||||
return Err(self.error(
|
||||
"analysis_depth",
|
||||
"Expression exceeds the logical depth limit.",
|
||||
));
|
||||
let mut frames = Vec::with_capacity(MAX_EXPRESSION_DEPTH);
|
||||
frames.push(Frame {
|
||||
expression,
|
||||
expected: expected.cloned(),
|
||||
state: State::Enter,
|
||||
});
|
||||
let mut returned = None;
|
||||
loop {
|
||||
let frame = frames.last_mut().expect("active expression frame");
|
||||
match self.advance(frame, returned.take())? {
|
||||
Step::Child(expression, expected) => {
|
||||
if frames.len() == MAX_EXPRESSION_DEPTH {
|
||||
// Preserve entry charging/error precedence without ever
|
||||
// allocating the rejected 129th frame.
|
||||
self.budget.charge(self.owner, self.field, 1)?;
|
||||
return Err(self.error(
|
||||
"analysis_depth",
|
||||
"Expression exceeds the logical depth limit.",
|
||||
));
|
||||
}
|
||||
frames.push(Frame {
|
||||
expression,
|
||||
expected,
|
||||
state: State::Enter,
|
||||
});
|
||||
}
|
||||
Step::Complete(actual) => {
|
||||
let frame = frames.pop().expect("completed expression frame");
|
||||
let expected = frame.expected.as_ref();
|
||||
if expected.is_some_and(|expected| expected != &actual) {
|
||||
return Err(self.error(
|
||||
"analysis_type",
|
||||
format!("Expected {expected:?}; expression has {actual:?}."),
|
||||
));
|
||||
}
|
||||
if frames.is_empty() {
|
||||
return Ok(actual);
|
||||
}
|
||||
returned = Some(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
let next = depth + 1;
|
||||
let actual = match expression {
|
||||
}
|
||||
|
||||
fn advance<'e>(
|
||||
&mut self,
|
||||
frame: &mut Frame<'e, 'p>,
|
||||
returned: Option<ResolvedType>,
|
||||
) -> Result<Step<'e>, AnalysisError> {
|
||||
let state = std::mem::replace(&mut frame.state, State::Enter);
|
||||
if let State::Enter = state {
|
||||
self.budget.charge(self.owner, self.field, 1)?;
|
||||
return self.enter(frame);
|
||||
}
|
||||
let child = returned.expect("continuation follows a completed child");
|
||||
match state {
|
||||
State::Enter => unreachable!("handled before consuming child"),
|
||||
State::Unary => {
|
||||
let Expr::Unary { op, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
Ok(Step::Complete(match op {
|
||||
UnaryOp::Not => ResolvedType::boolean(),
|
||||
UnaryOp::Negate | UnaryOp::Absolute => {
|
||||
self.operation(self.numeric(child)?.unary_result())?
|
||||
}
|
||||
}))
|
||||
}
|
||||
State::BinaryLeft => {
|
||||
let Expr::Binary { right, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
frame.state = State::BinaryRight(child);
|
||||
Ok(Step::Child(right, None))
|
||||
}
|
||||
State::BinaryRight(left) => {
|
||||
let Expr::Binary { op, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
Ok(Step::Complete(self.binary(*op, left, child)?))
|
||||
}
|
||||
State::IfCondition => {
|
||||
let Expr::If { when_true, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
frame.state = State::IfTrue;
|
||||
Ok(Step::Child(when_true, frame.expected.clone()))
|
||||
}
|
||||
State::IfTrue => {
|
||||
let Expr::If { when_false, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
frame.state = State::IfFalse(child.clone());
|
||||
Ok(Step::Child(when_false, Some(child)))
|
||||
}
|
||||
State::IfFalse(value) => Ok(Step::Complete(value)),
|
||||
State::List { next } => {
|
||||
let ResolvedType::Scalar(scalar) = child else {
|
||||
return Err(self.error(
|
||||
"analysis_list",
|
||||
"Lists are flat; nested lists are not admitted.",
|
||||
));
|
||||
};
|
||||
let Expr::List(items) = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
if let Some(item) = items.get(next) {
|
||||
frame.state = State::List { next: next + 1 };
|
||||
Ok(Step::Child(item, Some(ResolvedType::Scalar(scalar))))
|
||||
} else {
|
||||
Ok(Step::Complete(ResolvedType::List(scalar)))
|
||||
}
|
||||
}
|
||||
State::Call { calculation, next } => self.call_argument(frame, calculation, next),
|
||||
State::SumInitial => {
|
||||
let numeric = self.numeric(child.clone())?;
|
||||
self.operation(numeric.unary_result())?;
|
||||
let ResolvedType::Scalar(scalar) = &child else {
|
||||
unreachable!()
|
||||
};
|
||||
let expected = ResolvedType::List(scalar.clone());
|
||||
let Expr::Sum { values, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
frame.state = State::SumValues(child);
|
||||
Ok(Step::Child(values, Some(expected)))
|
||||
}
|
||||
State::SumValues(initial) => {
|
||||
let numeric = self.numeric(initial.clone())?;
|
||||
self.operation(numeric.additive_result(&numeric, false))?;
|
||||
Ok(Step::Complete(initial))
|
||||
}
|
||||
State::Power => {
|
||||
let Expr::Power { exponent, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
Ok(Step::Complete(
|
||||
self.operation(self.numeric(child)?.power_result(*exponent))?,
|
||||
))
|
||||
}
|
||||
State::AsKind(kind) => Ok(Step::Complete(
|
||||
self.operation(self.numeric(child)?.assertion_result(&kind))?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn enter<'e>(&mut self, frame: &mut Frame<'e, 'p>) -> Result<Step<'e>, AnalysisError> {
|
||||
let actual = match frame.expression {
|
||||
Expr::Boolean(_) => ResolvedType::boolean(),
|
||||
Expr::Number(_) => ResolvedType::numeric(NumericType::Number),
|
||||
Expr::Quantity { unit, .. } => {
|
||||
@@ -121,21 +304,13 @@ impl Checker<'_, '_, '_> {
|
||||
self.error("analysis_list", "List exceeds the flat-list length limit.")
|
||||
);
|
||||
}
|
||||
let mut scalar = match expected {
|
||||
let scalar = match &frame.expected {
|
||||
Some(ResolvedType::List(value)) => Some(value.clone()),
|
||||
_ => None,
|
||||
};
|
||||
for item in items {
|
||||
let wanted = scalar.clone().map(ResolvedType::Scalar);
|
||||
let ResolvedType::Scalar(found) =
|
||||
self.expression(item, wanted.as_ref(), next)?
|
||||
else {
|
||||
return Err(self.error(
|
||||
"analysis_list",
|
||||
"Lists are flat; nested lists are not admitted.",
|
||||
));
|
||||
};
|
||||
scalar = Some(found);
|
||||
if let Some(item) = items.first() {
|
||||
frame.state = State::List { next: 1 };
|
||||
return Ok(Step::Child(item, scalar.map(ResolvedType::Scalar)));
|
||||
}
|
||||
ResolvedType::List(scalar.ok_or_else(|| {
|
||||
self.error(
|
||||
@@ -144,63 +319,18 @@ impl Checker<'_, '_, '_> {
|
||||
)
|
||||
})?)
|
||||
}
|
||||
Expr::Unary { op, operand } => match op {
|
||||
UnaryOp::Not => {
|
||||
self.expression(operand, Some(&ResolvedType::boolean()), next)?;
|
||||
ResolvedType::boolean()
|
||||
}
|
||||
UnaryOp::Negate | UnaryOp::Absolute => {
|
||||
let operand = self.expression(operand, None, next)?;
|
||||
self.operation(self.numeric(operand)?.unary_result())?
|
||||
}
|
||||
},
|
||||
Expr::Binary { op, left, right } => {
|
||||
let left = self.expression(left, None, next)?;
|
||||
let right = self.expression(right, None, next)?;
|
||||
match op {
|
||||
BinaryOp::And | BinaryOp::Or => {
|
||||
if left != ResolvedType::boolean() || right != ResolvedType::boolean() {
|
||||
return Err(
|
||||
self.error("analysis_type", "Boolean operands are required.")
|
||||
);
|
||||
}
|
||||
ResolvedType::boolean()
|
||||
}
|
||||
BinaryOp::Equal | BinaryOp::NotEqual
|
||||
if left == ResolvedType::boolean() && right == ResolvedType::boolean() =>
|
||||
{
|
||||
ResolvedType::boolean()
|
||||
}
|
||||
BinaryOp::Equal
|
||||
| BinaryOp::NotEqual
|
||||
| BinaryOp::Less
|
||||
| BinaryOp::LessOrEqual
|
||||
| BinaryOp::Greater
|
||||
| BinaryOp::GreaterOrEqual => {
|
||||
self.numeric(left)?
|
||||
.comparison_compatible(&self.numeric(right)?)
|
||||
.map_err(|error| self.error("analysis_type", error.to_string()))?;
|
||||
ResolvedType::boolean()
|
||||
}
|
||||
BinaryOp::Add | BinaryOp::Subtract => self.operation(
|
||||
self.numeric(left)?
|
||||
.additive_result(&self.numeric(right)?, *op == BinaryOp::Subtract),
|
||||
)?,
|
||||
BinaryOp::Multiply | BinaryOp::Divide => self.operation(
|
||||
self.numeric(left)?
|
||||
.multiplicative_result(&self.numeric(right)?, *op == BinaryOp::Divide),
|
||||
)?,
|
||||
}
|
||||
Expr::Unary { op, operand } => {
|
||||
frame.state = State::Unary;
|
||||
let expected = (*op == UnaryOp::Not).then(ResolvedType::boolean);
|
||||
return Ok(Step::Child(operand, expected));
|
||||
}
|
||||
Expr::If {
|
||||
condition,
|
||||
when_true,
|
||||
when_false,
|
||||
} => {
|
||||
self.expression(condition, Some(&ResolvedType::boolean()), next)?;
|
||||
let true_type = self.expression(when_true, expected, next)?;
|
||||
self.expression(when_false, Some(&true_type), next)?;
|
||||
true_type
|
||||
Expr::Binary { left, .. } => {
|
||||
frame.state = State::BinaryLeft;
|
||||
return Ok(Step::Child(left, None));
|
||||
}
|
||||
Expr::If { condition, .. } => {
|
||||
frame.state = State::IfCondition;
|
||||
return Ok(Step::Child(condition, Some(ResolvedType::boolean())));
|
||||
}
|
||||
Expr::Call {
|
||||
definition,
|
||||
@@ -212,44 +342,28 @@ impl Checker<'_, '_, '_> {
|
||||
"Call must reference a calculation definition.",
|
||||
)
|
||||
})?;
|
||||
if arguments.len() != calculation.parameters.len()
|
||||
|| calculation
|
||||
.parameters
|
||||
.iter()
|
||||
.any(|parameter| !arguments.contains_key(¶meter.id))
|
||||
if arguments.len() != calculation.parameters.len() {
|
||||
return Err(self.error("analysis_arguments", "Call arguments must bind every declared parameter exactly once, by its scoped identity."));
|
||||
}
|
||||
// Charge the complete key scan before any parameter lookup.
|
||||
self.budget
|
||||
.charge(self.owner, self.field, calculation.parameters.len() as u64)?;
|
||||
if calculation
|
||||
.parameters
|
||||
.iter()
|
||||
.any(|parameter| !arguments.contains_key(¶meter.id))
|
||||
{
|
||||
return Err(self.error("analysis_arguments", "Call arguments must bind every declared parameter exactly once, by its scoped identity."));
|
||||
}
|
||||
// Declaration order, not UUID map order, is the evaluation contract.
|
||||
for parameter in &calculation.parameters {
|
||||
let expected = self
|
||||
.environment
|
||||
.declared_type(¶meter.value_type)
|
||||
.ok_or_else(|| {
|
||||
self.error("analysis_kind", "Parameter kind does not resolve.")
|
||||
})?;
|
||||
self.expression(&arguments[¶meter.id], Some(&expected), next)?;
|
||||
}
|
||||
self.environment
|
||||
.declared_type(&calculation.result)
|
||||
.ok_or_else(|| {
|
||||
self.error("analysis_kind", "Calculation result kind does not resolve.")
|
||||
})?
|
||||
return self.call_argument(frame, calculation, 0);
|
||||
}
|
||||
Expr::Sum { values, initial } => {
|
||||
let initial = self.expression(initial, None, next)?;
|
||||
let numeric = self.numeric(initial.clone())?;
|
||||
self.operation(numeric.unary_result())?;
|
||||
let ResolvedType::Scalar(scalar) = &initial else {
|
||||
unreachable!()
|
||||
};
|
||||
self.expression(values, Some(&ResolvedType::List(scalar.clone())), next)?;
|
||||
self.operation(numeric.additive_result(&numeric, false))?;
|
||||
initial
|
||||
Expr::Sum { initial, .. } => {
|
||||
frame.state = State::SumInitial;
|
||||
return Ok(Step::Child(initial, None));
|
||||
}
|
||||
Expr::Power { base, exponent } => {
|
||||
let base = self.expression(base, None, next)?;
|
||||
self.operation(self.numeric(base)?.power_result(*exponent))?
|
||||
Expr::Power { base, .. } => {
|
||||
frame.state = State::Power;
|
||||
return Ok(Step::Child(base, None));
|
||||
}
|
||||
Expr::AsKind { kind, value } => {
|
||||
let kind = self
|
||||
@@ -262,16 +376,83 @@ impl Checker<'_, '_, '_> {
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
let value = self.expression(value, None, next)?;
|
||||
self.operation(self.numeric(value)?.assertion_result(&kind))?
|
||||
frame.state = State::AsKind(kind);
|
||||
return Ok(Step::Child(value, None));
|
||||
}
|
||||
};
|
||||
if expected.is_some_and(|expected| expected != &actual) {
|
||||
return Err(self.error(
|
||||
"analysis_type",
|
||||
format!("Expected {expected:?}; expression has {actual:?}."),
|
||||
));
|
||||
Ok(Step::Complete(actual))
|
||||
}
|
||||
|
||||
/// Arguments follow parameter declaration order, not UUID map order. Retain
|
||||
/// the resolved calculation by reference and schedule one argument at a time.
|
||||
fn call_argument<'e>(
|
||||
&mut self,
|
||||
frame: &mut Frame<'e, 'p>,
|
||||
calculation: &'p CalculationDefinition,
|
||||
next: usize,
|
||||
) -> Result<Step<'e>, AnalysisError> {
|
||||
if let Some(parameter) = calculation.parameters.get(next) {
|
||||
self.budget.charge(self.owner, self.field, 1)?;
|
||||
let expected = self
|
||||
.environment
|
||||
.declared_type(¶meter.value_type)
|
||||
.ok_or_else(|| self.error("analysis_kind", "Parameter kind does not resolve."))?;
|
||||
let Expr::Call { arguments, .. } = frame.expression else {
|
||||
unreachable!()
|
||||
};
|
||||
frame.state = State::Call {
|
||||
calculation,
|
||||
next: next + 1,
|
||||
};
|
||||
Ok(Step::Child(&arguments[¶meter.id], Some(expected)))
|
||||
} else {
|
||||
let result = self
|
||||
.environment
|
||||
.declared_type(&calculation.result)
|
||||
.ok_or_else(|| {
|
||||
self.error("analysis_kind", "Calculation result kind does not resolve.")
|
||||
})?;
|
||||
Ok(Step::Complete(result))
|
||||
}
|
||||
}
|
||||
|
||||
fn binary(
|
||||
&self,
|
||||
op: BinaryOp,
|
||||
left: ResolvedType,
|
||||
right: ResolvedType,
|
||||
) -> Result<ResolvedType, AnalysisError> {
|
||||
match op {
|
||||
BinaryOp::And | BinaryOp::Or => {
|
||||
if left != ResolvedType::boolean() || right != ResolvedType::boolean() {
|
||||
return Err(self.error("analysis_type", "Boolean operands are required."));
|
||||
}
|
||||
Ok(ResolvedType::boolean())
|
||||
}
|
||||
BinaryOp::Equal | BinaryOp::NotEqual
|
||||
if left == ResolvedType::boolean() && right == ResolvedType::boolean() =>
|
||||
{
|
||||
Ok(ResolvedType::boolean())
|
||||
}
|
||||
BinaryOp::Equal
|
||||
| BinaryOp::NotEqual
|
||||
| BinaryOp::Less
|
||||
| BinaryOp::LessOrEqual
|
||||
| BinaryOp::Greater
|
||||
| BinaryOp::GreaterOrEqual => {
|
||||
self.numeric(left)?
|
||||
.comparison_compatible(&self.numeric(right)?)
|
||||
.map_err(|error| self.error("analysis_type", error.to_string()))?;
|
||||
Ok(ResolvedType::boolean())
|
||||
}
|
||||
BinaryOp::Add | BinaryOp::Subtract => self.operation(
|
||||
self.numeric(left)?
|
||||
.additive_result(&self.numeric(right)?, op == BinaryOp::Subtract),
|
||||
),
|
||||
BinaryOp::Multiply | BinaryOp::Divide => self.operation(
|
||||
self.numeric(left)?
|
||||
.multiplicative_result(&self.numeric(right)?, op == BinaryOp::Divide),
|
||||
),
|
||||
}
|
||||
Ok(actual)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user