Stage bounded source8 typed resolution and expression lowering
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
//! Unresolved typed source8 syntax, staged for the complete source adapter.
|
||||
//! This module does not admit a Project, resolve references or enable source8.
|
||||
//! This module does not admit a Project or enable public source8.
|
||||
mod budget;
|
||||
mod cursor;
|
||||
mod declarations;
|
||||
mod lex;
|
||||
mod lower;
|
||||
mod syntax;
|
||||
|
||||
use super::Span;
|
||||
|
||||
@@ -134,4 +134,21 @@ impl Budget {
|
||||
pub fn numeric_units(&self) -> u64 {
|
||||
self.numeric.usage().numeric_units
|
||||
}
|
||||
/// Check phase allocation bounds without recharging parsed syntax or exact
|
||||
/// work. S4 must also charge ordinary records through the inherited budget.
|
||||
pub fn lowering_entries(&self, count: usize, span: Span) -> Result<(), Error> {
|
||||
self.active()?;
|
||||
if count > self.limits.entries {
|
||||
return Err(error(span, "Source8 lowering entry limit exceeded."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn lowering_occurrences(&self, count: usize, span: Span) -> Result<(), Error> {
|
||||
self.active()?;
|
||||
// Parameter declarations are syntax entries, not reference uses.
|
||||
if count > self.limits.references.saturating_add(self.limits.entries) {
|
||||
return Err(error(span, "Source8 lowering occurrence limit exceeded."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,939 @@
|
||||
//! Staged typed resolution. The caller owns parsing, ID stabilization and the
|
||||
//! single complete-Project semantic admission after merging these additions.
|
||||
use super::declarations::{self as d, Declaration, DeclarationKind};
|
||||
use super::*;
|
||||
use crate::analysis as a;
|
||||
use crate::analysis_numeric::Dimension;
|
||||
use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Caller-assigned file key; spans are original offsets within that file.
|
||||
pub(super) struct Unit {
|
||||
pub file: usize,
|
||||
pub declarations: Vec<Declaration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct Failure {
|
||||
/// None denotes an invalid ordinary skeleton, before typed resolution.
|
||||
pub file: Option<usize>,
|
||||
pub error: Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Category {
|
||||
Project,
|
||||
Element(Kind, Layer),
|
||||
Relation,
|
||||
Diagram,
|
||||
Node,
|
||||
Artifact,
|
||||
Trace,
|
||||
QuantityKind,
|
||||
Unit,
|
||||
Calculation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Target {
|
||||
Global(Id),
|
||||
Parameter {
|
||||
calculation: Id,
|
||||
parameter: a::ParameterId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Field {
|
||||
Algebra,
|
||||
Kind,
|
||||
Input(a::ParameterId),
|
||||
Result,
|
||||
Body,
|
||||
Subject,
|
||||
Type,
|
||||
Base,
|
||||
DisplayUnit,
|
||||
DocumentedConstraint,
|
||||
Predicate,
|
||||
Override(Id),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Role {
|
||||
Reference,
|
||||
ParameterDeclaration,
|
||||
ParameterUse,
|
||||
CallTarget,
|
||||
ArgumentLabel,
|
||||
OverrideTarget,
|
||||
}
|
||||
|
||||
/// Stable structural edges, independent of alias spelling and argument order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Child {
|
||||
Item(usize),
|
||||
Operand,
|
||||
Left,
|
||||
Right,
|
||||
Condition,
|
||||
WhenTrue,
|
||||
WhenFalse,
|
||||
Argument(a::ParameterId),
|
||||
Values,
|
||||
Initial,
|
||||
Base,
|
||||
Value,
|
||||
}
|
||||
|
||||
/// An arena index is only local to this lowering result. Follow parent edges
|
||||
/// to obtain the owner/field/path identity for S3; never persist the index.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct ExpressionSite {
|
||||
pub file: usize,
|
||||
pub owner: Id,
|
||||
pub field: Field,
|
||||
pub parent: Option<(usize, Child)>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct Occurrence {
|
||||
pub file: usize,
|
||||
pub owner: Id,
|
||||
pub field: Field,
|
||||
pub expression: Option<usize>,
|
||||
pub role: Role,
|
||||
pub target: Target,
|
||||
/// Alias/name token only, excluding @/$ and surrounding trivia.
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Lowered {
|
||||
pub definitions: BTreeMap<Id, a::Definition>,
|
||||
pub elements: BTreeMap<Id, Element>,
|
||||
pub occurrences: Vec<Occurrence>,
|
||||
pub expressions: Vec<ExpressionSite>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Symbol {
|
||||
id: Id,
|
||||
category: Category,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Tables {
|
||||
aliases: BTreeMap<String, Symbol>,
|
||||
ids: BTreeSet<Id>,
|
||||
parameters: BTreeMap<Id, BTreeMap<String, a::ParameterId>>,
|
||||
entries: usize,
|
||||
}
|
||||
|
||||
fn category(kind: DeclarationKind) -> Category {
|
||||
match kind {
|
||||
DeclarationKind::QuantityKind => Category::QuantityKind,
|
||||
DeclarationKind::Unit => Category::Unit,
|
||||
DeclarationKind::Calculation => Category::Calculation,
|
||||
DeclarationKind::Property(layer) => Category::Element(Kind::EngineeringProperty, layer),
|
||||
DeclarationKind::Check(layer) => Category::Element(Kind::AnalysisCheck, layer),
|
||||
DeclarationKind::Case(layer) => Category::Element(Kind::AnalysisCase, layer),
|
||||
}
|
||||
}
|
||||
|
||||
fn identity(identity: &d::IdentitySyntax, span: Span) -> Result<Id, Error> {
|
||||
identity.value.ok_or_else(|| {
|
||||
error(
|
||||
span,
|
||||
"Missing ID requires source stabilization before lowering.",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl Tables {
|
||||
fn reserve(&mut self, span: Span, budget: &Budget) -> Result<(), Error> {
|
||||
budget.lowering_entries(self.entries + 1, span)?;
|
||||
self.entries += 1;
|
||||
Ok(())
|
||||
}
|
||||
fn insert(
|
||||
&mut self,
|
||||
id: Id,
|
||||
alias: &str,
|
||||
category: Category,
|
||||
span: Span,
|
||||
budget: &Budget,
|
||||
) -> Result<(), Error> {
|
||||
// A phase allocation bound, not a second syntax charge. Ordinary
|
||||
// records, typed headers and scoped parameters share this bound.
|
||||
self.reserve(span, budget)?;
|
||||
if self.ids.contains(&id) {
|
||||
return Err(error(span, "Duplicate global identity."));
|
||||
}
|
||||
if self.aliases.contains_key(alias) {
|
||||
return Err(error(span, format!("Duplicate global alias '{alias}'.")));
|
||||
}
|
||||
self.ids.insert(id);
|
||||
self.aliases
|
||||
.insert(alias.to_owned(), Symbol { id, category });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> {
|
||||
let at = Span::default();
|
||||
self.insert(project.id, &project.alias, Category::Project, at, budget)?;
|
||||
for value in project.definitions.values() {
|
||||
let category = match &value.body {
|
||||
a::DefinitionBody::QuantityKind(_) => Category::QuantityKind,
|
||||
a::DefinitionBody::Unit(_) => Category::Unit,
|
||||
a::DefinitionBody::Calculation(_) => Category::Calculation,
|
||||
};
|
||||
self.insert(value.id, &value.alias, category, at, budget)?;
|
||||
if let a::DefinitionBody::Calculation(calculation) = &value.body {
|
||||
let mut names = BTreeMap::new();
|
||||
let mut ids = BTreeSet::new();
|
||||
for parameter in &calculation.parameters {
|
||||
self.reserve(at, budget)?;
|
||||
if names.contains_key(¶meter.name) || !ids.insert(parameter.id) {
|
||||
return Err(error(
|
||||
at,
|
||||
"Duplicate parameter name or identity in ordinary catalog.",
|
||||
));
|
||||
}
|
||||
names.insert(parameter.name.clone(), parameter.id);
|
||||
}
|
||||
self.parameters.insert(value.id, names);
|
||||
}
|
||||
}
|
||||
for value in project.elements.values() {
|
||||
self.insert(
|
||||
value.id,
|
||||
&value.alias,
|
||||
Category::Element(value.kind(), value.layer()),
|
||||
at,
|
||||
budget,
|
||||
)?;
|
||||
}
|
||||
for value in project.relations.values() {
|
||||
self.insert(value.id, &value.alias, Category::Relation, at, budget)?;
|
||||
}
|
||||
for value in project.diagrams.values() {
|
||||
self.insert(value.id, &value.alias, Category::Diagram, at, budget)?;
|
||||
for node in value.nodes.values() {
|
||||
self.insert(node.id, &node.alias, Category::Node, at, budget)?;
|
||||
}
|
||||
}
|
||||
for value in project.artifact_links.values() {
|
||||
self.insert(value.id, &value.alias, Category::Artifact, at, budget)?;
|
||||
}
|
||||
for value in project.traces.values() {
|
||||
self.insert(value.id, &value.alias, Category::Trace, at, budget)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn typed(&mut self, declaration: &Declaration, budget: &Budget) -> Result<(), Error> {
|
||||
let header = &declaration.header;
|
||||
let id = identity(&header.identity, header.alias.span)?;
|
||||
self.insert(
|
||||
id,
|
||||
&header.alias.name,
|
||||
category(header.kind),
|
||||
header.alias.span,
|
||||
budget,
|
||||
)?;
|
||||
if let d::Body::Calculation { parameters, .. } = &declaration.body {
|
||||
let mut names = BTreeMap::new();
|
||||
let mut ids = BTreeSet::new();
|
||||
for parameter in parameters {
|
||||
self.reserve(parameter.span, budget)?;
|
||||
let id = a::ParameterId(identity(¶meter.identity, parameter.name.span)?);
|
||||
if names.contains_key(¶meter.name.name) || !ids.insert(id) {
|
||||
return Err(error(
|
||||
parameter.name.span,
|
||||
"Duplicate parameter name or identity.",
|
||||
));
|
||||
}
|
||||
names.insert(parameter.name.name.clone(), id);
|
||||
}
|
||||
self.parameters.insert(id, names);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Inputs must come from the same live Budget and stabilization pass. No scan,
|
||||
/// exact conversion or reference charge occurs here. The ordinary skeleton is
|
||||
/// not semantically admitted: its typed forward references may need additions.
|
||||
pub(super) fn lower(
|
||||
ordinary: &Project,
|
||||
units: &[Unit],
|
||||
budget: &Budget,
|
||||
) -> Result<Lowered, Failure> {
|
||||
budget
|
||||
.active()
|
||||
.map_err(|error| Failure { file: None, error })?;
|
||||
let mut tables = Tables::default();
|
||||
tables
|
||||
.ordinary(ordinary, budget)
|
||||
.map_err(|error| Failure { file: None, error })?;
|
||||
let mut files = BTreeSet::new();
|
||||
for unit in units {
|
||||
budget
|
||||
.lowering_entries(files.len() + 1, Span::default())
|
||||
.map_err(|error| Failure {
|
||||
file: Some(unit.file),
|
||||
error,
|
||||
})?;
|
||||
if !files.insert(unit.file) {
|
||||
return Err(Failure {
|
||||
file: Some(unit.file),
|
||||
error: error(Span::default(), "Duplicate source file key."),
|
||||
});
|
||||
}
|
||||
for declaration in &unit.declarations {
|
||||
tables.typed(declaration, budget).map_err(|error| Failure {
|
||||
file: Some(unit.file),
|
||||
error,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
let mut output = Lowered {
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
occurrences: Vec::new(),
|
||||
expressions: Vec::new(),
|
||||
};
|
||||
for unit in units {
|
||||
for declaration in &unit.declarations {
|
||||
let owner = declaration
|
||||
.header
|
||||
.identity
|
||||
.value
|
||||
.expect("collected stable identity");
|
||||
let mut resolver = Resolver {
|
||||
tables: &tables,
|
||||
output: &mut output,
|
||||
budget,
|
||||
file: unit.file,
|
||||
owner,
|
||||
};
|
||||
resolver.declaration(declaration).map_err(|error| Failure {
|
||||
file: Some(unit.file),
|
||||
error,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
struct Resolver<'a> {
|
||||
tables: &'a Tables,
|
||||
output: &'a mut Lowered,
|
||||
budget: &'a Budget,
|
||||
file: usize,
|
||||
owner: Id,
|
||||
}
|
||||
|
||||
impl Resolver<'_> {
|
||||
fn occurrence(
|
||||
&mut self,
|
||||
reference: &Reference,
|
||||
field: Field,
|
||||
expression: Option<usize>,
|
||||
role: Role,
|
||||
target: Target,
|
||||
) -> Result<(), Error> {
|
||||
self.budget
|
||||
.lowering_occurrences(self.output.occurrences.len() + 1, reference.span)?;
|
||||
self.output.occurrences.push(Occurrence {
|
||||
file: self.file,
|
||||
owner: self.owner,
|
||||
field,
|
||||
expression,
|
||||
role,
|
||||
target,
|
||||
span: reference.span,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn global(
|
||||
&mut self,
|
||||
reference: &Reference,
|
||||
field: Field,
|
||||
expression: Option<usize>,
|
||||
role: Role,
|
||||
expected: impl FnOnce(Category) -> bool,
|
||||
) -> Result<Symbol, Error> {
|
||||
let symbol = *self.tables.aliases.get(&reference.name).ok_or_else(|| {
|
||||
error(
|
||||
reference.span,
|
||||
format!("Unknown global reference '@{}'.", reference.name),
|
||||
)
|
||||
})?;
|
||||
if !expected(symbol.category) {
|
||||
return Err(error(
|
||||
reference.span,
|
||||
format!(
|
||||
"Wrong reference domain for '@{}': {:?}.",
|
||||
reference.name, symbol.category
|
||||
),
|
||||
));
|
||||
}
|
||||
self.occurrence(
|
||||
reference,
|
||||
field,
|
||||
expression,
|
||||
role,
|
||||
Target::Global(symbol.id),
|
||||
)?;
|
||||
Ok(symbol)
|
||||
}
|
||||
|
||||
fn definition_ref(
|
||||
&mut self,
|
||||
reference: &Reference,
|
||||
field: Field,
|
||||
expression: Option<usize>,
|
||||
expected: Category,
|
||||
) -> Result<a::DefinitionRef, Error> {
|
||||
self.global(reference, field, expression, Role::Reference, |category| {
|
||||
category == expected
|
||||
})
|
||||
.map(|symbol| a::DefinitionRef::Local(symbol.id))
|
||||
}
|
||||
|
||||
fn value_type(
|
||||
&mut self,
|
||||
value: &d::TypeSyntax,
|
||||
field: Field,
|
||||
) -> Result<a::AnalysisType, Error> {
|
||||
let scalar =
|
||||
match &value.scalar.kind {
|
||||
d::ScalarKind::Boolean => a::ScalarType::Boolean,
|
||||
d::ScalarKind::Number => a::ScalarType::Number,
|
||||
d::ScalarKind::Quantity(reference) => a::ScalarType::Quantity(
|
||||
self.definition_ref(reference, field, None, Category::QuantityKind)?,
|
||||
),
|
||||
};
|
||||
Ok(if value.list {
|
||||
a::AnalysisType::List(scalar)
|
||||
} else {
|
||||
a::AnalysisType::Scalar(scalar)
|
||||
})
|
||||
}
|
||||
|
||||
fn subject(&mut self, reference: &Reference) -> Result<a::AnalysisSubject, Error> {
|
||||
let symbol = self.global(
|
||||
reference,
|
||||
Field::Subject,
|
||||
None,
|
||||
Role::Reference,
|
||||
|category| matches!(category, Category::Element(..) | Category::Relation),
|
||||
)?;
|
||||
Ok(if symbol.category == Category::Relation {
|
||||
a::AnalysisSubject::Relation(symbol.id)
|
||||
} else {
|
||||
a::AnalysisSubject::Element(symbol.id)
|
||||
})
|
||||
}
|
||||
|
||||
fn optional_subject(
|
||||
&mut self,
|
||||
subject: &Option<d::Field<Option<Reference>>>,
|
||||
) -> Result<Option<a::AnalysisSubject>, Error> {
|
||||
subject
|
||||
.as_ref()
|
||||
.and_then(|field| field.value.as_ref())
|
||||
.map(|reference| self.subject(reference))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn declaration(&mut self, declaration: &Declaration) -> Result<(), Error> {
|
||||
let header = &declaration.header;
|
||||
let definition = match &declaration.body {
|
||||
d::Body::QuantityKind { dimension, algebra } => {
|
||||
Some(a::DefinitionBody::QuantityKind(a::QuantityKindDefinition {
|
||||
dimension: Dimension::new(dimension.value.exponents)
|
||||
.map_err(|cause| error(dimension.value_span, cause.to_string()))?,
|
||||
algebra: match &algebra.value {
|
||||
d::AlgebraSyntax::Linear => a::QuantityAlgebra::Linear,
|
||||
d::AlgebraSyntax::Affine(reference) => a::QuantityAlgebra::Affine {
|
||||
difference: self.definition_ref(
|
||||
reference,
|
||||
Field::Algebra,
|
||||
None,
|
||||
Category::QuantityKind,
|
||||
)?,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
d::Body::Unit {
|
||||
kind,
|
||||
scale,
|
||||
offset,
|
||||
symbol,
|
||||
} => Some(a::DefinitionBody::Unit(a::UnitDefinition {
|
||||
kind: self.definition_ref(
|
||||
&kind.value,
|
||||
Field::Kind,
|
||||
None,
|
||||
Category::QuantityKind,
|
||||
)?,
|
||||
scale: scale.value.value.clone(),
|
||||
offset: offset.value.value.clone(),
|
||||
symbol: symbol.value.clone(),
|
||||
})),
|
||||
d::Body::Calculation {
|
||||
parameters,
|
||||
result,
|
||||
body,
|
||||
} => {
|
||||
let mut lowered = Vec::new();
|
||||
for parameter in parameters {
|
||||
self.budget
|
||||
.lowering_entries(lowered.len() + 1, parameter.span)?;
|
||||
let id = self.tables.parameters[&self.owner][¶meter.name.name];
|
||||
self.occurrence(
|
||||
¶meter.name,
|
||||
Field::Input(id),
|
||||
None,
|
||||
Role::ParameterDeclaration,
|
||||
Target::Parameter {
|
||||
calculation: self.owner,
|
||||
parameter: id,
|
||||
},
|
||||
)?;
|
||||
lowered.push(a::Parameter {
|
||||
id,
|
||||
name: parameter.name.name.clone(),
|
||||
value_type: self.value_type(¶meter.value_type, Field::Input(id))?,
|
||||
});
|
||||
}
|
||||
Some(a::DefinitionBody::Calculation(a::CalculationDefinition {
|
||||
parameters: lowered,
|
||||
result: self.value_type(&result.value, Field::Result)?,
|
||||
body: self.binding(&body.value, Field::Body, Some(self.owner))?,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let description = declaration
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or_else(String::new, |field| field.value.clone());
|
||||
if let Some(body) = definition {
|
||||
self.output.definitions.insert(
|
||||
self.owner,
|
||||
a::Definition {
|
||||
id: self.owner,
|
||||
alias: header.alias.name.clone(),
|
||||
name: header.name.clone(),
|
||||
description,
|
||||
body,
|
||||
},
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let (layer, body) = match (&declaration.body, header.kind) {
|
||||
(
|
||||
d::Body::Property {
|
||||
subject,
|
||||
value_type,
|
||||
base,
|
||||
display_unit,
|
||||
},
|
||||
DeclarationKind::Property(layer),
|
||||
) => (
|
||||
layer,
|
||||
SharedBody::EngineeringProperty {
|
||||
subject: self.subject(&subject.value)?,
|
||||
value_type: self.value_type(&value_type.value, Field::Type)?,
|
||||
base: self.binding(&base.value, Field::Base, None)?,
|
||||
display_unit: display_unit
|
||||
.as_ref()
|
||||
.and_then(|field| field.value.as_ref())
|
||||
.map(|reference| {
|
||||
self.definition_ref(reference, Field::DisplayUnit, None, Category::Unit)
|
||||
})
|
||||
.transpose()?,
|
||||
},
|
||||
),
|
||||
(
|
||||
d::Body::Check {
|
||||
subject,
|
||||
documented_constraint,
|
||||
predicate,
|
||||
},
|
||||
DeclarationKind::Check(layer),
|
||||
) => (
|
||||
layer,
|
||||
SharedBody::AnalysisCheck {
|
||||
subject: self.optional_subject(subject)?,
|
||||
documented_constraint: documented_constraint
|
||||
.as_ref()
|
||||
.and_then(|field| field.value.as_ref())
|
||||
.map(|reference| {
|
||||
self.global(
|
||||
reference,
|
||||
Field::DocumentedConstraint,
|
||||
None,
|
||||
Role::Reference,
|
||||
|category| {
|
||||
matches!(category, Category::Element(Kind::Constraint, _))
|
||||
},
|
||||
)
|
||||
.map(|symbol| symbol.id)
|
||||
})
|
||||
.transpose()?,
|
||||
predicate: self.binding(&predicate.value, Field::Predicate, None)?,
|
||||
},
|
||||
),
|
||||
(
|
||||
d::Body::Case {
|
||||
subject, overrides, ..
|
||||
},
|
||||
DeclarationKind::Case(layer),
|
||||
) => {
|
||||
let subject = self.optional_subject(subject)?;
|
||||
let mut lowered = BTreeMap::new();
|
||||
for entry in &overrides.value {
|
||||
let property = self
|
||||
.global(
|
||||
&entry.target,
|
||||
Field::Base,
|
||||
None,
|
||||
Role::OverrideTarget,
|
||||
|category| {
|
||||
matches!(category, Category::Element(Kind::EngineeringProperty, _))
|
||||
},
|
||||
)?
|
||||
.id;
|
||||
// Replace the placeholder field after resolution so the key
|
||||
// and its binding share their stable override identity.
|
||||
self.output
|
||||
.occurrences
|
||||
.last_mut()
|
||||
.expect("override occurrence")
|
||||
.field = Field::Override(property);
|
||||
if lowered.contains_key(&property) {
|
||||
return Err(error(
|
||||
entry.target.span,
|
||||
"Duplicate resolved override target.",
|
||||
));
|
||||
}
|
||||
self.budget
|
||||
.lowering_entries(lowered.len() + 1, entry.span)?;
|
||||
lowered.insert(
|
||||
property,
|
||||
self.binding(&entry.binding, Field::Override(property), None)?,
|
||||
);
|
||||
}
|
||||
(
|
||||
layer,
|
||||
SharedBody::AnalysisCase {
|
||||
subject,
|
||||
base: a::BaselineBase::ModelBaseline,
|
||||
overrides: lowered,
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return Err(error(
|
||||
header.kind_span,
|
||||
"Typed declaration header/body mismatch.",
|
||||
));
|
||||
}
|
||||
};
|
||||
self.output.elements.insert(
|
||||
self.owner,
|
||||
Element {
|
||||
id: self.owner,
|
||||
alias: header.alias.name.clone(),
|
||||
name: header.name.clone(),
|
||||
description,
|
||||
parent: None,
|
||||
body: ElementBody::shared_in(layer, body),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn binding(
|
||||
&mut self,
|
||||
binding: &Binding,
|
||||
field: Field,
|
||||
calculation: Option<Id>,
|
||||
) -> Result<a::Binding, Error> {
|
||||
match binding {
|
||||
Binding::Unbound(_) => Ok(a::Binding::Unbound),
|
||||
Binding::Expression(expression) => self
|
||||
.expression(expression, field, calculation)
|
||||
.map(a::Binding::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
fn call(
|
||||
&mut self,
|
||||
definition: &Reference,
|
||||
arguments: &[Argument],
|
||||
field: Field,
|
||||
site: usize,
|
||||
) -> Result<Call, Error> {
|
||||
let calculation = self
|
||||
.global(
|
||||
definition,
|
||||
field,
|
||||
Some(site),
|
||||
Role::CallTarget,
|
||||
|category| category == Category::Calculation,
|
||||
)?
|
||||
.id;
|
||||
let tables = self.tables;
|
||||
let signature = &tables.parameters[&calculation];
|
||||
let mut parameters = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for argument in arguments {
|
||||
self.budget
|
||||
.lowering_entries(parameters.len() + 1, argument.parameter.span)?;
|
||||
let parameter = *signature.get(&argument.parameter.name).ok_or_else(|| {
|
||||
error(
|
||||
argument.parameter.span,
|
||||
"Unknown argument name in called calculation.",
|
||||
)
|
||||
})?;
|
||||
if !seen.insert(parameter) {
|
||||
return Err(error(
|
||||
argument.parameter.span,
|
||||
"Duplicate resolved argument parameter.",
|
||||
));
|
||||
}
|
||||
self.occurrence(
|
||||
&argument.parameter,
|
||||
field,
|
||||
Some(site),
|
||||
Role::ArgumentLabel,
|
||||
Target::Parameter {
|
||||
calculation,
|
||||
parameter,
|
||||
},
|
||||
)?;
|
||||
parameters.push(parameter);
|
||||
}
|
||||
// Argument-set/type checking remains in existing semantic admission.
|
||||
Ok(Call {
|
||||
calculation,
|
||||
parameters,
|
||||
})
|
||||
}
|
||||
|
||||
fn expression(
|
||||
&mut self,
|
||||
root: &Expression,
|
||||
field: Field,
|
||||
calculation: Option<Id>,
|
||||
) -> Result<a::Expr, Error> {
|
||||
enum Work<'a> {
|
||||
Visit(&'a Expression, Option<(usize, Child)>),
|
||||
Build(&'a Expression, usize, Option<Call>, usize),
|
||||
}
|
||||
let mut work = vec![Work::Visit(root, None)];
|
||||
let mut values = Vec::new();
|
||||
while let Some(task) = work.pop() {
|
||||
match task {
|
||||
Work::Visit(expression, parent) => {
|
||||
self.budget
|
||||
.lowering_entries(self.output.expressions.len() + 1, expression.span)?;
|
||||
let site = self.output.expressions.len();
|
||||
self.output.expressions.push(ExpressionSite {
|
||||
file: self.file,
|
||||
owner: self.owner,
|
||||
field,
|
||||
parent,
|
||||
span: expression.span,
|
||||
});
|
||||
let call = if let ExpressionKind::Call {
|
||||
definition,
|
||||
arguments,
|
||||
} = &expression.kind
|
||||
{
|
||||
Some(self.call(definition, arguments, field, site)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let child_count = match &expression.kind {
|
||||
ExpressionKind::List(items) => items.len(),
|
||||
ExpressionKind::Call { arguments, .. } => arguments.len(),
|
||||
ExpressionKind::Unary { .. }
|
||||
| ExpressionKind::Power { .. }
|
||||
| ExpressionKind::AsKind { .. } => 1,
|
||||
ExpressionKind::Binary { .. } | ExpressionKind::Sum { .. } => 2,
|
||||
ExpressionKind::If { .. } => 3,
|
||||
_ => 0,
|
||||
};
|
||||
self.budget
|
||||
.lowering_entries(work.len() + child_count + 1, expression.span)?;
|
||||
let mut children = Vec::with_capacity(child_count);
|
||||
match &expression.kind {
|
||||
ExpressionKind::List(items) => children.extend(
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, child)| (child, Child::Item(index))),
|
||||
),
|
||||
ExpressionKind::Unary { operand, .. } => {
|
||||
children.push((operand.as_ref(), Child::Operand))
|
||||
}
|
||||
ExpressionKind::Binary { left, right, .. } => children
|
||||
.extend([(left.as_ref(), Child::Left), (right.as_ref(), Child::Right)]),
|
||||
ExpressionKind::If {
|
||||
condition,
|
||||
when_true,
|
||||
when_false,
|
||||
} => children.extend([
|
||||
(condition.as_ref(), Child::Condition),
|
||||
(when_true.as_ref(), Child::WhenTrue),
|
||||
(when_false.as_ref(), Child::WhenFalse),
|
||||
]),
|
||||
ExpressionKind::Call { arguments, .. } => children.extend(
|
||||
arguments
|
||||
.iter()
|
||||
.zip(&call.as_ref().expect("call signature").parameters)
|
||||
.map(|(argument, id)| (&argument.value, Child::Argument(*id))),
|
||||
),
|
||||
ExpressionKind::Sum { values, initial } => children.extend([
|
||||
(values.as_ref(), Child::Values),
|
||||
(initial.as_ref(), Child::Initial),
|
||||
]),
|
||||
ExpressionKind::Power { base, .. } => {
|
||||
children.push((base.as_ref(), Child::Base))
|
||||
}
|
||||
ExpressionKind::AsKind { value, .. } => {
|
||||
children.push((value.as_ref(), Child::Value))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
work.push(Work::Build(expression, site, call, children.len()));
|
||||
work.extend(
|
||||
children
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|(child, edge)| Work::Visit(child, Some((site, edge)))),
|
||||
);
|
||||
}
|
||||
Work::Build(expression, site, call, count) => {
|
||||
let start = values.len() - count;
|
||||
let mut children = values.split_off(start).into_iter();
|
||||
let next = |children: &mut std::vec::IntoIter<a::Expr>| {
|
||||
Box::new(children.next().expect("visited expression child"))
|
||||
};
|
||||
use ExpressionKind as E;
|
||||
let value = match &expression.kind {
|
||||
E::Boolean(value) => a::Expr::Boolean(*value),
|
||||
E::Number(value) => a::Expr::Number(value.clone()),
|
||||
E::Quantity { magnitude, unit } => a::Expr::Quantity {
|
||||
magnitude: magnitude.clone(),
|
||||
unit: self.definition_ref(unit, field, Some(site), Category::Unit)?,
|
||||
},
|
||||
E::Property(reference) => a::Expr::Property(
|
||||
self.global(
|
||||
reference,
|
||||
field,
|
||||
Some(site),
|
||||
Role::Reference,
|
||||
|category| {
|
||||
matches!(
|
||||
category,
|
||||
Category::Element(Kind::EngineeringProperty, _)
|
||||
)
|
||||
},
|
||||
)?
|
||||
.id,
|
||||
),
|
||||
E::Parameter(reference) => {
|
||||
let calculation = calculation.ok_or_else(|| {
|
||||
error(
|
||||
reference.span,
|
||||
"Parameter use is only valid in its calculation body.",
|
||||
)
|
||||
})?;
|
||||
let parameter = *self.tables.parameters[&calculation]
|
||||
.get(&reference.name)
|
||||
.ok_or_else(|| {
|
||||
error(
|
||||
reference.span,
|
||||
"Unknown parameter in owning calculation.",
|
||||
)
|
||||
})?;
|
||||
self.occurrence(
|
||||
reference,
|
||||
field,
|
||||
Some(site),
|
||||
Role::ParameterUse,
|
||||
Target::Parameter {
|
||||
calculation,
|
||||
parameter,
|
||||
},
|
||||
)?;
|
||||
a::Expr::Parameter(parameter)
|
||||
}
|
||||
E::List(_) => a::Expr::List(children.collect()),
|
||||
E::Unary { op, .. } => a::Expr::Unary {
|
||||
op: *op,
|
||||
operand: next(&mut children),
|
||||
},
|
||||
E::Binary { op, .. } => a::Expr::Binary {
|
||||
op: *op,
|
||||
left: next(&mut children),
|
||||
right: next(&mut children),
|
||||
},
|
||||
E::If { .. } => a::Expr::If {
|
||||
condition: next(&mut children),
|
||||
when_true: next(&mut children),
|
||||
when_false: next(&mut children),
|
||||
},
|
||||
E::Call { .. } => {
|
||||
let call = call.expect("resolved call");
|
||||
a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(call.calculation),
|
||||
arguments: call.parameters.into_iter().zip(children).collect(),
|
||||
}
|
||||
}
|
||||
E::Sum { .. } => a::Expr::Sum {
|
||||
values: next(&mut children),
|
||||
initial: next(&mut children),
|
||||
},
|
||||
E::Power { exponent, .. } => a::Expr::Power {
|
||||
base: next(&mut children),
|
||||
exponent: *exponent,
|
||||
},
|
||||
E::AsKind { kind, .. } => a::Expr::AsKind {
|
||||
kind: self.definition_ref(
|
||||
kind,
|
||||
field,
|
||||
Some(site),
|
||||
Category::QuantityKind,
|
||||
)?,
|
||||
value: next(&mut children),
|
||||
},
|
||||
};
|
||||
self.budget
|
||||
.lowering_entries(values.len() + 1, expression.span)?;
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(values.pop().expect("one lowered expression"))
|
||||
}
|
||||
}
|
||||
|
||||
struct Call {
|
||||
calculation: Id,
|
||||
parameters: Vec<a::ParameterId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,7 @@
|
||||
arcadia 7;
|
||||
project response_budget "Response budget" id "4210b9f2-7046-47f0-aa3b-153f626a579d" {
|
||||
oa_activity respond "Respond" id "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59" {
|
||||
oa_activity receive "Receive" id "4ab9a8a7-7552-488d-9862-e14b89e7b353" {}
|
||||
oa_activity dispatch "Dispatch" id "6df991ba-4c5e-4535-99e2-524ae274ee8b" {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
arcadia 7;
|
||||
project response_budget "Response budget" id "c210b9f2-7046-47f0-aa3b-153f626a579d" {
|
||||
sa_function respond "Respond" id "4abec6a1-7d5d-48f3-8d2d-a7fc8cf78f59" {
|
||||
sa_function receive "Receive" id "cab9a8a7-7552-488d-9862-e14b89e7b353" {}
|
||||
sa_function dispatch "Dispatch" id "edf991ba-4c5e-4535-99e2-524ae274ee8b" {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Staged typed roots from the r2 example; ordinary roots are supplied separately.
|
||||
// Forward uses precede the neutral definitions. 雪 keeps offsets UTF-8 sensitive.
|
||||
oa_property receive_time "Receive time" id "63a931c2-675a-4fce-89e5-04361cb99450" {
|
||||
subject @receive; type Quantity(@duration); display_unit @millisecond;
|
||||
base 750 [@millisecond];
|
||||
}
|
||||
oa_property dispatch_time "Dispatch time" id "c33fc772-2a60-4044-9af9-73c61c563ace" {
|
||||
subject @dispatch; type Quantity(@duration); display_unit @second;
|
||||
base 1 [@second];
|
||||
}
|
||||
oa_property total_time "Total time" id "bd5b68e8-65fc-4e35-9071-a73061316a4b" {
|
||||
subject @respond; type Quantity(@duration); display_unit @second;
|
||||
base @combine(a: @receive_time, b: @dispatch_time);
|
||||
}
|
||||
oa_property limit "Allowed duration" id "437bedbb-2cf4-44ee-a139-6563cc5086ec" {
|
||||
subject @respond; type Quantity(@duration); display_unit @second;
|
||||
base 2 [@second];
|
||||
}
|
||||
oa_check budget "Response budget" id "bac0de99-9d2f-4e53-9158-d424ecac61bf" {
|
||||
subject none; documented_constraint none; predicate @total_time <= @limit;
|
||||
}
|
||||
oa_analysis degraded "Degraded radio" id "38036c65-16d4-449c-837b-91eb141c1d93" {
|
||||
subject none; base ModelBaseline;
|
||||
overrides { @receive_time = 1250 [@millisecond]; };
|
||||
}
|
||||
oa_analysis unmeasured "Unmeasured radio" id "99d3e09a-0ec3-4194-9dd4-9f3de6625cde" {
|
||||
subject none; base ModelBaseline;
|
||||
overrides { @receive_time = unbound; };
|
||||
}
|
||||
calculation combine "Combine durations" id "fd7ce7ab-f789-40bc-b699-0c37f1b6902f" {
|
||||
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Quantity(@duration);
|
||||
input b id "7a47cc1e-8ae5-4551-b2d8-8a68108e0976" : Quantity(@duration);
|
||||
result Quantity(@duration); body $a + $b;
|
||||
}
|
||||
unit second "Second" id "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce" {
|
||||
kind @duration; scale 1; offset 0; symbol "s";
|
||||
}
|
||||
unit millisecond "Millisecond" id "39053f37-fec9-4708-9f0a-3959d223f891" {
|
||||
kind @duration; scale ratio(1, 1000); offset 0; symbol "ms";
|
||||
}
|
||||
quantity_kind duration "Duration" id "188b911c-5f8b-431c-9eea-2d41f3953767" {
|
||||
dimension { time 1; }; algebra Linear;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Staged typed roots from the r2 example; ordinary roots are supplied separately.
|
||||
// Forward uses precede the neutral definitions. 雪 keeps offsets UTF-8 sensitive.
|
||||
sa_property receive_time "Receive time" id "e3a931c2-675a-4fce-89e5-04361cb99450" {
|
||||
subject @receive; type Quantity(@duration); display_unit @millisecond;
|
||||
base 750 [@millisecond];
|
||||
}
|
||||
sa_property dispatch_time "Dispatch time" id "433fc772-2a60-4044-9af9-73c61c563ace" {
|
||||
subject @dispatch; type Quantity(@duration); display_unit @second;
|
||||
base 1 [@second];
|
||||
}
|
||||
sa_property total_time "Total time" id "3d5b68e8-65fc-4e35-9071-a73061316a4b" {
|
||||
subject @respond; type Quantity(@duration); display_unit @second;
|
||||
base @combine(a: @receive_time, b: @dispatch_time);
|
||||
}
|
||||
sa_property limit "Allowed duration" id "c37bedbb-2cf4-44ee-a139-6563cc5086ec" {
|
||||
subject @respond; type Quantity(@duration); display_unit @second;
|
||||
base 2 [@second];
|
||||
}
|
||||
sa_check budget "Response budget" id "3ac0de99-9d2f-4e53-9158-d424ecac61bf" {
|
||||
subject none; documented_constraint none; predicate @total_time <= @limit;
|
||||
}
|
||||
sa_analysis degraded "Degraded radio" id "b8036c65-16d4-449c-837b-91eb141c1d93" {
|
||||
subject none; base ModelBaseline;
|
||||
overrides { @receive_time = 1250 [@millisecond]; };
|
||||
}
|
||||
sa_analysis unmeasured "Unmeasured radio" id "19d3e09a-0ec3-4194-9dd4-9f3de6625cde" {
|
||||
subject none; base ModelBaseline;
|
||||
overrides { @receive_time = unbound; };
|
||||
}
|
||||
calculation combine "Combine durations" id "7d7ce7ab-f789-40bc-b699-0c37f1b6902f" {
|
||||
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Quantity(@duration);
|
||||
input b id "7a47cc1e-8ae5-4551-b2d8-8a68108e0976" : Quantity(@duration);
|
||||
result Quantity(@duration); body $a + $b;
|
||||
}
|
||||
unit second "Second" id "d34f4672-1ba5-4157-8b0a-b9cf41d0a3ce" {
|
||||
kind @duration; scale 1; offset 0; symbol "s";
|
||||
}
|
||||
unit millisecond "Millisecond" id "b9053f37-fec9-4708-9f0a-3959d223f891" {
|
||||
kind @duration; scale ratio(1, 1000); offset 0; symbol "ms";
|
||||
}
|
||||
quantity_kind duration "Duration" id "988b911c-5f8b-431c-9eea-2d41f3953767" {
|
||||
dimension { time 1; }; algebra Linear;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! Independent model construction: never parses source or calls the writer.
|
||||
use crate::analysis::*;
|
||||
use crate::analysis_numeric::{Dimension, Exact, ExactWire, WorkLimits, WorkMeter};
|
||||
use crate::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(super) const NAMES: [(&str, &str); 14] = [
|
||||
("duration", "Duration"),
|
||||
("second", "Second"),
|
||||
("millisecond", "Millisecond"),
|
||||
("combine", "Combine durations"),
|
||||
("respond", "Respond"),
|
||||
("receive", "Receive"),
|
||||
("dispatch", "Dispatch"),
|
||||
("receive_time", "Receive time"),
|
||||
("dispatch_time", "Dispatch time"),
|
||||
("total_time", "Total time"),
|
||||
("limit", "Allowed duration"),
|
||||
("budget", "Response budget"),
|
||||
("degraded", "Degraded radio"),
|
||||
("unmeasured", "Unmeasured radio"),
|
||||
];
|
||||
|
||||
fn named(alias: &str) -> String {
|
||||
NAMES
|
||||
.iter()
|
||||
.find(|(key, _)| *key == alias)
|
||||
.unwrap()
|
||||
.1
|
||||
.into()
|
||||
}
|
||||
fn local(id: Id) -> DefinitionRef {
|
||||
DefinitionRef::Local(id)
|
||||
}
|
||||
fn quantity(value: i64, unit: Id) -> Expr {
|
||||
Expr::Quantity {
|
||||
magnitude: Exact::from_integer(value),
|
||||
unit: local(unit),
|
||||
}
|
||||
}
|
||||
fn expression(value: Expr) -> Binding {
|
||||
Binding::Expression(value)
|
||||
}
|
||||
fn binary(op: BinaryOp, left: Expr, right: Expr) -> Expr {
|
||||
Expr::Binary {
|
||||
op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn project(layer: Layer) -> Project {
|
||||
// OA is the fixed r2 set; SA independently constructs fresh global IDs.
|
||||
let id = |text| {
|
||||
let id = Id::parse_str(text).unwrap();
|
||||
if layer == Layer::Oa {
|
||||
id
|
||||
} else {
|
||||
Id::from_u128(id.as_u128() ^ (1 << 127))
|
||||
}
|
||||
};
|
||||
let project_id = id("4210b9f2-7046-47f0-aa3b-153f626a579d");
|
||||
let duration = id("188b911c-5f8b-431c-9eea-2d41f3953767");
|
||||
let second = id("534f4672-1ba5-4157-8b0a-b9cf41d0a3ce");
|
||||
let millisecond = id("39053f37-fec9-4708-9f0a-3959d223f891");
|
||||
let combine = id("fd7ce7ab-f789-40bc-b699-0c37f1b6902f");
|
||||
// Parameter IDs may be reused in another calculation/project.
|
||||
let a = ParameterId(Id::parse_str("28ff40b0-1b8f-4735-8536-5a6ab21b56cb").unwrap());
|
||||
let b = ParameterId(Id::parse_str("7a47cc1e-8ae5-4551-b2d8-8a68108e0976").unwrap());
|
||||
let respond = id("cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59");
|
||||
let receive = id("4ab9a8a7-7552-488d-9862-e14b89e7b353");
|
||||
let dispatch = id("6df991ba-4c5e-4535-99e2-524ae274ee8b");
|
||||
let receive_time = id("63a931c2-675a-4fce-89e5-04361cb99450");
|
||||
let dispatch_time = id("c33fc772-2a60-4044-9af9-73c61c563ace");
|
||||
let total = id("bd5b68e8-65fc-4e35-9071-a73061316a4b");
|
||||
let limit = id("437bedbb-2cf4-44ee-a139-6563cc5086ec");
|
||||
let check = id("bac0de99-9d2f-4e53-9158-d424ecac61bf");
|
||||
let degraded = id("38036c65-16d4-449c-837b-91eb141c1d93");
|
||||
let unmeasured = id("99d3e09a-0ec3-4194-9dd4-9f3de6625cde");
|
||||
let duration_type = || AnalysisType::Scalar(ScalarType::Quantity(local(duration)));
|
||||
let definition = |id, alias: &str, body| Definition {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: named(alias),
|
||||
description: String::new(),
|
||||
body,
|
||||
};
|
||||
let shared = |id, alias: &str, body| Element {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: named(alias),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::shared_in(layer, body),
|
||||
};
|
||||
let work = |id, alias: &str, parent| Element {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: named(alias),
|
||||
description: String::new(),
|
||||
parent,
|
||||
body: if layer == Layer::Oa {
|
||||
ElementBody::Oa(OaBody::Activity)
|
||||
} else {
|
||||
ElementBody::Sa(SaBody::Function)
|
||||
},
|
||||
};
|
||||
let definitions = [
|
||||
definition(
|
||||
duration,
|
||||
"duration",
|
||||
DefinitionBody::QuantityKind(QuantityKindDefinition {
|
||||
dimension: Dimension::new([0, 0, 1, 0, 0, 0, 0]).unwrap(),
|
||||
algebra: QuantityAlgebra::Linear,
|
||||
}),
|
||||
),
|
||||
definition(
|
||||
second,
|
||||
"second",
|
||||
DefinitionBody::Unit(UnitDefinition {
|
||||
kind: local(duration),
|
||||
scale: Exact::from_integer(1),
|
||||
offset: Exact::from_integer(0),
|
||||
symbol: "s".into(),
|
||||
}),
|
||||
),
|
||||
definition(
|
||||
millisecond,
|
||||
"millisecond",
|
||||
DefinitionBody::Unit(UnitDefinition {
|
||||
kind: local(duration),
|
||||
scale: Exact::from_wire(
|
||||
&ExactWire {
|
||||
numerator: "1".into(),
|
||||
denominator: "1000".into(),
|
||||
},
|
||||
&mut WorkMeter::new(WorkLimits::REFERENCE),
|
||||
)
|
||||
.unwrap(),
|
||||
offset: Exact::from_integer(0),
|
||||
symbol: "ms".into(),
|
||||
}),
|
||||
),
|
||||
definition(
|
||||
combine,
|
||||
"combine",
|
||||
DefinitionBody::Calculation(CalculationDefinition {
|
||||
parameters: vec![
|
||||
Parameter {
|
||||
id: a,
|
||||
name: "a".into(),
|
||||
value_type: duration_type(),
|
||||
},
|
||||
Parameter {
|
||||
id: b,
|
||||
name: "b".into(),
|
||||
value_type: duration_type(),
|
||||
},
|
||||
],
|
||||
result: duration_type(),
|
||||
body: expression(binary(
|
||||
BinaryOp::Add,
|
||||
Expr::Parameter(a),
|
||||
Expr::Parameter(b),
|
||||
)),
|
||||
}),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|definition| (definition.id, definition))
|
||||
.collect();
|
||||
let property = |id, alias, subject, display, base| {
|
||||
shared(
|
||||
id,
|
||||
alias,
|
||||
SharedBody::EngineeringProperty {
|
||||
subject: AnalysisSubject::Element(subject),
|
||||
value_type: duration_type(),
|
||||
display_unit: Some(local(display)),
|
||||
base: expression(base),
|
||||
},
|
||||
)
|
||||
};
|
||||
let case = |id, alias, binding| {
|
||||
shared(
|
||||
id,
|
||||
alias,
|
||||
SharedBody::AnalysisCase {
|
||||
subject: None,
|
||||
base: BaselineBase::ModelBaseline,
|
||||
overrides: BTreeMap::from([(receive_time, binding)]),
|
||||
},
|
||||
)
|
||||
};
|
||||
let elements = [
|
||||
work(respond, "respond", None),
|
||||
work(receive, "receive", Some(respond)),
|
||||
work(dispatch, "dispatch", Some(respond)),
|
||||
property(
|
||||
receive_time,
|
||||
"receive_time",
|
||||
receive,
|
||||
millisecond,
|
||||
quantity(750, millisecond),
|
||||
),
|
||||
property(
|
||||
dispatch_time,
|
||||
"dispatch_time",
|
||||
dispatch,
|
||||
second,
|
||||
quantity(1, second),
|
||||
),
|
||||
property(
|
||||
total,
|
||||
"total_time",
|
||||
respond,
|
||||
second,
|
||||
Expr::Call {
|
||||
definition: local(combine),
|
||||
arguments: BTreeMap::from([
|
||||
(a, Expr::Property(receive_time)),
|
||||
(b, Expr::Property(dispatch_time)),
|
||||
]),
|
||||
},
|
||||
),
|
||||
property(limit, "limit", respond, second, quantity(2, second)),
|
||||
shared(
|
||||
check,
|
||||
"budget",
|
||||
SharedBody::AnalysisCheck {
|
||||
subject: None,
|
||||
documented_constraint: None,
|
||||
predicate: expression(binary(
|
||||
BinaryOp::LessOrEqual,
|
||||
Expr::Property(total),
|
||||
Expr::Property(limit),
|
||||
)),
|
||||
},
|
||||
),
|
||||
case(
|
||||
degraded,
|
||||
"degraded",
|
||||
expression(quantity(1250, millisecond)),
|
||||
),
|
||||
case(unmeasured, "unmeasured", Binding::Unbound),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|element| (element.id, element))
|
||||
.collect();
|
||||
Project {
|
||||
schema_version: 8,
|
||||
id: project_id,
|
||||
alias: "response_budget".into(),
|
||||
name: "Response budget".into(),
|
||||
description: String::new(),
|
||||
definitions,
|
||||
elements,
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
use super::*;
|
||||
use crate::language::ParseLimits;
|
||||
use crate::language::analysis_source::{budget::Limits, cursor::Cursor};
|
||||
|
||||
#[path = "oracle.rs"]
|
||||
mod oracle;
|
||||
|
||||
const OA: &str = include_str!("fixtures/typed-oa.arcadia");
|
||||
const SA: &str = include_str!("fixtures/typed-sa.arcadia");
|
||||
const ORDINARY_OA: &str = include_str!("fixtures/ordinary-oa.arcadia");
|
||||
const ORDINARY_SA: &str = include_str!("fixtures/ordinary-sa.arcadia");
|
||||
|
||||
fn budget() -> Budget {
|
||||
Budget::new(Limits::source8(ParseLimits::UNLIMITED)).unwrap()
|
||||
}
|
||||
fn id(text: &str) -> Id {
|
||||
Id::parse_str(text).unwrap()
|
||||
}
|
||||
fn by_alias(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
fn ordinary(source: &str) -> Project {
|
||||
crate::language::parse(source).unwrap()
|
||||
}
|
||||
fn unit(source: &str, file: usize, budget: &mut Budget) -> Unit {
|
||||
let tokens = lex::tokens(source, budget).unwrap();
|
||||
let mut cursor = Cursor::new(source, &tokens, budget);
|
||||
let mut declarations = Vec::new();
|
||||
while cursor.cursor < tokens.len() {
|
||||
declarations.push(d::parse(&mut cursor, 8, None).unwrap());
|
||||
}
|
||||
Unit { file, declarations }
|
||||
}
|
||||
fn merge(mut ordinary: Project, lowered: Lowered) -> Project {
|
||||
ordinary.definitions.extend(lowered.definitions);
|
||||
ordinary.elements.extend(lowered.elements);
|
||||
ordinary
|
||||
}
|
||||
fn admit(project: &Project) {
|
||||
a::admit(project, a::AdmissionLimits::REFERENCE).unwrap();
|
||||
}
|
||||
fn failure(source: &str, ordinary: &Project) -> Failure {
|
||||
let mut budget = budget();
|
||||
let unit = unit(source, 7, &mut budget);
|
||||
lower(ordinary, &[unit], &budget).unwrap_err()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_oa_and_sa_lowering_match_complete_independent_17_uuid_oracles() {
|
||||
for (layer, ordinary_source, typed_source) in
|
||||
[(Layer::Oa, ORDINARY_OA, OA), (Layer::Sa, ORDINARY_SA, SA)]
|
||||
{
|
||||
let expected = oracle::project(layer);
|
||||
assert_eq!(expected.name, "Response budget");
|
||||
assert_eq!(expected.identities().len(), 15); // two additional scoped IDs
|
||||
for (alias, name) in oracle::NAMES {
|
||||
let object = by_alias(&expected, alias);
|
||||
let actual_name = expected
|
||||
.definitions
|
||||
.get(&object)
|
||||
.map(|value| value.name.as_str())
|
||||
.or_else(|| {
|
||||
expected
|
||||
.elements
|
||||
.get(&object)
|
||||
.map(|value| value.name.as_str())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(actual_name, name);
|
||||
assert_ne!(actual_name, alias);
|
||||
}
|
||||
let mut budget = budget();
|
||||
let units = [unit(typed_source, 17, &mut budget)];
|
||||
let usage = budget.usage;
|
||||
let numeric = budget.numeric_units();
|
||||
let skeleton = ordinary(ordinary_source);
|
||||
let first = lower(&skeleton, &units, &budget).unwrap();
|
||||
let second = lower(&skeleton, &units, &budget).unwrap();
|
||||
assert_eq!(first.occurrences, second.occurrences);
|
||||
assert_eq!(first.expressions, second.expressions);
|
||||
assert_eq!(budget.usage, usage);
|
||||
assert_eq!(budget.numeric_units(), numeric);
|
||||
assert_eq!(
|
||||
first
|
||||
.occurrences
|
||||
.iter()
|
||||
.filter(|occurrence| occurrence.role != Role::ParameterDeclaration)
|
||||
.count(),
|
||||
usage.references
|
||||
);
|
||||
let actual = merge(skeleton, first);
|
||||
assert_eq!(actual, expected);
|
||||
admit(&actual); // the complete model's one semantic admission
|
||||
}
|
||||
assert!(crate::language::parse(&ORDINARY_OA.replacen("arcadia 7", "arcadia 8", 1)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_cross_unit_resolution_keeps_original_offsets_and_shared_work() {
|
||||
let source = OA.replace('\n', "\r\n");
|
||||
let split = source.find("calculation combine").unwrap();
|
||||
let sources = [&source[..split], &source[split..]];
|
||||
let mut budget = budget();
|
||||
let mut units = [
|
||||
unit(sources[0], 4, &mut budget),
|
||||
unit(sources[1], 9, &mut budget),
|
||||
];
|
||||
let usage = budget.usage;
|
||||
assert_eq!(usage.bytes, source.len());
|
||||
let lowered = lower(&ordinary(ORDINARY_OA), &units, &budget).unwrap();
|
||||
units.reverse();
|
||||
// This second order uses the same bounded lowerer; output models must not
|
||||
// depend on declaration/file traversal order. It is not an include parser.
|
||||
let reordered = lower(&ordinary(ORDINARY_OA), &units, &budget).unwrap();
|
||||
assert_eq!(
|
||||
merge(ordinary(ORDINARY_OA), reordered),
|
||||
oracle::project(Layer::Oa)
|
||||
);
|
||||
for occurrence in &lowered.occurrences {
|
||||
let source = if occurrence.file == 4 {
|
||||
sources[0]
|
||||
} else {
|
||||
assert_eq!(occurrence.file, 9);
|
||||
sources[1]
|
||||
};
|
||||
let text = &source[occurrence.span.start..occurrence.span.end];
|
||||
assert!(crate::valid_alias(text));
|
||||
assert!(!text.starts_with('@') && !text.starts_with('$'));
|
||||
}
|
||||
assert_eq!(budget.usage, usage);
|
||||
assert_eq!(
|
||||
merge(ordinary(ORDINARY_OA), lowered),
|
||||
oracle::project(Layer::Oa)
|
||||
);
|
||||
}
|
||||
|
||||
fn number(value: i64) -> a::Expr {
|
||||
a::Expr::Number(crate::analysis_numeric::Exact::from_integer(value))
|
||||
}
|
||||
fn binary(op: a::BinaryOp, left: a::Expr, right: a::Expr) -> a::Expr {
|
||||
a::Expr::Binary {
|
||||
op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_thirteen_expression_variants_lower_to_real_typed_values_and_admit() {
|
||||
let mut project = oracle::project(Layer::Oa);
|
||||
let duration = by_alias(&project, "duration");
|
||||
let second = by_alias(&project, "second");
|
||||
let property = by_alias(&project, "receive_time");
|
||||
let owner = by_alias(&project, "total_time");
|
||||
let calculation = by_alias(&project, "combine");
|
||||
let parameter = a::ParameterId(id("28ff40b0-1b8f-4735-8536-5a6ab21b56cb"));
|
||||
let b = a::ParameterId(id("7a47cc1e-8ae5-4551-b2d8-8a68108e0976"));
|
||||
let quantity =
|
||||
a::AnalysisType::Scalar(a::ScalarType::Quantity(a::DefinitionRef::Local(duration)));
|
||||
let numeric = a::AnalysisType::Scalar(a::ScalarType::Number);
|
||||
let boolean = a::AnalysisType::Scalar(a::ScalarType::Boolean);
|
||||
let cases = [
|
||||
("true", a::Expr::Boolean(true), boolean.clone()),
|
||||
(
|
||||
"ratio(3, 2)",
|
||||
a::Expr::Number(
|
||||
Exact::from_wire(
|
||||
&crate::analysis_numeric::ExactWire {
|
||||
numerator: "3".into(),
|
||||
denominator: "2".into(),
|
||||
},
|
||||
&mut crate::analysis_numeric::WorkMeter::new(
|
||||
crate::analysis_numeric::WorkLimits::REFERENCE,
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
numeric.clone(),
|
||||
),
|
||||
(
|
||||
"2 [@second]",
|
||||
a::Expr::Quantity {
|
||||
magnitude: Exact::from_integer(2),
|
||||
unit: a::DefinitionRef::Local(second),
|
||||
},
|
||||
quantity.clone(),
|
||||
),
|
||||
(
|
||||
"@receive_time",
|
||||
a::Expr::Property(property),
|
||||
quantity.clone(),
|
||||
),
|
||||
("$a", a::Expr::Parameter(parameter), quantity.clone()),
|
||||
(
|
||||
"[1, 2]",
|
||||
a::Expr::List(vec![number(1), number(2)]),
|
||||
a::AnalysisType::List(a::ScalarType::Number),
|
||||
),
|
||||
(
|
||||
"abs(-2)",
|
||||
a::Expr::Unary {
|
||||
op: a::UnaryOp::Absolute,
|
||||
operand: Box::new(a::Expr::Unary {
|
||||
op: a::UnaryOp::Negate,
|
||||
operand: Box::new(number(2)),
|
||||
}),
|
||||
},
|
||||
numeric.clone(),
|
||||
),
|
||||
(
|
||||
"1 + 2",
|
||||
binary(a::BinaryOp::Add, number(1), number(2)),
|
||||
numeric.clone(),
|
||||
),
|
||||
(
|
||||
"if(true, 1, 2)",
|
||||
a::Expr::If {
|
||||
condition: Box::new(a::Expr::Boolean(true)),
|
||||
when_true: Box::new(number(1)),
|
||||
when_false: Box::new(number(2)),
|
||||
},
|
||||
numeric.clone(),
|
||||
),
|
||||
(
|
||||
"@combine(b: 2 [@second], a: 1 [@second])",
|
||||
a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(calculation),
|
||||
arguments: BTreeMap::from([
|
||||
(
|
||||
parameter,
|
||||
a::Expr::Quantity {
|
||||
magnitude: Exact::from_integer(1),
|
||||
unit: a::DefinitionRef::Local(second),
|
||||
},
|
||||
),
|
||||
(
|
||||
b,
|
||||
a::Expr::Quantity {
|
||||
magnitude: Exact::from_integer(2),
|
||||
unit: a::DefinitionRef::Local(second),
|
||||
},
|
||||
),
|
||||
]),
|
||||
},
|
||||
quantity.clone(),
|
||||
),
|
||||
(
|
||||
"sum([1, 2], 0)",
|
||||
a::Expr::Sum {
|
||||
values: Box::new(a::Expr::List(vec![number(1), number(2)])),
|
||||
initial: Box::new(number(0)),
|
||||
},
|
||||
numeric.clone(),
|
||||
),
|
||||
(
|
||||
"power(2, -2)",
|
||||
a::Expr::Power {
|
||||
base: Box::new(number(2)),
|
||||
exponent: -2,
|
||||
},
|
||||
numeric.clone(),
|
||||
),
|
||||
(
|
||||
"as_kind(@duration, 1 [@second])",
|
||||
a::Expr::AsKind {
|
||||
kind: a::DefinitionRef::Local(duration),
|
||||
value: Box::new(a::Expr::Quantity {
|
||||
magnitude: Exact::from_integer(1),
|
||||
unit: a::DefinitionRef::Local(second),
|
||||
}),
|
||||
},
|
||||
quantity.clone(),
|
||||
),
|
||||
];
|
||||
// Remove the original check's dependent quantity comparison: each case
|
||||
// installs a different declared result type on total_time below.
|
||||
project.elements.remove(&by_alias(&project, "budget"));
|
||||
for (source, expected, result_type) in cases {
|
||||
let mut budget = budget();
|
||||
let Binding::Expression(syntax) = super::super::parse(source, &mut budget).unwrap() else {
|
||||
panic!("expression")
|
||||
};
|
||||
let mut tables = Tables::default();
|
||||
tables.ordinary(&project, &budget).unwrap();
|
||||
let mut output = Lowered {
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
occurrences: Vec::new(),
|
||||
expressions: Vec::new(),
|
||||
};
|
||||
let is_parameter = source == "$a";
|
||||
let expression_owner = if is_parameter { calculation } else { owner };
|
||||
let actual = Resolver {
|
||||
tables: &tables,
|
||||
output: &mut output,
|
||||
budget: &budget,
|
||||
file: 3,
|
||||
owner: expression_owner,
|
||||
}
|
||||
.expression(
|
||||
&syntax,
|
||||
if is_parameter {
|
||||
Field::Body
|
||||
} else {
|
||||
Field::Base
|
||||
},
|
||||
is_parameter.then_some(calculation),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual, expected, "{source}");
|
||||
assert_eq!(output.expressions[0].span, syntax.span);
|
||||
assert!(output.expressions[0].parent.is_none());
|
||||
let mut candidate = project.clone();
|
||||
if is_parameter {
|
||||
let a::DefinitionBody::Calculation(definition) =
|
||||
&mut candidate.definitions.get_mut(&calculation).unwrap().body
|
||||
else {
|
||||
panic!("calculation")
|
||||
};
|
||||
definition.body = a::Binding::Expression(actual);
|
||||
} else {
|
||||
let SharedBody::EngineeringProperty {
|
||||
value_type,
|
||||
display_unit,
|
||||
base,
|
||||
..
|
||||
} = candidate
|
||||
.elements
|
||||
.get_mut(&owner)
|
||||
.unwrap()
|
||||
.body
|
||||
.shared_mut()
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("property")
|
||||
};
|
||||
*value_type = result_type;
|
||||
*display_unit = None;
|
||||
*base = a::Binding::Expression(actual);
|
||||
}
|
||||
admit(&candidate);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_sites_and_parameter_scopes_preserve_distinct_original_occurrences() {
|
||||
let source = OA.replace("base @combine(a: @receive_time, b: @dispatch_time);", "base @combine(a: @receive_time, /* first */ b: @dispatch_time) + @combine(/* second */ b: @dispatch_time, a: @receive_time);") + r#"
|
||||
calculation other "Other" id "00000000-0000-0000-0000-000000000020" {
|
||||
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Number;
|
||||
result Number; body $a;
|
||||
}
|
||||
quantity_kind collision "Global ID equals a scoped ID" id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" { dimension {}; algebra Linear; }
|
||||
calculation caller "Caller" id "00000000-0000-0000-0000-000000000021" {
|
||||
input a id "00000000-0000-0000-0000-000000000022" : Number;
|
||||
result Number; body @other(a: $a) + @other(a: 3);
|
||||
}
|
||||
"#;
|
||||
let mut budget = budget();
|
||||
let units = [unit(&source, 2, &mut budget)];
|
||||
let output = lower(&ordinary(ORDINARY_OA), &units, &budget).unwrap();
|
||||
let total = id("bd5b68e8-65fc-4e35-9071-a73061316a4b");
|
||||
let combine = id("fd7ce7ab-f789-40bc-b699-0c37f1b6902f");
|
||||
let labels: Vec<_> = output
|
||||
.occurrences
|
||||
.iter()
|
||||
.filter(|value| value.owner == total && value.role == Role::ArgumentLabel)
|
||||
.collect();
|
||||
assert_eq!(labels.len(), 4);
|
||||
assert_eq!(
|
||||
labels
|
||||
.iter()
|
||||
.map(|value| value.span.start)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len(),
|
||||
4
|
||||
);
|
||||
let sites: BTreeSet<_> = labels
|
||||
.iter()
|
||||
.map(|value| value.expression.unwrap())
|
||||
.collect();
|
||||
assert_eq!(sites.len(), 2);
|
||||
let paths: Vec<_> = sites
|
||||
.iter()
|
||||
.map(|site| output.expressions[*site].parent.unwrap().1)
|
||||
.collect();
|
||||
assert_eq!(paths, [Child::Left, Child::Right]);
|
||||
for label in labels {
|
||||
assert_eq!(label.field, Field::Base);
|
||||
assert!(
|
||||
matches!(label.target, Target::Parameter { calculation, .. } if calculation == combine)
|
||||
);
|
||||
assert!(matches!(
|
||||
&source[label.span.start..label.span.end],
|
||||
"a" | "b"
|
||||
));
|
||||
}
|
||||
let reused = a::ParameterId(id("28ff40b0-1b8f-4735-8536-5a6ab21b56cb"));
|
||||
let scopes: BTreeSet<_> = output
|
||||
.occurrences
|
||||
.iter()
|
||||
.filter_map(|occurrence| match occurrence.target {
|
||||
Target::Parameter {
|
||||
calculation,
|
||||
parameter,
|
||||
} if parameter == reused => Some(calculation),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
scopes,
|
||||
BTreeSet::from([combine, id("00000000-0000-0000-0000-000000000020")])
|
||||
);
|
||||
let caller = id("00000000-0000-0000-0000-000000000021");
|
||||
let use_in_caller = output
|
||||
.occurrences
|
||||
.iter()
|
||||
.find(|value| value.owner == caller && value.role == Role::ParameterUse)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
use_in_caller.target,
|
||||
Target::Parameter {
|
||||
calculation: caller,
|
||||
parameter: a::ParameterId(id("00000000-0000-0000-0000-000000000022"))
|
||||
}
|
||||
);
|
||||
for site in &output.expressions {
|
||||
assert_eq!(site.file, 2);
|
||||
assert!(source.get(site.span.start..site.span.end).is_some());
|
||||
}
|
||||
admit(&merge(ordinary(ORDINARY_OA), output));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_ids_require_stabilization_without_uuid_or_budget_churn() {
|
||||
for source in [
|
||||
r#"calculation pending "Pending" { result Number; body unbound; }"#,
|
||||
r#"calculation pending "Pending" id "00000000-0000-0000-0000-000000000040" { input value : Number; result Number; body $value; }"#,
|
||||
] {
|
||||
let mut budget = budget();
|
||||
let units = [unit(source, 3, &mut budget)];
|
||||
let usage = budget.usage;
|
||||
let skeleton = ordinary(ORDINARY_OA);
|
||||
let first = lower(&skeleton, &units, &budget).unwrap_err();
|
||||
assert_eq!(first, lower(&skeleton, &units, &budget).unwrap_err());
|
||||
assert!(first.error.message.contains("stabilization"));
|
||||
assert_eq!(budget.usage, usage);
|
||||
let declaration = &units[0].declarations[0];
|
||||
if declaration.header.identity.value.is_none() {
|
||||
assert!(declaration.header.identity.insertion.is_some());
|
||||
} else {
|
||||
let d::Body::Calculation { parameters, .. } = &declaration.body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
assert!(parameters[0].identity.value.is_none());
|
||||
assert!(parameters[0].identity.insertion.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_global_aliases_and_ids_fail_before_body_resolution() {
|
||||
let ordinary = ordinary(ORDINARY_OA);
|
||||
for (source, message) in [
|
||||
(
|
||||
OA.replace("quantity_kind duration", "quantity_kind respond"),
|
||||
"Duplicate global alias",
|
||||
),
|
||||
(
|
||||
OA.replace(
|
||||
"188b911c-5f8b-431c-9eea-2d41f3953767",
|
||||
"cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59",
|
||||
),
|
||||
"Duplicate global identity",
|
||||
),
|
||||
(format!("{OA}\n{OA}"), "Duplicate global"),
|
||||
] {
|
||||
let failed = failure(&source, &ordinary);
|
||||
assert_eq!(failed.file, Some(7));
|
||||
assert!(failed.error.message.contains(message), "{:?}", failed);
|
||||
assert!(!failed.error.message.contains("Unknown"));
|
||||
}
|
||||
let mut budget = budget();
|
||||
let units = [unit(OA, 3, &mut budget), unit(OA, 5, &mut budget)];
|
||||
let failed = lower(&ordinary, &units, &budget).unwrap_err();
|
||||
assert_eq!(failed.file, Some(5));
|
||||
assert!(failed.error.message.contains("Duplicate global identity"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_reference_domains_unknown_names_and_parameter_scope_fail_at_tokens() {
|
||||
let skeleton = ordinary(ORDINARY_OA);
|
||||
for (old, new, token, message) in [
|
||||
(
|
||||
"subject @receive",
|
||||
"subject @second",
|
||||
"second",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"Quantity(@duration)",
|
||||
"Quantity(@second)",
|
||||
"second",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"display_unit @second",
|
||||
"display_unit @duration",
|
||||
"duration",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"750 [@millisecond]",
|
||||
"750 [@duration]",
|
||||
"duration",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"base 1 [@second]",
|
||||
"base @respond",
|
||||
"respond",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"@combine(a:",
|
||||
"@second(a:",
|
||||
"second",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"@combine(a:",
|
||||
"@combine(unknown:",
|
||||
"unknown",
|
||||
"Unknown argument",
|
||||
),
|
||||
(
|
||||
"body $a + $b",
|
||||
"body $outside + $b",
|
||||
"outside",
|
||||
"Unknown parameter",
|
||||
),
|
||||
(
|
||||
"base 1 [@second]",
|
||||
"base $a",
|
||||
"a",
|
||||
"only valid in its calculation",
|
||||
),
|
||||
(
|
||||
"subject @receive",
|
||||
"subject @missing",
|
||||
"missing",
|
||||
"Unknown global",
|
||||
),
|
||||
(
|
||||
"documented_constraint none",
|
||||
"documented_constraint @respond",
|
||||
"respond",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
(
|
||||
"@receive_time = unbound",
|
||||
"@second = unbound",
|
||||
"second",
|
||||
"Wrong reference domain",
|
||||
),
|
||||
] {
|
||||
let source = OA.replace(old, new);
|
||||
assert_ne!(source, OA, "mutation {old}");
|
||||
let failed = failure(&source, &skeleton);
|
||||
assert!(
|
||||
failed.error.message.contains(message),
|
||||
"{new}: {:?}",
|
||||
failed
|
||||
);
|
||||
assert_eq!(
|
||||
&source[failed.error.span.start..failed.error.span.end],
|
||||
token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_non_element_categories_are_collected_and_never_property_fallbacks() {
|
||||
let mut skeleton = ordinary(ORDINARY_OA);
|
||||
let respond = by_alias(&skeleton, "respond");
|
||||
let receive = by_alias(&skeleton, "receive");
|
||||
let relation = Id::from_u128(100);
|
||||
skeleton.relations.insert(
|
||||
relation,
|
||||
crate::Relation {
|
||||
id: relation,
|
||||
alias: "link".into(),
|
||||
name: "Link".into(),
|
||||
description: String::new(),
|
||||
body: crate::RelationBody::new(
|
||||
crate::RelationKind::FunctionalExchange,
|
||||
respond,
|
||||
receive,
|
||||
),
|
||||
},
|
||||
);
|
||||
let diagram = skeleton.ensure_diagram(Layer::Oa);
|
||||
skeleton.diagrams.get_mut(&diagram).unwrap().alias = "view".into();
|
||||
let node = skeleton.place(diagram, respond, crate::Position { x: 0., y: 0. });
|
||||
skeleton
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.alias = "placement".into();
|
||||
let artifact = Id::from_u128(101);
|
||||
skeleton.artifact_links.insert(
|
||||
artifact,
|
||||
crate::ArtifactLink {
|
||||
id: artifact,
|
||||
alias: "document".into(),
|
||||
name: "Document".into(),
|
||||
description: String::new(),
|
||||
owner: respond,
|
||||
relation: crate::ArtifactRelation::References,
|
||||
uri: "urn:test".into(),
|
||||
revision: None,
|
||||
fragment: None,
|
||||
},
|
||||
);
|
||||
let trace = Id::from_u128(102);
|
||||
skeleton.traces.insert(
|
||||
trace,
|
||||
crate::Trace {
|
||||
id: trace,
|
||||
alias: "mapping".into(),
|
||||
name: "Mapping".into(),
|
||||
description: String::new(),
|
||||
oa: respond,
|
||||
sa: receive,
|
||||
kind: crate::TraceKind::Manual,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
// Resolution classification is independent of whole-model admission; the
|
||||
// synthetic trace is deliberately not asserted to be a valid OA/SA trace.
|
||||
let budget = budget();
|
||||
let mut tables = Tables::default();
|
||||
tables.ordinary(&skeleton, &budget).unwrap();
|
||||
for (alias, category) in [
|
||||
("response_budget", Category::Project),
|
||||
("link", Category::Relation),
|
||||
("view", Category::Diagram),
|
||||
("placement", Category::Node),
|
||||
("document", Category::Artifact),
|
||||
("mapping", Category::Trace),
|
||||
] {
|
||||
assert_eq!(tables.aliases[alias].category, category);
|
||||
let failed = failure(
|
||||
&OA.replace("base 1 [@second]", &format!("base @{alias}")),
|
||||
&skeleton,
|
||||
);
|
||||
assert!(
|
||||
failed.error.message.contains("Wrong reference domain"),
|
||||
"{:?}",
|
||||
failed
|
||||
);
|
||||
}
|
||||
let source = OA.replace("subject @receive", "subject @link");
|
||||
let mut budget = self::budget();
|
||||
let units = [unit(&source, 1, &mut budget)];
|
||||
let output = lower(&skeleton, &units, &budget).unwrap();
|
||||
let property = output
|
||||
.elements
|
||||
.values()
|
||||
.find(|value| value.alias == "receive_time")
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(property.body.shared(), Some(SharedBody::EngineeringProperty { subject: a::AnalysisSubject::Relation(value), .. }) if *value == relation)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_lowering_defers_type_purity_layer_and_argument_sets_to_admission() {
|
||||
for (old, new) in [
|
||||
("base 750 [@millisecond]", "base true"),
|
||||
("body $a + $b", "body @receive_time"),
|
||||
("a: @receive_time, b: @dispatch_time", "a: @receive_time"),
|
||||
("oa_property receive_time", "sa_property receive_time"),
|
||||
("base 1 [@second]", "base @dispatch_time"),
|
||||
] {
|
||||
let source = OA.replace(old, new);
|
||||
let mut budget = budget();
|
||||
let units = [unit(&source, 1, &mut budget)];
|
||||
let output = lower(&ordinary(ORDINARY_OA), &units, &budget).unwrap();
|
||||
let project = merge(ordinary(ORDINARY_OA), output);
|
||||
assert!(
|
||||
a::admit(&project, a::AdmissionLimits::REFERENCE).is_err(),
|
||||
"{new}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lowering_phase_bounds_count_ordinary_globals_and_scoped_parameters_together() {
|
||||
let ordinary = oracle::project(Layer::Oa);
|
||||
for (maximum, passes) in [(0, false), (16, false), (17, true)] {
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.entries = maximum;
|
||||
let budget = Budget::new(limits).unwrap();
|
||||
let result = lower(&ordinary, &[], &budget);
|
||||
assert_eq!(result.is_ok(), passes, "entry bound {maximum}");
|
||||
if let Err(failure) = result {
|
||||
assert!(failure.error.message.contains("lowering entry limit"));
|
||||
assert_eq!(failure.file, None);
|
||||
}
|
||||
assert_eq!(budget.usage, budget::Usage::default());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affine_kinds_optional_fields_list_signatures_and_unused_unbound_definitions_survive() {
|
||||
let source = r#"
|
||||
unit celsius "Celsius" id "00000000-0000-0000-0000-000000000061" {
|
||||
kind @point; scale 1; offset ratio(27315, 100); symbol "°C";
|
||||
}
|
||||
quantity_kind point "Temperature point" id "00000000-0000-0000-0000-000000000062" {
|
||||
dimension { temperature 1; }; algebra Affine(@interval);
|
||||
}
|
||||
quantity_kind interval "Temperature interval" id "00000000-0000-0000-0000-000000000063" {
|
||||
dimension { temperature 1; }; algebra Linear;
|
||||
}
|
||||
calculation unused "Unused list calculation" id "00000000-0000-0000-0000-000000000064" {
|
||||
input xs id "00000000-0000-0000-0000-000000000065" : List(Quantity(@interval));
|
||||
result List(Quantity(@interval)); body unbound; description "保留";
|
||||
}
|
||||
calculation empty "Empty calculation" id "00000000-0000-0000-0000-000000000066" {
|
||||
result Boolean; body unbound;
|
||||
}
|
||||
oa_property ambient "Ambient temperature" id "00000000-0000-0000-0000-000000000067" {
|
||||
subject @respond; type Quantity(@point); display_unit none; base 0 [@celsius]; parent none;
|
||||
}
|
||||
oa_check documented "Documented check" id "00000000-0000-0000-0000-000000000068" {
|
||||
subject @respond; documented_constraint @requirement; predicate unbound;
|
||||
}
|
||||
oa_analysis changed "Changed temperature" id "00000000-0000-0000-0000-000000000069" {
|
||||
subject @respond; base ModelBaseline; overrides { @ambient = unbound; };
|
||||
}
|
||||
"#;
|
||||
let mut skeleton = ordinary(ORDINARY_OA);
|
||||
let requirement = Id::from_u128(70);
|
||||
skeleton.elements.insert(
|
||||
requirement,
|
||||
Element {
|
||||
id: requirement,
|
||||
alias: "requirement".into(),
|
||||
name: "Requirement".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Oa,
|
||||
SharedBody::Constraint {
|
||||
expression: String::new(),
|
||||
constrained: Vec::new(),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
let mut budget = budget();
|
||||
let units = [unit(source, 8, &mut budget)];
|
||||
let output = lower(&skeleton, &units, &budget).unwrap();
|
||||
assert_eq!(output.definitions.len(), 5);
|
||||
let point = id("00000000-0000-0000-0000-000000000062");
|
||||
let interval = id("00000000-0000-0000-0000-000000000063");
|
||||
let a::DefinitionBody::QuantityKind(kind) = &output.definitions[&point].body else {
|
||||
panic!("quantity kind")
|
||||
};
|
||||
assert_eq!(
|
||||
kind.algebra,
|
||||
a::QuantityAlgebra::Affine {
|
||||
difference: a::DefinitionRef::Local(interval)
|
||||
}
|
||||
);
|
||||
let unused = &output.definitions[&id("00000000-0000-0000-0000-000000000064")];
|
||||
assert_eq!(unused.description, "保留");
|
||||
let a::DefinitionBody::Calculation(calculation) = &unused.body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
assert_eq!(calculation.body, a::Binding::Unbound);
|
||||
assert_eq!(
|
||||
calculation.parameters[0].value_type,
|
||||
a::AnalysisType::List(a::ScalarType::Quantity(a::DefinitionRef::Local(interval)))
|
||||
);
|
||||
assert_eq!(calculation.result, calculation.parameters[0].value_type);
|
||||
assert!(
|
||||
output
|
||||
.occurrences
|
||||
.iter()
|
||||
.any(|occurrence| occurrence.field == Field::DocumentedConstraint
|
||||
&& occurrence.target == Target::Global(requirement))
|
||||
);
|
||||
let override_id = id("00000000-0000-0000-0000-000000000067");
|
||||
assert!(output.occurrences.iter().any(|occurrence| occurrence.field
|
||||
== Field::Override(override_id)
|
||||
&& occurrence.role == Role::OverrideTarget));
|
||||
assert!(
|
||||
output
|
||||
.occurrences
|
||||
.iter()
|
||||
.any(|occurrence| occurrence.owner == point
|
||||
&& occurrence.field == Field::Algebra
|
||||
&& occurrence.target == Target::Global(interval))
|
||||
);
|
||||
admit(&merge(skeleton, output));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_file_keys_and_exhausted_shared_budget_cannot_enter_lowering() {
|
||||
let mut budget = budget();
|
||||
let units = [unit("", 3, &mut budget), unit("", 3, &mut budget)];
|
||||
let failed = lower(&ordinary(ORDINARY_OA), &units, &budget).unwrap_err();
|
||||
assert_eq!(failed.file, Some(3));
|
||||
assert!(failed.error.message.contains("Duplicate source file"));
|
||||
let failure = budget
|
||||
.bytes(usize::MAX, Span { start: 5, end: 6 })
|
||||
.unwrap_err();
|
||||
let failed = lower(&ordinary(ORDINARY_OA), &[], &budget).unwrap_err();
|
||||
assert_eq!(failed.error, failure);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterative_lowering_preserves_128_deep_and_1024_wide_accepted_expressions() {
|
||||
let deep = format!("{}1{}", "abs(".repeat(127), ")".repeat(127));
|
||||
let wide = std::iter::repeat_n("1", 1024)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let source = format!(
|
||||
r#"
|
||||
calculation deep "Deep" id "00000000-0000-0000-0000-000000000081" {{ result Number; body {deep}; }}
|
||||
calculation wide "Wide" id "00000000-0000-0000-0000-000000000082" {{ result List(Number); body [{wide}]; }}
|
||||
"#
|
||||
);
|
||||
let mut budget = budget();
|
||||
let units = [unit(&source, 1, &mut budget)];
|
||||
let usage = budget.usage;
|
||||
let output = lower(&ordinary(ORDINARY_OA), &units, &budget).unwrap();
|
||||
assert_eq!(output.expressions.len(), 128 + 1025);
|
||||
let a::DefinitionBody::Calculation(deep) = &output.definitions[&Id::from_u128(0x81)].body
|
||||
else {
|
||||
panic!("deep calculation")
|
||||
};
|
||||
let a::Binding::Expression(root) = &deep.body else {
|
||||
panic!("deep expression")
|
||||
};
|
||||
let mut current = root;
|
||||
let mut depth = 1;
|
||||
while let a::Expr::Unary { operand, .. } = current {
|
||||
depth += 1;
|
||||
current = operand;
|
||||
}
|
||||
assert_eq!(depth, 128);
|
||||
assert_eq!(current, &number(1));
|
||||
let a::DefinitionBody::Calculation(wide) = &output.definitions[&Id::from_u128(0x82)].body
|
||||
else {
|
||||
panic!("wide calculation")
|
||||
};
|
||||
let a::Binding::Expression(a::Expr::List(values)) = &wide.body else {
|
||||
panic!("wide list")
|
||||
};
|
||||
assert_eq!(values.len(), 1024);
|
||||
assert!(values.iter().all(|value| *value == number(1)));
|
||||
assert_eq!(budget.usage, usage);
|
||||
admit(&merge(ordinary(ORDINARY_OA), output));
|
||||
}
|
||||
Reference in New Issue
Block a user