Stage bounded typed source8 canonical and subtree writers

This commit is contained in:
2026-09-13 00:34:59 -07:00
parent bed5b0e832
commit f1d79df497
6 changed files with 1863 additions and 2 deletions
@@ -6,6 +6,10 @@ mod declarations;
mod lex;
mod lower;
mod syntax;
mod write;
#[cfg(test)]
mod model_oracle;
use super::Span;
use crate::analysis::{BinaryOp, UnaryOp};
@@ -2,8 +2,7 @@ use super::*;
use crate::language::ParseLimits;
use crate::language::analysis_source::{budget::Limits, cursor::Cursor};
#[path = "oracle.rs"]
mod oracle;
use super::super::model_oracle as oracle;
const OA: &str = include_str!("fixtures/typed-oa.arcadia");
const SA: &str = include_str!("fixtures/typed-sa.arcadia");
@@ -0,0 +1,664 @@
//! Private typed canonical output. Complete semantic admission belongs to the
//! caller. No public formatter, version dispatch or source edits use this yet.
mod expression;
pub(super) use expression::{Parent, Side};
use super::{budget, lower::Field};
use crate::analysis as a;
use crate::analysis_numeric::Exact;
use crate::{Id, Kind, Layer, Project, SharedBody};
use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
#[derive(Clone, Copy)]
pub(super) struct Limits {
pub bytes: usize,
pub entries: usize,
}
impl Limits {
pub fn source8(caller: crate::language::ParseLimits) -> Self {
let limits = budget::Limits::source8(caller);
Self {
bytes: limits.bytes,
entries: limits.entries,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Error {
pub owner: Option<Id>,
pub field: &'static str,
pub message: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct Usage {
pub bytes: usize,
pub entries: usize,
}
struct Sink {
buffer: Vec<u8>,
written: usize,
maximum: usize,
}
impl Sink {
fn room(&self, bytes: usize) -> std::io::Result<()> {
if bytes > self.maximum.saturating_sub(self.written) {
return Err(std::io::Error::other(
"Source8 writer output byte limit exceeded.",
));
}
Ok(())
}
}
impl Write for Sink {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.room(bytes.len())?;
self.buffer.extend_from_slice(bytes);
self.written += bytes.len();
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub(super) struct Writer<'p> {
project: &'p Project,
parameters: BTreeMap<Id, BTreeMap<a::ParameterId, &'p a::Parameter>>,
limits: Limits,
entries: usize,
sink: Sink,
owner: Option<Id>,
field: &'static str,
failed: Option<Error>,
}
impl<'p> Writer<'p> {
pub fn new(project: &'p Project, limits: Limits) -> Result<Self, Error> {
let hard = Limits::source8(crate::language::ParseLimits::UNLIMITED);
if limits.bytes > hard.bytes || limits.entries > hard.entries {
return Err(Error {
owner: None,
field: "writer",
message: "Writer limits exceed the source8 profile.".into(),
});
}
let mut writer = Self {
project,
parameters: BTreeMap::new(),
limits,
entries: 0,
sink: Sink {
buffer: Vec::new(),
written: 0,
maximum: limits.bytes,
},
owner: None,
field: "writer",
failed: None,
};
for definition in project.definitions.values() {
writer.owner = Some(definition.id);
writer.field = "parameters";
writer.entry()?;
if let a::DefinitionBody::Calculation(calculation) = &definition.body {
let mut parameters = BTreeMap::new();
let mut names = BTreeSet::new();
for parameter in &calculation.parameters {
writer.entry()?;
if parameters.contains_key(&parameter.id)
|| !names.insert(parameter.name.as_str())
{
return Err(writer.error("Duplicate scoped parameter identity or name."));
}
parameters.insert(parameter.id, parameter);
}
writer.parameters.insert(definition.id, parameters);
}
}
Ok(writer)
}
pub fn usage(&self) -> Usage {
Usage {
bytes: self.sink.written,
entries: self.entries,
}
}
fn error(&self, message: impl Into<String>) -> Error {
Error {
owner: self.owner,
field: self.field,
message: message.into(),
}
}
fn entry(&mut self) -> Result<(), Error> {
if self.entries == self.limits.entries {
return Err(self.error("Source8 writer entry limit exceeded."));
}
self.entries += 1;
Ok(())
}
fn pending(&self, count: usize) -> Result<(), Error> {
if count > self.limits.entries {
return Err(self.error("Source8 writer pending work limit exceeded."));
}
Ok(())
}
fn text(&mut self, text: &str) -> Result<(), Error> {
self.sink
.write_all(text.as_bytes())
.map_err(|cause| self.error(cause.to_string()))
}
fn quote(&mut self, text: &str) -> Result<(), Error> {
// Identical serde_json string escaping to language::quote, streamed
// directly to the checked sink instead of an unbounded scratch String.
self.sink
.room(text.len().saturating_add(2))
.map_err(|cause| self.error(cause.to_string()))?;
serde_json::to_writer(&mut self.sink, text).map_err(|cause| self.error(cause.to_string()))
}
fn name(&mut self, name: &str) -> Result<(), Error> {
self.sink
.room(name.len())
.map_err(|cause| self.error(cause.to_string()))?;
if !crate::valid_alias(name) {
return Err(self.error("Invalid alias or parameter name."));
}
self.text(name)
}
fn exact(&mut self, exact: &Exact) -> Result<(), Error> {
// At most two already-bounded 4096-bit components, never an Expr-sized
// temporary or a second parse/normalization of the stored value.
self.text(&exact.canonical_literal())
}
fn run(
&mut self,
owner: Id,
field: &'static str,
emit: impl FnOnce(&mut Self) -> Result<(), Error>,
) -> Result<String, Error> {
if let Some(error) = &self.failed {
return Err(error.clone());
}
self.owner = Some(owner);
self.field = field;
debug_assert!(self.sink.buffer.is_empty());
if let Err(error) = emit(self) {
self.sink.buffer.clear();
self.failed = Some(error.clone());
return Err(error);
}
Ok(String::from_utf8(std::mem::take(&mut self.sink.buffer))
.expect("text and JSON string writes are UTF-8"))
}
fn definition_name(
&self,
reference: a::DefinitionRef,
kind: DefinitionKind,
) -> Result<&'p str, Error> {
let definition = self
.project
.definitions
.get(&reference.local())
.ok_or_else(|| self.error("Unknown definition reference."))?;
let matches = matches!(
(&definition.body, kind),
(
a::DefinitionBody::QuantityKind(_),
DefinitionKind::QuantityKind
) | (a::DefinitionBody::Unit(_), DefinitionKind::Unit)
| (
a::DefinitionBody::Calculation(_),
DefinitionKind::Calculation
)
);
if !matches {
return Err(self.error("Wrong definition reference domain."));
}
Ok(&definition.alias)
}
fn definition_ref(
&mut self,
reference: a::DefinitionRef,
kind: DefinitionKind,
) -> Result<(), Error> {
let alias = self.definition_name(reference, kind)?;
self.text("@")?;
self.name(alias)
}
fn property_name(&self, id: Id) -> Result<&'p str, Error> {
self.project
.elements
.get(&id)
.filter(|value| value.kind() == Kind::EngineeringProperty)
.map(|value| value.alias.as_str())
.ok_or_else(|| self.error("Unknown property reference or wrong reference domain."))
}
fn subject(&mut self, subject: a::AnalysisSubject) -> Result<(), Error> {
let alias = match subject {
a::AnalysisSubject::Element(id) => self
.project
.elements
.get(&id)
.filter(|value| !value.kind().is_analysis())
.map(|value| value.alias.as_str()),
a::AnalysisSubject::Relation(id) => self
.project
.relations
.get(&id)
.map(|value| value.alias.as_str()),
}
.ok_or_else(|| self.error("Unknown architectural subject or wrong reference domain."))?;
self.text("@")?;
self.name(alias)
}
fn optional_subject(&mut self, subject: Option<a::AnalysisSubject>) -> Result<(), Error> {
if let Some(subject) = subject {
self.subject(subject)
} else {
self.text("none")
}
}
fn type_value(&mut self, value: &a::AnalysisType) -> Result<(), Error> {
self.entry()?;
let (scalar, list) = match value {
a::AnalysisType::Scalar(value) => (value, false),
a::AnalysisType::List(value) => (value, true),
};
if list {
self.text("List(")?;
}
match scalar {
a::ScalarType::Boolean => self.text("Boolean")?,
a::ScalarType::Number => self.text("Number")?,
a::ScalarType::Quantity(kind) => {
self.text("Quantity(")?;
self.definition_ref(*kind, DefinitionKind::QuantityKind)?;
self.text(")")?;
}
}
if list {
self.text(")")?;
}
Ok(())
}
pub fn value_type(&mut self, owner: Id, value: &a::AnalysisType) -> Result<String, Error> {
self.run(owner, "type", |writer| writer.type_value(value))
}
fn model_binding(
&self,
owner: Id,
field: Field,
) -> Result<(&'p a::Binding, Option<Id>), Error> {
if field == Field::Body {
if let Some(a::Definition {
body: a::DefinitionBody::Calculation(calculation),
..
}) = self.project.definitions.get(&owner)
{
return Ok((&calculation.body, Some(owner)));
}
} else if let Some(element) = self.project.elements.get(&owner) {
match (element.shared(), field) {
(Some(SharedBody::EngineeringProperty { base, .. }), Field::Base) => {
return Ok((base, None));
}
(Some(SharedBody::AnalysisCheck { predicate, .. }), Field::Predicate) => {
return Ok((predicate, None));
}
(Some(SharedBody::AnalysisCase { overrides, .. }), Field::Override(property)) => {
return overrides
.get(&property)
.map(|binding| (binding, None))
.ok_or_else(|| self.error("Missing override binding."));
}
_ => {}
}
}
Err(self.error("Binding field does not belong to the requested owner."))
}
fn binding_value(
&mut self,
binding: &a::Binding,
calculation: Option<Id>,
) -> Result<(), Error> {
match binding {
a::Binding::Unbound => {
self.entry()?;
self.text("unbound")
}
a::Binding::Expression(expression) => {
self.emit_expression(expression, calculation, Parent::Root)
}
}
}
pub fn binding(&mut self, owner: Id, field: Field) -> Result<String, Error> {
self.run(owner, binding_field(field), |writer| {
let (binding, scope) = writer.model_binding(owner, field)?;
writer.binding_value(binding, scope)
})
}
pub fn expression(
&mut self,
owner: Id,
field: Field,
expression: &a::Expr,
parent: Parent,
) -> Result<String, Error> {
self.run(owner, binding_field(field), |writer| {
let (_, scope) = writer.model_binding(owner, field)?;
writer.emit_expression(expression, scope, parent)
})
}
fn parameter_value(&mut self, calculation: Id, id: a::ParameterId) -> Result<(), Error> {
self.entry()?;
let parameter = *self
.parameters
.get(&calculation)
.and_then(|parameters| parameters.get(&id))
.ok_or_else(|| self.error("Unknown scoped parameter."))?;
self.text("input ")?;
self.name(&parameter.name)?;
self.text(" id \"")?;
self.text(&parameter.id.0.to_string())?;
self.text("\" : ")?;
self.type_value(&parameter.value_type)?;
self.text(";")
}
pub fn parameter(
&mut self,
calculation: Id,
parameter: a::ParameterId,
) -> Result<String, Error> {
self.run(calculation, "input", |writer| {
writer.parameter_value(calculation, parameter)
})
}
fn override_value(&mut self, property: Id, binding: &a::Binding) -> Result<(), Error> {
self.entry()?;
let alias = self.property_name(property)?;
self.text("@")?;
self.name(alias)?;
self.text(" = ")?;
self.binding_value(binding, None)?;
self.text(";")
}
pub fn override_entry(&mut self, case: Id, property: Id) -> Result<String, Error> {
self.run(case, "overrides", |writer| {
let (binding, _) = writer.model_binding(case, Field::Override(property))?;
writer.override_value(property, binding)
})
}
fn header(
&mut self,
word: &str,
alias: &str,
name: &str,
id: Id,
description: &str,
) -> Result<(), Error> {
self.entry()?;
self.text(" ")?;
self.text(word)?;
self.text(" ")?;
self.name(alias)?;
self.text(" ")?;
self.quote(name)?;
self.text(" id \"")?;
self.text(&id.to_string())?;
self.text("\" {\n")?;
self.start_field("description")?;
self.quote(description)?;
self.text(";\n")
}
fn start_field(&mut self, name: &'static str) -> Result<(), Error> {
self.field = name;
self.entry()?;
self.text(" ")?;
self.text(name)?;
self.text(" ")
}
pub fn definition(&mut self, id: Id) -> Result<String, Error> {
self.run(id, "definition", |writer| {
let project = writer.project;
let definition = project
.definitions
.get(&id)
.ok_or_else(|| writer.error("Unknown definition."))?;
let word = match &definition.body {
a::DefinitionBody::QuantityKind(_) => "quantity_kind",
a::DefinitionBody::Unit(_) => "unit",
a::DefinitionBody::Calculation(_) => "calculation",
};
writer.header(
word,
&definition.alias,
&definition.name,
id,
&definition.description,
)?;
match &definition.body {
a::DefinitionBody::QuantityKind(kind) => {
writer.start_field("dimension")?;
writer.text("{")?;
const AXES: [&str; 7] = [
"length",
"mass",
"time",
"electric_current",
"temperature",
"amount",
"luminous_intensity",
];
let mut nonzero = false;
for (axis, exponent) in AXES.into_iter().zip(kind.dimension.exponents()) {
if exponent != 0 {
writer.entry()?;
writer.text(" ")?;
writer.text(axis)?;
writer.text(" ")?;
writer.text(&exponent.to_string())?;
writer.text(";")?;
nonzero = true;
}
}
if nonzero {
writer.text(" ")?;
}
writer.text("};\n")?;
writer.start_field("algebra")?;
match kind.algebra {
a::QuantityAlgebra::Linear => writer.text("Linear")?,
a::QuantityAlgebra::Affine { difference } => {
writer.text("Affine(")?;
writer.definition_ref(difference, DefinitionKind::QuantityKind)?;
writer.text(")")?;
}
}
writer.text(";\n")?;
}
a::DefinitionBody::Unit(unit) => {
writer.start_field("kind")?;
writer.definition_ref(unit.kind, DefinitionKind::QuantityKind)?;
writer.text(";\n")?;
writer.start_field("scale")?;
writer.exact(&unit.scale)?;
writer.text(";\n")?;
writer.start_field("offset")?;
writer.exact(&unit.offset)?;
writer.text(";\n")?;
writer.start_field("symbol")?;
writer.quote(&unit.symbol)?;
writer.text(";\n")?;
}
a::DefinitionBody::Calculation(calculation) => {
for parameter in &calculation.parameters {
writer.field = "input";
writer.text(" ")?;
writer.parameter_value(id, parameter.id)?;
writer.text("\n")?;
}
writer.start_field("result")?;
writer.type_value(&calculation.result)?;
writer.text(";\n")?;
writer.start_field("body")?;
writer.binding_value(&calculation.body, Some(id))?;
writer.text(";\n")?;
}
}
writer.text(" }\n\n")
})
}
pub fn analysis_element(&mut self, id: Id) -> Result<String, Error> {
self.run(id, "element", |writer| {
let project = writer.project;
let element = project
.elements
.get(&id)
.ok_or_else(|| writer.error("Unknown analysis element."))?;
let word = match (element.kind(), element.layer()) {
(Kind::EngineeringProperty, Layer::Oa) => "oa_property",
(Kind::EngineeringProperty, Layer::Sa) => "sa_property",
(Kind::AnalysisCheck, Layer::Oa) => "oa_check",
(Kind::AnalysisCheck, Layer::Sa) => "sa_check",
(Kind::AnalysisCase, Layer::Oa) => "oa_analysis",
(Kind::AnalysisCase, Layer::Sa) => "sa_analysis",
_ => return Err(writer.error("Expected an analysis element.")),
};
if element.parent.is_some() {
return Err(writer.error("Analysis declarations must be roots."));
}
writer.header(
word,
&element.alias,
&element.name,
id,
&element.description,
)?;
writer.start_field("parent")?;
writer.text("none;\n")?;
match element.shared().expect("analysis kind is a shared body") {
SharedBody::EngineeringProperty {
subject,
value_type,
display_unit,
base,
} => {
writer.start_field("subject")?;
writer.subject(*subject)?;
writer.text(";\n")?;
writer.start_field("type")?;
writer.type_value(value_type)?;
writer.text(";\n")?;
writer.start_field("display_unit")?;
if let Some(unit) = display_unit {
writer.definition_ref(*unit, DefinitionKind::Unit)?;
} else {
writer.text("none")?;
}
writer.text(";\n")?;
writer.start_field("base")?;
writer.binding_value(base, None)?;
writer.text(";\n")?;
}
SharedBody::AnalysisCheck {
subject,
documented_constraint,
predicate,
} => {
writer.start_field("subject")?;
writer.optional_subject(*subject)?;
writer.text(";\n")?;
writer.start_field("documented_constraint")?;
if let Some(id) = documented_constraint {
let alias = project
.elements
.get(id)
.filter(|element| element.kind() == Kind::Constraint)
.map(|element| element.alias.as_str())
.ok_or_else(|| {
writer.error("Unknown constraint or wrong reference domain.")
})?;
writer.text("@")?;
writer.name(alias)?;
} else {
writer.text("none")?;
}
writer.text(";\n")?;
writer.start_field("predicate")?;
writer.binding_value(predicate, None)?;
writer.text(";\n")?;
}
SharedBody::AnalysisCase {
subject,
base: a::BaselineBase::ModelBaseline,
overrides,
} => {
writer.start_field("subject")?;
writer.optional_subject(*subject)?;
writer.text(";\n")?;
writer.start_field("base")?;
writer.text("ModelBaseline;\n")?;
writer.start_field("overrides")?;
if overrides.is_empty() {
writer.text("{};\n")?;
} else {
writer.text("{\n")?;
for (property, binding) in overrides {
writer.text(" ")?;
writer.override_value(*property, binding)?;
writer.text("\n")?;
}
writer.text(" };\n")?;
}
}
_ => return Err(writer.error("Expected an analysis body.")),
}
writer.text(" }\n\n")
})
}
pub fn typed_declarations(&mut self) -> Result<Vec<(Id, String)>, Error> {
if let Some(error) = &self.failed {
return Err(error.clone());
}
let project = self.project;
let mut declarations = Vec::new();
for id in project.definitions.keys() {
declarations.push((*id, self.definition(*id)?));
}
for element in project
.elements
.values()
.filter(|element| element.kind().is_analysis())
{
declarations.push((element.id, self.analysis_element(element.id)?));
}
Ok(declarations)
}
}
#[derive(Clone, Copy)]
enum DefinitionKind {
QuantityKind,
Unit,
Calculation,
}
fn binding_field(field: Field) -> &'static str {
match field {
Field::Body => "body",
Field::Base => "base",
Field::Predicate => "predicate",
Field::Override(_) => "overrides",
_ => "binding",
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,284 @@
use super::*;
#[derive(Clone, Copy)]
pub enum Side {
Left,
Right,
}
#[derive(Clone, Copy)]
pub enum Parent {
Root,
Binary(a::BinaryOp, Side),
Negate,
}
fn operator(op: a::BinaryOp) -> (&'static str, u8) {
use a::BinaryOp::*;
match op {
Or => (" or ", 1),
And => (" and ", 2),
Equal => (" == ", 3),
NotEqual => (" != ", 3),
Less => (" < ", 4),
LessOrEqual => (" <= ", 4),
Greater => (" > ", 4),
GreaterOrEqual => (" >= ", 4),
Add => (" + ", 5),
Subtract => (" - ", 5),
Multiply => (" * ", 6),
Divide => (" / ", 6),
}
}
fn comparison(op: a::BinaryOp) -> bool {
matches!(operator(op).1, 3 | 4)
}
fn group(expression: &a::Expr, parent: Parent) -> bool {
let a::Expr::Binary { op, .. } = expression else {
return false;
};
match parent {
Parent::Root => false,
Parent::Negate => true,
Parent::Binary(parent, side) => {
(comparison(*op) && comparison(parent))
|| operator(*op).1 < operator(parent).1
|| (operator(*op).1 == operator(parent).1 && matches!(side, Side::Right))
}
}
}
#[derive(Clone, Copy)]
enum Task<'a> {
Expression(&'a a::Expr, Parent, usize),
Text(&'static str),
Alias(&'a str),
Literal(&'a Exact),
Exponent(i8),
}
fn schedule<'a>(
writer: &Writer<'_>,
tasks: &mut Vec<Task<'a>>,
ordered: &[Task<'a>],
) -> Result<(), Error> {
writer.pending(tasks.len().saturating_add(ordered.len()))?;
tasks.extend(ordered.iter().rev().copied());
Ok(())
}
impl Writer<'_> {
pub(super) fn emit_expression(
&mut self,
expression: &a::Expr,
calculation: Option<Id>,
parent: Parent,
) -> Result<(), Error> {
self.pending(1)?;
let mut tasks = vec![Task::Expression(expression, parent, 1)];
while let Some(task) = tasks.pop() {
match task {
Task::Text(text) => self.text(text)?,
Task::Alias(name) => {
self.entry()?;
self.name(name)?;
}
Task::Literal(exact) => self.exact(exact)?,
Task::Exponent(exponent) => self.text(&exponent.to_string())?,
Task::Expression(expression, parent, depth) => {
self.entry()?;
if depth > a::MAX_EXPRESSION_DEPTH {
return Err(self.error("Expression depth exceeds128."));
}
if group(expression, parent) {
self.text("(")?;
schedule(self, &mut tasks, &[Task::Text(")")])?;
}
use Task::{Alias, Exponent, Expression as Child, Literal, Text};
use a::Expr as E;
match expression {
E::Boolean(value) => self.text(if *value { "true" } else { "false" })?,
E::Number(value) => self.exact(value)?,
E::Quantity { magnitude, unit } => {
let alias = self.definition_name(*unit, DefinitionKind::Unit)?;
schedule(
self,
&mut tasks,
&[Literal(magnitude), Text(" [@"), Alias(alias), Text("]")],
)?;
}
E::Property(id) => {
let alias = self.property_name(*id)?;
schedule(self, &mut tasks, &[Text("@"), Alias(alias)])?;
}
E::Parameter(parameter) => {
let calculation = calculation.ok_or_else(|| {
self.error("Parameter use requires an owning calculation.")
})?;
let parameter = *self
.parameters
.get(&calculation)
.and_then(|parameters| parameters.get(parameter))
.ok_or_else(|| {
self.error("Unknown parameter in owning calculation.")
})?;
schedule(self, &mut tasks, &[Text("$"), Alias(&parameter.name)])?;
}
E::List(items) => {
if items.len() > a::MAX_LIST_LENGTH {
return Err(self.error("List exceeds1024 items."));
}
self.pending(
tasks
.len()
.saturating_add(items.len().saturating_mul(2))
.saturating_add(1),
)?;
self.text("[")?;
tasks.push(Text("]"));
for (index, item) in items.iter().enumerate().rev() {
tasks.push(Child(item, Parent::Root, depth + 1));
if index != 0 {
tasks.push(Text(", "));
}
}
}
E::Unary { op, operand } => match op {
a::UnaryOp::Negate => schedule(
self,
&mut tasks,
&[Text("- "), Child(operand, Parent::Negate, depth + 1)],
)?,
a::UnaryOp::Absolute | a::UnaryOp::Not => schedule(
self,
&mut tasks,
&[
Text(if *op == a::UnaryOp::Absolute {
"abs("
} else {
"not("
}),
Child(operand, Parent::Root, depth + 1),
Text(")"),
],
)?,
},
E::Binary { op, left, right } => schedule(
self,
&mut tasks,
&[
Child(left, Parent::Binary(*op, Side::Left), depth + 1),
Text(operator(*op).0),
Child(right, Parent::Binary(*op, Side::Right), depth + 1),
],
)?,
E::If {
condition,
when_true,
when_false,
} => schedule(
self,
&mut tasks,
&[
Text("if("),
Child(condition, Parent::Root, depth + 1),
Text(", "),
Child(when_true, Parent::Root, depth + 1),
Text(", "),
Child(when_false, Parent::Root, depth + 1),
Text(")"),
],
)?,
E::Call {
definition,
arguments,
} => {
let alias =
self.definition_name(*definition, DefinitionKind::Calculation)?;
let project = self.project;
let a::DefinitionBody::Calculation(callee) =
&project.definitions[&definition.local()].body
else {
unreachable!("checked calculation domain")
};
if arguments.len() != callee.parameters.len()
|| callee
.parameters
.iter()
.any(|parameter| !arguments.contains_key(&parameter.id))
{
return Err(self.error(
"Call argument IDs must match the called signature exactly.",
));
}
self.pending(
tasks
.len()
.saturating_add(arguments.len().saturating_mul(4))
.saturating_add(1),
)?;
self.entry()?;
self.text("@")?;
self.name(alias)?;
self.text("(")?;
tasks.push(Text(")"));
for (index, parameter) in callee.parameters.iter().enumerate().rev() {
tasks.push(Child(
&arguments[&parameter.id],
Parent::Root,
depth + 1,
));
tasks.push(Text(": "));
tasks.push(Alias(&parameter.name));
if index != 0 {
tasks.push(Text(", "));
}
}
}
E::Sum { values, initial } => schedule(
self,
&mut tasks,
&[
Text("sum("),
Child(values, Parent::Root, depth + 1),
Text(", "),
Child(initial, Parent::Root, depth + 1),
Text(")"),
],
)?,
E::Power { base, exponent } => {
if !(-16..=16).contains(exponent) {
return Err(self.error("Power exponent exceeds -16..16."));
}
schedule(
self,
&mut tasks,
&[
Text("power("),
Child(base, Parent::Root, depth + 1),
Text(", "),
Exponent(*exponent),
Text(")"),
],
)?;
}
E::AsKind { kind, value } => {
let alias =
self.definition_name(*kind, DefinitionKind::QuantityKind)?;
schedule(
self,
&mut tasks,
&[
Text("as_kind(@"),
Alias(alias),
Text(", "),
Child(value, Parent::Root, depth + 1),
Text(")"),
],
)?;
}
}
}
}
}
Ok(())
}
}
@@ -0,0 +1,910 @@
use super::super::{cursor::Cursor, declarations, lex, lower, model_oracle};
use super::*;
use crate::language::ParseLimits;
use a::{
AnalysisType, BinaryOp, Binding, DefinitionBody, DefinitionRef, Expr, ParameterId, ScalarType,
UnaryOp,
};
fn limits() -> Limits {
Limits::source8(ParseLimits::UNLIMITED)
}
fn writer(project: &Project) -> Writer<'_> {
Writer::new(project, limits()).unwrap()
}
fn id(text: &str) -> Id {
Id::parse_str(text).unwrap()
}
fn object(project: &Project, alias: &str) -> Id {
project
.identities()
.into_iter()
.find(|(_, name)| *name == alias)
.unwrap()
.0
}
fn number(value: i64) -> Expr {
Expr::Number(Exact::from_integer(value))
}
fn unary(op: UnaryOp, value: Expr) -> Expr {
Expr::Unary {
op,
operand: Box::new(value),
}
}
fn binary(op: BinaryOp, left: Expr, right: Expr) -> Expr {
Expr::Binary {
op,
left: Box::new(left),
right: Box::new(right),
}
}
fn quantity(value: i64, unit: Id) -> Expr {
Expr::Quantity {
magnitude: Exact::from_integer(value),
unit: DefinitionRef::Local(unit),
}
}
fn budget() -> budget::Budget {
budget::Budget::new(budget::Limits::source8(ParseLimits::UNLIMITED)).unwrap()
}
fn unit(source: &str, budget: &mut budget::Budget) -> Result<lower::Unit, super::super::Error> {
let tokens = lex::tokens(source, budget)?;
let mut cursor = Cursor::new(source, &tokens, budget);
let mut parsed = Vec::new();
while cursor.cursor < tokens.len() {
parsed.push(declarations::parse(&mut cursor, 8, None)?);
}
Ok(lower::Unit {
file: 1,
declarations: parsed,
})
}
fn merge(mut project: Project, lowered: lower::Lowered) -> Project {
project.definitions.extend(lowered.definitions);
project.elements.extend(lowered.elements);
project
}
fn parse_typed(project: Project, source: &str) -> Project {
let mut budget = budget();
let unit = unit(source, &mut budget).unwrap();
let lowered = lower::lower(&project, &[unit], &budget).unwrap();
merge(project, lowered)
}
fn skeleton(project: &Project) -> Project {
let mut project = project.clone();
project.definitions.clear();
project
.elements
.retain(|_, element| !element.kind().is_analysis());
project
}
fn canonical(project: &Project) -> String {
writer(project)
.typed_declarations()
.unwrap()
.into_iter()
.map(|(_, text)| text)
.collect()
}
fn read_expression(project: &Project, source: &str) -> Expr {
// A staged syntax/lowering wrapper, not a whole public source8 parser.
let source = format!(
r#"calculation writer_probe "Probe" id "00000000-0000-0000-0000-000000000099" {{
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Quantity(@duration);
input b id "7a47cc1e-8ae5-4551-b2d8-8a68108e0976" : Quantity(@duration);
result Number; body {source};
}}"#
);
let mut parsed = parse_typed(project.clone(), &source);
let definition = parsed.definitions.remove(&Id::from_u128(0x99)).unwrap();
let DefinitionBody::Calculation(calculation) = definition.body else {
panic!("calculation")
};
let Binding::Expression(expression) = calculation.body else {
panic!("expression")
};
expression
}
#[test]
fn typed_oa_sa_canonical_roundtrips_match_independent_projects() {
for (layer, ordinary_source) in [
(
Layer::Oa,
include_str!("../lower/fixtures/ordinary-oa.arcadia"),
),
(
Layer::Sa,
include_str!("../lower/fixtures/ordinary-sa.arcadia"),
),
] {
let expected = model_oracle::project(layer);
for (alias, name) in model_oracle::NAMES {
let id = object(&expected, alias);
assert_eq!(
expected
.definitions
.get(&id)
.map(|value| value.name.as_str())
.or_else(|| expected.elements.get(&id).map(|value| value.name.as_str()))
.unwrap(),
name
);
}
let text = canonical(&expected);
let actual = parse_typed(crate::language::parse(ordinary_source).unwrap(), &text);
assert_eq!(actual, expected);
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
assert_eq!(canonical(&actual), text);
assert!(
!text.contains("engineering_property")
&& !text.contains("analysis_case")
&& !text.contains("analysis_check")
);
let prefix = if layer == Layer::Oa { "oa" } else { "sa" };
for word in ["property", "check", "analysis"] {
assert!(text.contains(&format!("{prefix}_{word} ")));
}
}
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
}
#[test]
fn typed_declaration_order_and_required_empty_values_are_canonical() {
let mut project = model_oracle::project(Layer::Oa);
let duration = object(&project, "duration");
let total = object(&project, "total_time");
if let Some(SharedBody::EngineeringProperty { display_unit, .. }) =
project.elements.get_mut(&total).unwrap().body.shared_mut()
{
*display_unit = None;
}
let empty_case = object(&project, "unmeasured");
if let Some(SharedBody::AnalysisCase { overrides, .. }) = project
.elements
.get_mut(&empty_case)
.unwrap()
.body
.shared_mut()
{
overrides.clear();
}
let degraded = object(&project, "degraded");
let dispatch = object(&project, "dispatch_time");
let second = object(&project, "second");
if let Some(SharedBody::AnalysisCase { overrides, .. }) = project
.elements
.get_mut(&degraded)
.unwrap()
.body
.shared_mut()
{
overrides.insert(dispatch, Binding::Expression(quantity(1, second)));
}
let definitions = [
(
100,
"dimensionless",
DefinitionBody::QuantityKind(a::QuantityKindDefinition {
dimension: crate::analysis_numeric::Dimension::ZERO,
algebra: a::QuantityAlgebra::Linear,
}),
),
(
101,
"point",
DefinitionBody::QuantityKind(a::QuantityKindDefinition {
dimension: crate::analysis_numeric::Dimension::new([0, 0, 1, 0, 0, 0, 0]).unwrap(),
algebra: a::QuantityAlgebra::Affine {
difference: DefinitionRef::Local(duration),
},
}),
),
(
102,
"point_unit",
DefinitionBody::Unit(a::UnitDefinition {
kind: DefinitionRef::Local(Id::from_u128(101)),
scale: Exact::from_integer(1),
offset: Exact::from_integer(-2),
symbol: "\"café\"\n雪".into(),
}),
),
(
103,
"unused",
DefinitionBody::Calculation(a::CalculationDefinition {
parameters: Vec::new(),
result: AnalysisType::List(ScalarType::Number),
body: Binding::Unbound,
}),
),
];
for (id, alias, body) in definitions {
let id = Id::from_u128(id);
project.definitions.insert(
id,
a::Definition {
id,
alias: alias.into(),
name: alias.into(),
description: String::new(),
body,
},
);
}
let mut writer = writer(&project);
let declarations = writer.typed_declarations().unwrap();
let ids: Vec<_> = declarations.iter().map(|(id, _)| *id).collect();
let expected_ids: Vec<_> = project
.definitions
.keys()
.copied()
.chain(
project
.elements
.values()
.filter(|element| element.kind().is_analysis())
.map(|element| element.id),
)
.collect();
assert_eq!(ids, expected_ids);
let text: String = declarations.into_iter().map(|(_, text)| text).collect();
assert!(text.find("@receive_time =").unwrap() < text.find("@dispatch_time =").unwrap());
for expected in [
"dimension {};",
"dimension { time 1; };",
"algebra Affine(@duration);",
"offset ratio(-2, 1);",
"result List(Number);",
"body unbound;",
"display_unit none;",
"overrides {};",
"parent none;",
"subject none;",
"documented_constraint none;",
] {
assert!(text.contains(expected), "{expected}");
}
assert_eq!(
text.matches("\" id \"").count(),
project.definitions.len() + 7
);
let actual = parse_typed(skeleton(&project), &text);
assert_eq!(actual, project);
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
assert_eq!(canonical(&actual), text);
assert_eq!(
self::writer(&project)
.override_entry(empty_case, total)
.unwrap_err()
.field,
"overrides"
);
}
#[test]
fn all_expr_variants_and_precedence_preserve_exact_trees() {
let mut project = model_oracle::project(Layer::Oa);
let second = object(&project, "second");
let duration = object(&project, "duration");
let property = object(&project, "receive_time");
let total = object(&project, "total_time");
let combine = object(&project, "combine");
let a = ParameterId(id("28ff40b0-1b8f-4735-8536-5a6ab21b56cb"));
let b = ParameterId(id("7a47cc1e-8ae5-4551-b2d8-8a68108e0976"));
let numeric = AnalysisType::Scalar(ScalarType::Number);
let boolean = AnalysisType::Scalar(ScalarType::Boolean);
let duration_type = AnalysisType::Scalar(ScalarType::Quantity(DefinitionRef::Local(duration)));
let cases = [
(Expr::Boolean(true), "true", boolean.clone()),
(number(-1), "ratio(-1, 1)", numeric.clone()),
(
quantity(-1, second),
"ratio(-1, 1) [@second]",
duration_type.clone(),
),
(
Expr::Property(property),
"@receive_time",
duration_type.clone(),
),
(Expr::Parameter(a), "$a", duration_type.clone()),
(
Expr::List(vec![number(1), number(2)]),
"[1, 2]",
AnalysisType::List(ScalarType::Number),
),
(
unary(UnaryOp::Absolute, unary(UnaryOp::Negate, number(2))),
"abs(- 2)",
numeric.clone(),
),
(
binary(BinaryOp::Add, number(1), number(2)),
"1 + 2",
numeric.clone(),
),
(
Expr::If {
condition: Box::new(Expr::Boolean(true)),
when_true: Box::new(number(1)),
when_false: Box::new(number(2)),
},
"if(true, 1, 2)",
numeric.clone(),
),
(
Expr::Call {
definition: DefinitionRef::Local(combine),
arguments: BTreeMap::from([(b, quantity(2, second)), (a, quantity(1, second))]),
},
"@combine(a: 1 [@second], b: 2 [@second])",
duration_type.clone(),
),
(
Expr::Sum {
values: Box::new(Expr::List(vec![number(1), number(2)])),
initial: Box::new(number(0)),
},
"sum([1, 2], 0)",
numeric.clone(),
),
(
Expr::Power {
base: Box::new(number(2)),
exponent: -2,
},
"power(2, -2)",
numeric.clone(),
),
(
Expr::AsKind {
kind: DefinitionRef::Local(duration),
value: Box::new(binary(
BinaryOp::Divide,
Expr::Power {
base: Box::new(quantity(1, second)),
exponent: 2,
},
quantity(1, second),
)),
},
"as_kind(@duration, power(1 [@second], 2) / 1 [@second])",
duration_type,
),
];
project.elements.remove(&object(&project, "budget"));
for (expected, golden, value_type) in cases {
let parameter = matches!(expected, Expr::Parameter(_));
let owner = if parameter { combine } else { total };
let field = if parameter { Field::Body } else { Field::Base };
let text = writer(&project)
.expression(owner, field, &expected, Parent::Root)
.unwrap();
assert_eq!(text, golden);
let actual = read_expression(&project, &text);
assert_eq!(actual, expected);
let mut candidate = project.clone();
if parameter {
let DefinitionBody::Calculation(calculation) =
&mut candidate.definitions.get_mut(&combine).unwrap().body
else {
panic!()
};
calculation.body = Binding::Expression(actual);
} else {
let Some(SharedBody::EngineeringProperty {
base,
value_type: ty,
display_unit,
..
}) = candidate
.elements
.get_mut(&total)
.unwrap()
.body
.shared_mut()
else {
panic!()
};
*base = Binding::Expression(actual);
*ty = value_type;
*display_unit = None;
}
a::admit(&candidate, a::AdmissionLimits::REFERENCE).unwrap();
}
let cases = [
(unary(UnaryOp::Negate, number(1)), "- 1"),
(unary(UnaryOp::Negate, number(-1)), "- ratio(-1, 1)"),
(
unary(UnaryOp::Negate, unary(UnaryOp::Negate, number(1))),
"- - 1",
),
(
unary(UnaryOp::Negate, binary(BinaryOp::Add, number(1), number(2))),
"- (1 + 2)",
),
(unary(UnaryOp::Not, Expr::Boolean(true)), "not(true)"),
(
binary(
BinaryOp::Subtract,
number(1),
binary(BinaryOp::Subtract, number(2), number(3)),
),
"1 - (2 - 3)",
),
(
binary(
BinaryOp::Subtract,
binary(BinaryOp::Subtract, number(1), number(2)),
number(3),
),
"1 - 2 - 3",
),
(
binary(
BinaryOp::Add,
number(1),
binary(BinaryOp::Add, number(2), number(3)),
),
"1 + (2 + 3)",
),
(
binary(
BinaryOp::Multiply,
binary(BinaryOp::Add, number(1), number(2)),
number(3),
),
"(1 + 2) * 3",
),
(
binary(
BinaryOp::Equal,
binary(BinaryOp::Less, number(1), number(2)),
Expr::Boolean(true),
),
"(1 < 2) == true",
),
(
binary(
BinaryOp::Equal,
binary(BinaryOp::Equal, Expr::Boolean(true), Expr::Boolean(false)),
Expr::Boolean(true),
),
"(true == false) == true",
),
(
binary(
BinaryOp::Equal,
Expr::Boolean(true),
binary(BinaryOp::Less, number(1), number(2)),
),
"true == (1 < 2)",
),
(Expr::List(Vec::new()), "[]"),
];
for (expected, golden) in cases {
let text = writer(&project)
.expression(total, Field::Base, &expected, Parent::Root)
.unwrap();
assert_eq!(text, golden);
assert_eq!(read_expression(&project, &text), expected);
}
}
#[test]
fn canonical_calls_use_callee_parameter_order_and_qualified_names() {
let mut project = model_oracle::project(Layer::Oa);
let combine = object(&project, "combine");
let total = object(&project, "total_time");
let duration = object(&project, "duration");
let a = ParameterId(id("28ff40b0-1b8f-4735-8536-5a6ab21b56cb"));
let b = ParameterId(id("7a47cc1e-8ae5-4551-b2d8-8a68108e0976"));
let DefinitionBody::Calculation(calculation) =
&mut project.definitions.get_mut(&combine).unwrap().body
else {
panic!()
};
calculation.parameters.reverse();
let other = Id::from_u128(120);
project.definitions.insert(
other,
a::Definition {
id: other,
alias: "other".into(),
name: "Other".into(),
description: String::new(),
body: DefinitionBody::Calculation(a::CalculationDefinition {
parameters: vec![a::Parameter {
id: a,
name: "first".into(),
value_type: AnalysisType::Scalar(ScalarType::Quantity(DefinitionRef::Local(
duration,
))),
}],
result: AnalysisType::Scalar(ScalarType::Quantity(DefinitionRef::Local(duration))),
body: Binding::Expression(Expr::Parameter(a)),
}),
},
);
project.definitions.insert(
a.0,
a::Definition {
id: a.0,
alias: "collision".into(),
name: "Collision".into(),
description: String::new(),
body: DefinitionBody::QuantityKind(a::QuantityKindDefinition {
dimension: crate::analysis_numeric::Dimension::ZERO,
algebra: a::QuantityAlgebra::Linear,
}),
},
);
let mut out = writer(&project);
let text = out.binding(total, Field::Base).unwrap();
assert!(text.starts_with("@combine(b: @dispatch_time, a: @receive_time)"));
let call = Expr::Call {
definition: DefinitionRef::Local(other),
arguments: BTreeMap::from([(a, Expr::Parameter(a))]),
};
assert_eq!(
out.expression(combine, Field::Body, &call, Parent::Root)
.unwrap(),
"@other(first: $a)"
);
assert!(out.parameter(combine, a).unwrap().starts_with("input a id"));
assert!(
out.parameter(other, a)
.unwrap()
.starts_with("input first id")
);
assert!(out.parameter(combine, b).unwrap().starts_with("input b id"));
let text = canonical(&project);
let actual = parse_typed(skeleton(&project), &text);
assert_eq!(actual, project);
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
}
#[test]
fn subtree_output_respects_parent_context_and_retained_delimiters() {
let project = model_oracle::project(Layer::Oa);
let owner = object(&project, "total_time");
for replacement in [
binary(BinaryOp::Add, number(1), number(2)),
number(-1),
unary(UnaryOp::Negate, number(1)),
] {
let text = writer(&project)
.expression(owner, Field::Base, &replacement, Parent::Negate)
.unwrap();
let source = format!("-{text}"); // replace the entire original operand span `(1)` in `-(1)`
assert_eq!(
read_expression(&project, &source),
unary(UnaryOp::Negate, replacement)
);
}
let value = binary(BinaryOp::Subtract, number(2), number(3));
let text = writer(&project)
.expression(
owner,
Field::Base,
&value,
Parent::Binary(BinaryOp::Subtract, Side::Right),
)
.unwrap();
assert_eq!(text, "(2 - 3)");
assert_eq!(
read_expression(&project, &format!("1 - {text}")),
binary(BinaryOp::Subtract, number(1), value)
);
assert_eq!(
writer(&project)
.value_type(owner, &AnalysisType::List(ScalarType::Boolean))
.unwrap(),
"List(Boolean)"
);
let case = object(&project, "unmeasured");
let property = object(&project, "receive_time");
assert_eq!(
writer(&project).override_entry(case, property).unwrap(),
"@receive_time = unbound;"
);
}
#[test]
fn canonical_strings_and_exact_literals_use_existing_encodings() {
let mut project = model_oracle::project(Layer::Oa);
let second = object(&project, "second");
let text = "\"\\\n\r\t\u{0000} café 雪";
project.definitions.get_mut(&second).unwrap().name = text.into();
project.definitions.get_mut(&second).unwrap().description = text.into();
let DefinitionBody::Unit(unit) = &mut project.definitions.get_mut(&second).unwrap().body else {
panic!()
};
unit.symbol = text.into();
let output = writer(&project).definition(second).unwrap();
assert_eq!(output.matches(&crate::language::quote(text)).count(), 3);
let actual = parse_typed(skeleton(&project), &canonical(&project));
assert_eq!(actual, project);
let owner = object(&project, "total_time");
for source in ["0", "ratio(-1, 1)", "ratio(3, 2)", "1e100"] {
let expression = read_expression(&project, source);
let Expr::Number(value) = &expression else {
panic!()
};
assert_eq!(
writer(&project)
.expression(owner, Field::Base, &expression, Parent::Root)
.unwrap(),
value.canonical_literal()
);
}
}
#[test]
fn writer_limits_are_aggregate_exact_and_latched_without_partial_results() {
let project = model_oracle::project(Layer::Oa);
let owner = object(&project, "total_time");
let mut one = Writer::new(
&project,
Limits {
bytes: 1,
..limits()
},
)
.unwrap();
assert_eq!(
one.expression(owner, Field::Base, &number(1), Parent::Root)
.unwrap(),
"1"
);
let failure = one
.expression(owner, Field::Base, &number(2), Parent::Root)
.unwrap_err();
assert_eq!(
failure,
one.expression(owner, Field::Base, &number(0), Parent::Root)
.unwrap_err()
);
assert_eq!(one.usage().bytes, 1);
let mut measured = writer(&project);
measured
.expression(owner, Field::Base, &number(1), Parent::Root)
.unwrap();
let usage = measured.usage();
for (maximum, pass) in [(usage.entries, true), (usage.entries - 1, false)] {
let mut bounded = Writer::new(
&project,
Limits {
entries: maximum,
..limits()
},
)
.unwrap();
assert_eq!(
bounded
.expression(owner, Field::Base, &number(1), Parent::Root)
.is_ok(),
pass
);
}
let output = canonical(&project);
let mut exact = Writer::new(
&project,
Limits {
bytes: output.len(),
..limits()
},
)
.unwrap();
assert_eq!(exact.typed_declarations().unwrap().len(), 11);
let mut short = Writer::new(
&project,
Limits {
bytes: output.len() - 1,
..limits()
},
)
.unwrap();
assert!(short.typed_declarations().is_err());
assert_eq!(canonical(&project), output);
let mut quoted = project.clone();
let unit_id = object(&quoted, "second");
quoted.definitions.get_mut(&unit_id).unwrap().name = "雪\n\"".into();
let text = writer(&quoted).definition(unit_id).unwrap();
for (maximum, pass) in [(text.len(), true), (text.len() - 1, false)] {
assert_eq!(
Writer::new(
&quoted,
Limits {
bytes: maximum,
..limits()
}
)
.unwrap()
.definition(unit_id)
.is_ok(),
pass
);
}
assert!(
Writer::new(
&project,
Limits {
bytes: usize::MAX,
..limits()
}
)
.is_err()
);
}
#[test]
fn writer_lookup_scope_shape_and_depth_failures_are_model_located() {
let project = model_oracle::project(Layer::Oa);
let owner = object(&project, "total_time");
let combine = object(&project, "combine");
let duration = object(&project, "duration");
let a = ParameterId(id("28ff40b0-1b8f-4735-8536-5a6ab21b56cb"));
let cases = [
Expr::Property(duration),
Expr::Property(Id::nil()),
Expr::Quantity {
magnitude: Exact::from_integer(1),
unit: DefinitionRef::Local(duration),
},
Expr::Parameter(a),
Expr::Call {
definition: DefinitionRef::Local(duration),
arguments: BTreeMap::new(),
},
Expr::Call {
definition: DefinitionRef::Local(combine),
arguments: BTreeMap::new(),
},
Expr::Call {
definition: DefinitionRef::Local(combine),
arguments: BTreeMap::from([(ParameterId(Id::nil()), number(1)), (a, number(2))]),
},
Expr::Power {
base: Box::new(number(1)),
exponent: 17,
},
Expr::List((0..1025).map(|_| number(1)).collect()),
];
for expression in cases {
let error = writer(&project)
.expression(owner, Field::Base, &expression, Parent::Root)
.unwrap_err();
assert_eq!(error.owner, Some(owner));
assert_eq!(error.field, "base");
}
assert!(
writer(&project)
.expression(
combine,
Field::Body,
&Expr::Parameter(ParameterId(Id::nil())),
Parent::Root
)
.is_err()
);
assert!(writer(&project).binding(owner, Field::Body).is_err());
assert!(
writer(&project)
.analysis_element(object(&project, "respond"))
.is_err()
);
let mut deep = number(-1);
for _ in 0..128 {
deep = unary(UnaryOp::Negate, deep);
}
assert!(
writer(&project)
.expression(owner, Field::Base, &deep, Parent::Root)
.unwrap_err()
.message
.contains("depth")
);
}
#[test]
fn negate_override_roundtrip_preserves_depth128_within_frame256() {
let original = model_oracle::project(Layer::Oa);
let prefix = format!("{}ratio(-1, 1)", "- ".repeat(127));
let grouped = format!("{}ratio(-1, 1){}", "-(".repeat(127), ")".repeat(127));
let property = r#"oa_property measure "Measure" id "00000000-0000-0000-0000-000000000130" { subject @respond; type Number; base unbound; }"#;
let case = |expression: &str| {
format!(
r#"oa_analysis measured "Measured" id "00000000-0000-0000-0000-000000000131" {{ base ModelBaseline; overrides {{ @measure = {expression}; }}; }}"#
)
};
let calculation = |expression: &str| {
format!(
r#"calculation measured_calc "Measured calculation" id "00000000-0000-0000-0000-000000000132" {{ result Number; body {expression}; }}"#
)
};
for (source, frames) in [
(case(&prefix), 130),
(calculation(&prefix), 129),
(calculation(&grouped), 256),
] {
let mut budget = budget();
unit(&source, &mut budget).unwrap();
assert_eq!(budget.usage.deepest_frame, frames);
}
let mut failed_budget = budget();
let error = unit(&case(&grouped), &mut failed_budget).err().unwrap();
assert!(error.message.contains("syntax frames"));
assert_eq!(failed_budget.usage.deepest_frame, 256);
let project = parse_typed(original, &format!("{property}\n{}", case(&prefix)));
a::admit(&project, a::AdmissionLimits::REFERENCE).unwrap();
let case_id = Id::from_u128(0x131);
let property_id = Id::from_u128(0x130);
let Some(SharedBody::AnalysisCase { overrides, .. }) = project.elements[&case_id].shared()
else {
panic!()
};
let Binding::Expression(root) = &overrides[&property_id] else {
panic!()
};
let mut current = root;
let mut negates = 0;
while let Expr::Unary {
op: UnaryOp::Negate,
operand,
} = current
{
current = operand;
negates += 1;
}
assert_eq!(negates, 127);
assert_eq!(current, &number(-1));
let text = writer(&project).analysis_element(case_id).unwrap();
assert!(text.contains(&prefix));
let mut budget = budget();
unit(&text, &mut budget).unwrap();
assert_eq!(budget.usage.deepest_frame, 130);
let mut before_case = project.clone();
before_case.elements.remove(&case_id);
let actual = parse_typed(before_case, &text);
assert_eq!(actual, project);
assert_eq!(writer(&actual).analysis_element(case_id).unwrap(), text);
}
#[test]
fn iterative_writer_preserves_width1024_and_empty_call_topology() {
let mut project = model_oracle::project(Layer::Oa);
let owner = object(&project, "total_time");
let expected = Expr::List((0..1024).map(|_| number(1)).collect());
let text = writer(&project)
.expression(owner, Field::Base, &expected, Parent::Root)
.unwrap();
assert_eq!(read_expression(&project, &text), expected);
let empty = Id::from_u128(140);
project.definitions.insert(
empty,
a::Definition {
id: empty,
alias: "empty".into(),
name: "Empty".into(),
description: String::new(),
body: DefinitionBody::Calculation(a::CalculationDefinition {
parameters: Vec::new(),
result: AnalysisType::Scalar(ScalarType::Number),
body: Binding::Unbound,
}),
},
);
let call = Expr::Call {
definition: DefinitionRef::Local(empty),
arguments: BTreeMap::new(),
};
let text = writer(&project)
.expression(owner, Field::Base, &call, Parent::Root)
.unwrap();
assert_eq!(text, "@empty()");
assert_eq!(read_expression(&project, &text), call);
}