Add private complete mixed Project resolution with retained provenance
This commit is contained in:
@@ -1664,17 +1664,25 @@ impl Parser<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_declarations(
|
||||
// The construction phase is shared with private mixed resolution. Migration and
|
||||
// complete-model admission remain exclusively in each caller's finishing tail.
|
||||
struct ConstructedDeclarations {
|
||||
project: Project,
|
||||
legacy_elements: serde_json::Map<String, Value>,
|
||||
}
|
||||
|
||||
fn construct_declarations<'a>(
|
||||
mut project: Project,
|
||||
declarations: &[Declaration],
|
||||
declarations: impl IntoIterator<Item = &'a Declaration>,
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
limits: ParseLimits,
|
||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
||||
mut before_fields: impl FnMut(&Declaration) -> Result<(), Error>,
|
||||
) -> Result<ConstructedDeclarations, (usize, Error)> {
|
||||
let legacy = project.schema_version == 3;
|
||||
let mut legacy_elements = serde_json::Map::new();
|
||||
let mut nodes = Vec::new();
|
||||
for (index, declaration) in declarations.iter().enumerate() {
|
||||
for (index, declaration) in declarations.into_iter().enumerate() {
|
||||
(|| -> Result<(), Error> {
|
||||
before_fields(declaration)?;
|
||||
let mut fields = declaration
|
||||
.fields
|
||||
.iter()
|
||||
@@ -1899,6 +1907,18 @@ pub(crate) fn resolve_declarations(
|
||||
.map_err(|error| (index, error))?;
|
||||
target.nodes.insert(node.id, node);
|
||||
}
|
||||
Ok(ConstructedDeclarations { project, legacy_elements })
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_declarations(
|
||||
project: Project,
|
||||
declarations: &[Declaration],
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
limits: ParseLimits,
|
||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
||||
let legacy = project.schema_version == 3;
|
||||
let ConstructedDeclarations { project, legacy_elements } =
|
||||
construct_declarations(project, declarations, aliases, |_| Ok(()))?;
|
||||
if legacy {
|
||||
let migrated = source3_bridge::from_scratch(project, legacy_elements)
|
||||
.and_then(|old| crate::persistence::v3::migrate(old, limits))
|
||||
|
||||
@@ -124,7 +124,7 @@ struct Symbol {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Tables {
|
||||
pub(super) struct Tables {
|
||||
aliases: BTreeMap<String, Symbol>,
|
||||
ids: BTreeSet<Id>,
|
||||
parameters: BTreeMap<Id, BTreeMap<String, a::ParameterId>>,
|
||||
@@ -157,7 +157,7 @@ impl Tables {
|
||||
self.entries += 1;
|
||||
Ok(())
|
||||
}
|
||||
fn insert(
|
||||
pub(super) fn insert(
|
||||
&mut self,
|
||||
id: Id,
|
||||
alias: &str,
|
||||
@@ -233,7 +233,11 @@ impl Tables {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn typed(&mut self, declaration: &Declaration, budget: &Budget) -> Result<(), Error> {
|
||||
pub(super) fn typed(
|
||||
&mut self,
|
||||
declaration: &Declaration,
|
||||
budget: &Budget,
|
||||
) -> Result<(), Error> {
|
||||
let header = &declaration.header;
|
||||
let id = identity(&header.identity, header.alias.span)?;
|
||||
self.insert(
|
||||
@@ -299,31 +303,49 @@ pub(super) fn lower(
|
||||
})?;
|
||||
}
|
||||
}
|
||||
lower_bodies(
|
||||
&tables,
|
||||
units
|
||||
.iter()
|
||||
.flat_map(|unit| unit.declarations.iter().map(move |d| (unit.file, d))),
|
||||
budget,
|
||||
)
|
||||
}
|
||||
|
||||
/// Catalog and borrowed declarations must be from the same private witness.
|
||||
/// The mixed caller reserves aggregate live storage before entering this body;
|
||||
/// the legacy wrapper retains its original individual phase checks.
|
||||
pub(super) fn lower_bodies<'a>(
|
||||
tables: &Tables,
|
||||
declarations: impl IntoIterator<Item = (usize, &'a Declaration)>,
|
||||
budget: &Budget,
|
||||
) -> Result<Lowered, Failure> {
|
||||
budget
|
||||
.active()
|
||||
.map_err(|error| Failure { file: None, 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,
|
||||
})?;
|
||||
}
|
||||
for (file, declaration) in declarations {
|
||||
let owner = declaration
|
||||
.header
|
||||
.identity
|
||||
.value
|
||||
.expect("collected stable identity");
|
||||
let mut resolver = Resolver {
|
||||
tables,
|
||||
output: &mut output,
|
||||
budget,
|
||||
file,
|
||||
owner,
|
||||
};
|
||||
resolver.declaration(declaration).map_err(|error| Failure {
|
||||
file: Some(file),
|
||||
error,
|
||||
})?;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ pub(super) struct ParsedMixedSyntax<'s> {
|
||||
limits: ProjectLimits,
|
||||
files: Vec<FileSyntax<'s>>,
|
||||
budget: Budget,
|
||||
// Includes the conservative dropped value-end subtotal. The semantic
|
||||
// phase adds syntax entries to this once, not tokens/positions again.
|
||||
metadata: usize,
|
||||
}
|
||||
|
||||
struct FileSyntax<'s> {
|
||||
@@ -369,6 +372,7 @@ fn parse_with_profile(
|
||||
limits,
|
||||
files: parsed.into_values().collect(),
|
||||
budget,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -398,3 +402,5 @@ fn ordinary_position_slots(unit: &SourceUnit) -> usize {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(super) mod project;
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
//! Private complete-Project admission from the actual complete syntax witness.
|
||||
//! No Index, update authority, public source8 dispatch or replacement policy.
|
||||
use super::super::{
|
||||
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
|
||||
declarations as d, error, lower,
|
||||
};
|
||||
use super::{FileSyntax, ParsedMixedSyntax};
|
||||
use crate::language::{Ast, DeclKind};
|
||||
use crate::source_project::{SourceDiagnostic, SourceLocation};
|
||||
use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
|
||||
use std::collections::BTreeMap;
|
||||
mod accounting;
|
||||
use accounting::{Ledger, Slots, add};
|
||||
|
||||
/// Read-only authority: every component originates in the consumed syntax.
|
||||
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
|
||||
syntax: ParsedMixedSyntax<'s>,
|
||||
project: Project,
|
||||
warnings: Vec<SourceDiagnostic>,
|
||||
static_limits: analysis::AdmissionLimits,
|
||||
static_usage: analysis::AdmissionUsage,
|
||||
expressions: Vec<lower::ExpressionSite>,
|
||||
occurrences: Vec<lower::Occurrence>,
|
||||
storage: Ledger,
|
||||
}
|
||||
impl ResolvedMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(in crate::language::analysis_source) struct Failure {
|
||||
pub diagnostics: Vec<SourceDiagnostic>,
|
||||
}
|
||||
impl Failure {
|
||||
// Rejection transport is not retained in a successful witness. In particular
|
||||
// a storage refusal needs a small diagnostic even when its allowance is zero.
|
||||
fn typed(syntax: &ParsedMixedSyntax<'_>, file: Option<usize>, e: TypedError) -> Self {
|
||||
let mut failure = Self::ordinary(
|
||||
syntax,
|
||||
file,
|
||||
language::Error {
|
||||
code: "invalid_source",
|
||||
offset: e.span.start,
|
||||
message: e.message,
|
||||
},
|
||||
);
|
||||
failure.diagnostics[0].diagnostic.span = e.span;
|
||||
failure
|
||||
}
|
||||
fn ordinary(syntax: &ParsedMixedSyntax<'_>, file: Option<usize>, e: language::Error) -> Self {
|
||||
let file = file.unwrap_or_else(|| entry(syntax));
|
||||
let unit = &syntax.files[file];
|
||||
Self {
|
||||
diagnostics: super::located(unit.path, unit.source, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum OwnerKind {
|
||||
Project,
|
||||
Ordinary(usize),
|
||||
Typed(usize),
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct Owner {
|
||||
file: usize,
|
||||
kind: OwnerKind,
|
||||
span: Span,
|
||||
}
|
||||
struct Catalog {
|
||||
tables: lower::Tables,
|
||||
aliases: BTreeMap<String, Id>,
|
||||
owners: BTreeMap<Id, Owner>,
|
||||
tables_storage: Slots,
|
||||
aliases_storage: Slots,
|
||||
owners_storage: Slots,
|
||||
}
|
||||
fn entry(syntax: &ParsedMixedSyntax<'_>) -> usize {
|
||||
syntax
|
||||
.files
|
||||
.iter()
|
||||
.position(|f| f.path == syntax.bundle.entry)
|
||||
.expect("complete entry")
|
||||
}
|
||||
fn category(kind: DeclKind) -> lower::Category {
|
||||
match kind {
|
||||
DeclKind::Element(kind, layer) => lower::Category::Element(kind, layer),
|
||||
DeclKind::Relation(_) => lower::Category::Relation,
|
||||
DeclKind::Diagram => lower::Category::Diagram,
|
||||
DeclKind::Node => lower::Category::Node,
|
||||
DeclKind::Artifact => lower::Category::Artifact,
|
||||
DeclKind::Trace => lower::Category::Trace,
|
||||
}
|
||||
}
|
||||
impl Catalog {
|
||||
fn insert(
|
||||
&mut self,
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
ledger: &mut Ledger,
|
||||
id: Id,
|
||||
alias: &str,
|
||||
owner: Owner,
|
||||
) -> Result<(), Failure> {
|
||||
// Alias first, even when both aliases and IDs collide. Owner is an
|
||||
// occurrence position, never a lookup through the rejected UUID.
|
||||
let prior = self
|
||||
.aliases
|
||||
.get(alias)
|
||||
.map(|id| ("alias", self.owners[id]))
|
||||
.or_else(|| self.owners.get(&id).map(|o| ("identity", *o)));
|
||||
if let Some((domain, prior)) = prior {
|
||||
let mut failure = Failure::typed(
|
||||
syntax,
|
||||
Some(owner.file),
|
||||
error(owner.span, format!("Duplicate global {domain}.")),
|
||||
);
|
||||
failure.diagnostics[0].related.push(SourceLocation {
|
||||
path: syntax.files[prior.file].path.into(),
|
||||
span: prior.span,
|
||||
});
|
||||
return Err(failure);
|
||||
}
|
||||
let alias_slots = Slots {
|
||||
entries: 1,
|
||||
text: alias.len(),
|
||||
..Slots::default()
|
||||
};
|
||||
ledger
|
||||
.reserve(
|
||||
&syntax.budget,
|
||||
alias_slots
|
||||
.add(Slots::entries(1), owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?,
|
||||
owner.span,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.aliases_storage = self
|
||||
.aliases_storage
|
||||
.add(alias_slots, owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.owners_storage = self
|
||||
.owners_storage
|
||||
.add(Slots::entries(1), owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.aliases.insert(alias.into(), id);
|
||||
self.owners.insert(id, owner);
|
||||
Ok(())
|
||||
}
|
||||
fn reserve_tables(
|
||||
&mut self,
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
ledger: &mut Ledger,
|
||||
slots: Slots,
|
||||
owner: Owner,
|
||||
) -> Result<(), Failure> {
|
||||
ledger
|
||||
.reserve(&syntax.budget, slots, owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.tables_storage = self
|
||||
.tables_storage
|
||||
.add(slots, owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn catalog(syntax: &ParsedMixedSyntax<'_>, ledger: &mut Ledger) -> Result<Catalog, Failure> {
|
||||
let mut catalog = Catalog {
|
||||
tables: lower::Tables::default(),
|
||||
aliases: BTreeMap::new(),
|
||||
owners: BTreeMap::new(),
|
||||
tables_storage: Slots::default(),
|
||||
aliases_storage: Slots::default(),
|
||||
owners_storage: Slots::default(),
|
||||
};
|
||||
let file = entry(syntax);
|
||||
let header = syntax.files[file]
|
||||
.ordinary
|
||||
.header
|
||||
.as_ref()
|
||||
.expect("entry header");
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Project,
|
||||
span: header.span,
|
||||
};
|
||||
catalog.insert(
|
||||
syntax,
|
||||
ledger,
|
||||
header.project.id,
|
||||
&header.project.alias,
|
||||
owner,
|
||||
)?;
|
||||
catalog.reserve_tables(
|
||||
syntax,
|
||||
ledger,
|
||||
Slots {
|
||||
entries: 2,
|
||||
text: header.project.alias.len(),
|
||||
..Slots::default()
|
||||
},
|
||||
owner,
|
||||
)?;
|
||||
catalog
|
||||
.tables
|
||||
.insert(
|
||||
header.project.id,
|
||||
&header.project.alias,
|
||||
lower::Category::Project,
|
||||
header.span,
|
||||
&syntax.budget,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
for (file, unit) in syntax.files.iter().enumerate() {
|
||||
let mut ordinary = unit.ordinary.declarations.iter().enumerate().peekable();
|
||||
let mut typed = unit.typed.iter().enumerate().peekable();
|
||||
while ordinary.peek().is_some() || typed.peek().is_some() {
|
||||
let use_ordinary = match (ordinary.peek(), typed.peek()) {
|
||||
(Some((_, a)), Some((_, b))) => a.span.start < b.span.start,
|
||||
(Some(_), None) => true,
|
||||
_ => false,
|
||||
};
|
||||
if use_ordinary {
|
||||
let (index, d) = ordinary.next().expect("ordinary head");
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Ordinary(index),
|
||||
span: d.span,
|
||||
};
|
||||
catalog.insert(syntax, ledger, d.id, &d.alias, owner)?;
|
||||
catalog.reserve_tables(
|
||||
syntax,
|
||||
ledger,
|
||||
Slots {
|
||||
entries: 2,
|
||||
text: d.alias.len(),
|
||||
..Slots::default()
|
||||
},
|
||||
owner,
|
||||
)?;
|
||||
catalog
|
||||
.tables
|
||||
.insert(d.id, &d.alias, category(d.kind), d.span, &syntax.budget)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
} else {
|
||||
let (index, d) = typed.next().expect("typed head");
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Typed(index),
|
||||
span: d.span,
|
||||
};
|
||||
catalog.insert(
|
||||
syntax,
|
||||
ledger,
|
||||
d.header.identity.value.expect("syntax explicit ID"),
|
||||
&d.header.alias.name,
|
||||
owner,
|
||||
)?;
|
||||
let mut slots = Slots {
|
||||
entries: 2,
|
||||
text: d.header.alias.name.len(),
|
||||
..Slots::default()
|
||||
};
|
||||
let mut scratch = 0;
|
||||
if let d::Body::Calculation { parameters, .. } = &d.body {
|
||||
slots.entries = add(
|
||||
slots.entries,
|
||||
add(1, parameters.len(), d.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?,
|
||||
d.span,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
scratch = parameters.len(); // per-signature duplicate-ID set
|
||||
for p in parameters {
|
||||
slots.text = add(slots.text, p.name.name.len(), p.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
}
|
||||
}
|
||||
catalog.reserve_tables(syntax, ledger, slots, owner)?;
|
||||
ledger
|
||||
.reserve(&syntax.budget, Slots::entries(scratch), d.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
catalog
|
||||
.tables
|
||||
.typed(d, &syntax.budget)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
ledger.release(Slots::entries(scratch));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(catalog)
|
||||
}
|
||||
|
||||
fn object<'a>(value: Option<&'a Ast>, key: &str) -> Option<&'a Ast> {
|
||||
match value {
|
||||
Some(Ast::Object(fields, _)) => fields.get(key),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Selector only, not a decoder. This executes before ordinary fields clone.
|
||||
fn active_references(
|
||||
declaration: &language::Declaration,
|
||||
budget: &mut Budget,
|
||||
) -> Result<(), language::Error> {
|
||||
let field = |key: &str| declaration.fields.get(key);
|
||||
let arrays = match declaration.kind {
|
||||
DeclKind::Element(Kind::Process | Kind::FunctionalChain | Kind::Scenario, _) => {
|
||||
[field("steps"), None]
|
||||
}
|
||||
DeclKind::Element(Kind::Occurrence, _) => {
|
||||
[object(object(field("body"), "Branch"), "occurrences"), None]
|
||||
}
|
||||
DeclKind::Relation(RelationKind::ContextualLink) => [
|
||||
object(field("source"), "references"),
|
||||
object(field("target"), "references"),
|
||||
],
|
||||
_ => [None, None],
|
||||
};
|
||||
for array in arrays.into_iter().flatten() {
|
||||
if let Ast::Array(items, _) = array {
|
||||
for item in items {
|
||||
let start = item.offset();
|
||||
budget
|
||||
.reference(Span { start, end: start })
|
||||
.map_err(super::ordinary_error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::language::analysis_source) fn resolve(
|
||||
mut syntax: ParsedMixedSyntax<'_>,
|
||||
) -> Result<ResolvedMixedProject<'_>, Failure> {
|
||||
let mut storage = Ledger::default();
|
||||
let lower_storage = accounting::syntax(&syntax, &mut storage)?;
|
||||
let catalog = catalog(&syntax, &mut storage)?;
|
||||
let entry = entry(&syntax);
|
||||
// Only the empty model header is copied: retain its original syntax/header
|
||||
// ownership and construct the final model through the shared decoder once.
|
||||
let project = syntax.files[entry]
|
||||
.ordinary
|
||||
.header
|
||||
.as_ref()
|
||||
.expect("entry header")
|
||||
.project
|
||||
.clone();
|
||||
let count = syntax
|
||||
.files
|
||||
.iter()
|
||||
.try_fold(0usize, |n, f| {
|
||||
add(n, f.ordinary.declarations.len(), Span::default())
|
||||
})
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
storage
|
||||
.reserve(&syntax.budget, Slots::entries(count), Span::default())
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
let declarations: Vec<_> = syntax
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(file, unit)| unit.ordinary.declarations.iter().map(move |d| (file, d)))
|
||||
.collect();
|
||||
let mut nodes = 0;
|
||||
let constructed = language::construct_declarations(
|
||||
project,
|
||||
declarations.iter().map(|(_, d)| *d),
|
||||
&catalog.aliases,
|
||||
|d| {
|
||||
active_references(d, &mut syntax.budget)?;
|
||||
if matches!(d.kind, DeclKind::Node) {
|
||||
storage
|
||||
.reserve(&syntax.budget, Slots::entries(1), d.span)
|
||||
.map_err(super::ordinary_error)?;
|
||||
nodes += 1;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.map_err(|(index, error)| {
|
||||
Failure::ordinary(
|
||||
&syntax,
|
||||
declarations.get(index).map(|(file, _)| *file),
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
let language::ConstructedDeclarations {
|
||||
mut project,
|
||||
legacy_elements,
|
||||
} = constructed;
|
||||
debug_assert!(legacy_elements.is_empty()); // mixed syntax only admits source8
|
||||
drop(legacy_elements);
|
||||
drop(declarations);
|
||||
storage.release(Slots::entries(count));
|
||||
storage.release(Slots::entries(nodes));
|
||||
drop(catalog.aliases);
|
||||
storage.release(catalog.aliases_storage);
|
||||
|
||||
let all_lower = lower_storage
|
||||
.all()
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
storage
|
||||
.reserve(&syntax.budget, all_lower, Span::default())
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
let lower::Lowered {
|
||||
definitions,
|
||||
elements,
|
||||
occurrences,
|
||||
expressions,
|
||||
} = lower::lower_bodies(
|
||||
&catalog.tables,
|
||||
syntax
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(file, unit)| unit.typed.iter().map(move |d| (file, d))),
|
||||
&syntax.budget,
|
||||
)
|
||||
.map_err(|e| Failure::typed(&syntax, e.file, e.error))?;
|
||||
// One global arena transfers verbatim; only now are all Resolver temporaries
|
||||
// dead. Output counts must fit the independently counted reservation.
|
||||
storage.release(Slots {
|
||||
entries: all_lower
|
||||
.entries
|
||||
.checked_sub(expressions.len())
|
||||
.expect("expression bound"),
|
||||
occurrences: all_lower
|
||||
.occurrences
|
||||
.checked_sub(occurrences.len())
|
||||
.expect("occurrence bound"),
|
||||
text: 0,
|
||||
});
|
||||
drop(catalog.tables);
|
||||
storage.release(catalog.tables_storage);
|
||||
for (id, value) in definitions {
|
||||
if project.definitions.contains_key(&id) {
|
||||
return Err(Failure::typed(
|
||||
&syntax,
|
||||
None,
|
||||
error(Span::default(), "Non-disjoint definition merge."),
|
||||
));
|
||||
}
|
||||
project.definitions.insert(id, value);
|
||||
}
|
||||
for (id, value) in elements {
|
||||
if project.elements.contains_key(&id) {
|
||||
return Err(Failure::typed(
|
||||
&syntax,
|
||||
None,
|
||||
error(Span::default(), "Non-disjoint element merge."),
|
||||
));
|
||||
}
|
||||
project.elements.insert(id, value);
|
||||
}
|
||||
syntax
|
||||
.limits
|
||||
.model
|
||||
.check_model(&project)
|
||||
.map_err(|message| {
|
||||
Failure::ordinary(
|
||||
&syntax,
|
||||
None,
|
||||
language::Error {
|
||||
code: "model_limit",
|
||||
offset: 0,
|
||||
message,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let (issues, admission) = validation::validate_with_usage(&project);
|
||||
// Raw architecture Issues are excluded. Each transferred string, copied path
|
||||
// and destination slot is admitted before mapping/retention; never truncate.
|
||||
let mut warnings = Vec::new();
|
||||
let mut rejected = false;
|
||||
for issue in issues {
|
||||
let owner = issue
|
||||
.element
|
||||
.and_then(|id| catalog.owners.get(&id))
|
||||
.copied()
|
||||
.unwrap_or(catalog.owners[&project.id]);
|
||||
let span = field_span(&syntax.files[owner.file], owner, issue.field.as_deref());
|
||||
let unit = &syntax.files[owner.file];
|
||||
let text = [
|
||||
unit.path.len(),
|
||||
issue.code.len(),
|
||||
issue.field.as_ref().map_or(0, String::len),
|
||||
issue.message.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.try_fold(0, |n, k| add(n, k, span))
|
||||
.map_err(|e| Failure::typed(&syntax, Some(owner.file), e))?;
|
||||
storage
|
||||
.reserve(
|
||||
&syntax.budget,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text,
|
||||
},
|
||||
span,
|
||||
)
|
||||
.map_err(|e| Failure::typed(&syntax, Some(owner.file), e))?;
|
||||
rejected |= issue.severity == validation::Severity::Error;
|
||||
let prefix = &unit.source[..span.start];
|
||||
let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
|
||||
let column = prefix.rsplit('\n').next().expect("prefix").chars().count() + 1;
|
||||
warnings.push(SourceDiagnostic {
|
||||
path: unit.path.into(),
|
||||
related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: issue.severity,
|
||||
code: issue.code,
|
||||
object_id: issue.element,
|
||||
field: issue.field,
|
||||
message: issue.message,
|
||||
span,
|
||||
line,
|
||||
column,
|
||||
},
|
||||
});
|
||||
}
|
||||
if rejected {
|
||||
return Err(Failure {
|
||||
diagnostics: warnings,
|
||||
});
|
||||
}
|
||||
let (static_limits, static_usage) = admission.expect("successful validation retains admission");
|
||||
drop(catalog.owners);
|
||||
storage.release(catalog.owners_storage);
|
||||
Ok(ResolvedMixedProject {
|
||||
syntax,
|
||||
project,
|
||||
warnings,
|
||||
static_limits,
|
||||
static_usage,
|
||||
expressions,
|
||||
occurrences,
|
||||
storage,
|
||||
})
|
||||
}
|
||||
|
||||
/// Exact known field or explicit whole-owner fallback. Never guess by offsets.
|
||||
fn field_span(file: &FileSyntax<'_>, owner: Owner, field: Option<&str>) -> Span {
|
||||
let Some(field) = field else {
|
||||
return owner.span;
|
||||
};
|
||||
match owner.kind {
|
||||
OwnerKind::Project => file
|
||||
.ordinary
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(|h| (field == "description").then_some(h.description).flatten())
|
||||
.unwrap_or(owner.span),
|
||||
OwnerKind::Ordinary(index) => file.ordinary.declarations[index]
|
||||
.field_spans
|
||||
.get(field)
|
||||
.copied()
|
||||
.unwrap_or(owner.span),
|
||||
OwnerKind::Typed(index) => typed_field(&file.typed[index], field).unwrap_or(owner.span),
|
||||
}
|
||||
}
|
||||
fn typed_field(d: &d::Declaration, field: &str) -> Option<Span> {
|
||||
if field == "description" {
|
||||
return d.description.as_ref().map(|f| f.value_span);
|
||||
}
|
||||
if field == "alias" {
|
||||
return Some(d.header.alias.span);
|
||||
}
|
||||
if field == "name" {
|
||||
return Some(d.header.name_span);
|
||||
}
|
||||
match &d.body {
|
||||
d::Body::QuantityKind { dimension, algebra } => match field {
|
||||
"dimension" => Some(dimension.value_span),
|
||||
"algebra" => Some(algebra.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Unit {
|
||||
kind,
|
||||
scale,
|
||||
offset,
|
||||
symbol,
|
||||
} => match field {
|
||||
"kind" => Some(kind.value_span),
|
||||
"scale" => Some(scale.value_span),
|
||||
"offset" => Some(offset.value_span),
|
||||
"symbol" => Some(symbol.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Calculation { result, body, .. } => match field {
|
||||
"result" => Some(result.value_span),
|
||||
"body" => Some(body.value_span),
|
||||
// Current semantic admission reports aggregate "parameters", not a
|
||||
// qualified parameter/type path. Do not manufacture precision from
|
||||
// a UUID substring; lowerer failures already carry exact file/span.
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Property {
|
||||
subject,
|
||||
value_type,
|
||||
base,
|
||||
display_unit,
|
||||
} => match field {
|
||||
"subject" => Some(subject.value_span),
|
||||
"type" | "value_type" => Some(value_type.value_span),
|
||||
"base" => Some(base.value_span),
|
||||
"display_unit" => display_unit.as_ref().map(|f| f.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Check {
|
||||
subject,
|
||||
documented_constraint,
|
||||
predicate,
|
||||
} => match field {
|
||||
"subject" => subject.as_ref().map(|f| f.value_span),
|
||||
"documented_constraint" => documented_constraint.as_ref().map(|f| f.value_span),
|
||||
"predicate" => Some(predicate.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Case {
|
||||
subject,
|
||||
base,
|
||||
overrides,
|
||||
} => match field {
|
||||
"subject" => subject.as_ref().map(|f| f.value_span),
|
||||
"base" => Some(base.value_span),
|
||||
"overrides" => Some(overrides.value_span),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Logical auxiliary storage, separate from model/serde and raw validator heap.
|
||||
use super::*;
|
||||
use crate::language::{EditKind, EditValue};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(super) struct Slots {
|
||||
pub entries: usize,
|
||||
pub occurrences: usize,
|
||||
pub text: usize,
|
||||
}
|
||||
impl Slots {
|
||||
pub fn entries(entries: usize) -> Self {
|
||||
Self {
|
||||
entries,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn text(text: usize) -> Self {
|
||||
Self {
|
||||
text,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn add(self, other: Self, at: Span) -> Result<Self, super::super::super::Error> {
|
||||
Ok(Self {
|
||||
entries: add(self.entries, other.entries, at)?,
|
||||
occurrences: add(self.occurrences, other.occurrences, at)?,
|
||||
text: add(self.text, other.text, at)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub(super) fn add(a: usize, b: usize, at: Span) -> Result<usize, super::super::super::Error> {
|
||||
a.checked_add(b)
|
||||
.ok_or_else(|| error(at, "Mixed auxiliary storage size overflow."))
|
||||
}
|
||||
pub(super) fn mul(a: usize, b: usize, at: Span) -> Result<usize, super::super::super::Error> {
|
||||
a.checked_mul(b)
|
||||
.ok_or_else(|| error(at, "Mixed auxiliary storage size overflow."))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Ledger {
|
||||
pub live: Slots,
|
||||
}
|
||||
impl Ledger {
|
||||
pub fn reserve(&mut self, budget: &Budget, slots: Slots, at: Span) -> Result<(), TypedError> {
|
||||
let next = self.live.add(slots, at)?;
|
||||
budget.lowering_entries(next.entries, at)?;
|
||||
budget.lowering_occurrences(next.occurrences, at)?;
|
||||
budget.index_bytes(next.text, at)?;
|
||||
self.live = next;
|
||||
Ok(())
|
||||
}
|
||||
pub fn release(&mut self, slots: Slots) {
|
||||
self.live.entries = self
|
||||
.live
|
||||
.entries
|
||||
.checked_sub(slots.entries)
|
||||
.expect("entry reservation");
|
||||
self.live.occurrences = self
|
||||
.live
|
||||
.occurrences
|
||||
.checked_sub(slots.occurrences)
|
||||
.expect("occurrence reservation");
|
||||
self.live.text = self
|
||||
.live
|
||||
.text
|
||||
.checked_sub(slots.text)
|
||||
.expect("text reservation");
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact output-site counts and conservative whole-lowering temporary bound.
|
||||
/// N expression nodes: at most 2N Visit/Build slots, N borrowed child slots,
|
||||
/// N value/split slots. A arguments: A signature slots plus A seen-set slots.
|
||||
/// `Resolver::call` borrows the callee signature. Its parameter Vec/seen set
|
||||
/// contain only supplied arguments, including on incomplete/invalid calls;
|
||||
/// no allocation is proportional to the full signature during body lowering.
|
||||
/// The model expressions/maps themselves are outside this auxiliary ledger.
|
||||
#[derive(Default)]
|
||||
pub(super) struct LowerStorage {
|
||||
pub nodes: usize,
|
||||
pub occurrences: usize,
|
||||
pub arguments: usize,
|
||||
}
|
||||
impl LowerStorage {
|
||||
pub fn all(&self) -> Result<Slots, TypedError> {
|
||||
let at = Span::default();
|
||||
Ok(Slots {
|
||||
entries: add(mul(self.nodes, 5, at)?, mul(self.arguments, 2, at)?, at)?,
|
||||
occurrences: self.occurrences,
|
||||
text: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum Item<'a> {
|
||||
Ast(&'a Ast),
|
||||
Edit(&'a EditValue),
|
||||
Expr(&'a Expression),
|
||||
}
|
||||
struct Walk<'a, 'b> {
|
||||
ledger: &'b mut Ledger,
|
||||
budget: &'b Budget,
|
||||
work: Vec<Item<'a>>,
|
||||
lower: LowerStorage,
|
||||
}
|
||||
impl<'a> Walk<'a, '_> {
|
||||
fn text(&mut self, text: &str, at: Span) -> Result<(), TypedError> {
|
||||
self.ledger
|
||||
.reserve(self.budget, Slots::text(text.len()), at)
|
||||
}
|
||||
fn push(&mut self, item: Item<'a>) -> Result<(), TypedError> {
|
||||
self.ledger
|
||||
.reserve(self.budget, Slots::entries(1), Span::default())?;
|
||||
self.work.push(item);
|
||||
Ok(())
|
||||
}
|
||||
fn reference(&mut self, r: &Reference) -> Result<(), TypedError> {
|
||||
self.text(&r.name, r.span)?;
|
||||
self.lower.occurrences = add(self.lower.occurrences, 1, r.span)?;
|
||||
Ok(())
|
||||
}
|
||||
fn optional(&mut self, field: &Option<d::Field<Option<Reference>>>) -> Result<(), TypedError> {
|
||||
if let Some(r) = field.as_ref().and_then(|f| f.value.as_ref()) {
|
||||
self.reference(r)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn ty(&mut self, ty: &d::TypeSyntax) -> Result<(), TypedError> {
|
||||
if let d::ScalarKind::Quantity(r) = &ty.scalar.kind {
|
||||
self.reference(r)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn binding(&mut self, binding: &'a Binding) -> Result<(), TypedError> {
|
||||
if let Binding::Expression(e) = binding {
|
||||
self.push(Item::Expr(e))?;
|
||||
}
|
||||
self.drain()
|
||||
}
|
||||
fn drain(&mut self) -> Result<(), TypedError> {
|
||||
while let Some(item) = self.work.pop() {
|
||||
self.ledger.release(Slots::entries(1));
|
||||
match item {
|
||||
Item::Ast(ast) => match ast {
|
||||
Ast::Literal(serde_json::Value::String(s), _)
|
||||
| Ast::Word(s, _)
|
||||
| Ast::Reference(s, _) => self.text(s, Span::default())?,
|
||||
Ast::Array(items, _) => {
|
||||
for child in items {
|
||||
self.push(Item::Ast(child))?;
|
||||
}
|
||||
}
|
||||
Ast::Object(fields, _) => {
|
||||
for (key, child) in fields {
|
||||
self.text(key, Span::default())?;
|
||||
self.push(Item::Ast(child))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Item::Edit(value) => match &value.kind {
|
||||
EditKind::Reference(s) => self.text(s, value.span)?,
|
||||
EditKind::Record(fields) => {
|
||||
for (key, field) in fields {
|
||||
self.text(key, field.span)?;
|
||||
self.push(Item::Edit(&field.value))?;
|
||||
}
|
||||
}
|
||||
EditKind::List(items) => {
|
||||
for child in items {
|
||||
self.push(Item::Edit(child))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Item::Expr(e) => {
|
||||
self.lower.nodes = add(self.lower.nodes, 1, e.span)?;
|
||||
use ExpressionKind as E;
|
||||
match &e.kind {
|
||||
E::Quantity { unit, .. } | E::Property(unit) | E::Parameter(unit) => {
|
||||
self.reference(unit)?
|
||||
}
|
||||
E::List(items) => {
|
||||
for child in items {
|
||||
self.push(Item::Expr(child))?;
|
||||
}
|
||||
}
|
||||
E::Unary { operand, .. } => self.push(Item::Expr(operand))?,
|
||||
E::Binary { left, right, .. } => {
|
||||
self.push(Item::Expr(left))?;
|
||||
self.push(Item::Expr(right))?;
|
||||
}
|
||||
E::If {
|
||||
condition,
|
||||
when_true,
|
||||
when_false,
|
||||
} => {
|
||||
self.push(Item::Expr(condition))?;
|
||||
self.push(Item::Expr(when_true))?;
|
||||
self.push(Item::Expr(when_false))?;
|
||||
}
|
||||
E::Call {
|
||||
definition,
|
||||
arguments,
|
||||
} => {
|
||||
self.reference(definition)?;
|
||||
self.lower.arguments =
|
||||
add(self.lower.arguments, arguments.len(), e.span)?;
|
||||
for argument in arguments {
|
||||
self.reference(&argument.parameter)?;
|
||||
self.push(Item::Expr(&argument.value))?;
|
||||
}
|
||||
}
|
||||
E::Sum { values, initial } => {
|
||||
self.push(Item::Expr(values))?;
|
||||
self.push(Item::Expr(initial))?;
|
||||
}
|
||||
E::Power { base, .. } => self.push(Item::Expr(base))?,
|
||||
E::AsKind { kind, value } => {
|
||||
self.reference(kind)?;
|
||||
self.push(Item::Expr(value))?;
|
||||
}
|
||||
E::Boolean(_) | E::Number(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn syntax(
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
ledger: &mut Ledger,
|
||||
) -> Result<LowerStorage, Failure> {
|
||||
let at = Span::default();
|
||||
ledger
|
||||
.reserve(
|
||||
&syntax.budget,
|
||||
Slots::entries(
|
||||
add(syntax.metadata, syntax.budget.usage.entries, at)
|
||||
.map_err(|e| Failure::typed(syntax, None, e))?,
|
||||
),
|
||||
at,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, None, e))?;
|
||||
let mut walk = Walk {
|
||||
ledger,
|
||||
budget: &syntax.budget,
|
||||
work: Vec::new(),
|
||||
lower: LowerStorage::default(),
|
||||
};
|
||||
for (file, unit) in syntax.files.iter().enumerate() {
|
||||
let mut count = || -> Result<(), TypedError> {
|
||||
if let Some(header) = &unit.ordinary.header {
|
||||
for s in [
|
||||
&header.project.alias,
|
||||
&header.project.name,
|
||||
&header.project.description,
|
||||
] {
|
||||
walk.text(s, header.span)?;
|
||||
}
|
||||
}
|
||||
for (path, span) in &unit.ordinary.includes {
|
||||
walk.text(path, *span)?;
|
||||
}
|
||||
for declaration in &unit.ordinary.declarations {
|
||||
walk.text(&declaration.alias, declaration.span)?;
|
||||
walk.text(&declaration.name, declaration.span)?;
|
||||
for key in declaration.field_spans.keys() {
|
||||
walk.text(key, declaration.span)?;
|
||||
}
|
||||
for (key, value) in &declaration.fields {
|
||||
walk.text(key, declaration.span)?;
|
||||
walk.push(Item::Ast(value))?;
|
||||
walk.drain()?;
|
||||
}
|
||||
}
|
||||
for (_, position) in &unit.positions {
|
||||
walk.push(Item::Edit(&position.fields))?;
|
||||
walk.drain()?;
|
||||
}
|
||||
for declaration in &unit.typed {
|
||||
walk.text(
|
||||
&declaration.header.alias.name,
|
||||
declaration.header.alias.span,
|
||||
)?;
|
||||
walk.text(&declaration.header.name, declaration.header.name_span)?;
|
||||
if let Some(f) = &declaration.description {
|
||||
walk.text(&f.value, f.span)?;
|
||||
}
|
||||
match &declaration.body {
|
||||
d::Body::QuantityKind { algebra, .. } => {
|
||||
if let d::AlgebraSyntax::Affine(r) = &algebra.value {
|
||||
walk.reference(r)?;
|
||||
}
|
||||
}
|
||||
d::Body::Unit { kind, symbol, .. } => {
|
||||
walk.reference(&kind.value)?;
|
||||
walk.text(&symbol.value, symbol.span)?;
|
||||
}
|
||||
d::Body::Calculation {
|
||||
parameters,
|
||||
result,
|
||||
body,
|
||||
} => {
|
||||
for p in parameters {
|
||||
walk.reference(&p.name)?;
|
||||
walk.ty(&p.value_type)?;
|
||||
}
|
||||
walk.ty(&result.value)?;
|
||||
walk.binding(&body.value)?;
|
||||
}
|
||||
d::Body::Property {
|
||||
subject,
|
||||
value_type,
|
||||
base,
|
||||
display_unit,
|
||||
} => {
|
||||
walk.reference(&subject.value)?;
|
||||
walk.ty(&value_type.value)?;
|
||||
walk.binding(&base.value)?;
|
||||
walk.optional(display_unit)?;
|
||||
}
|
||||
d::Body::Check {
|
||||
subject,
|
||||
documented_constraint,
|
||||
predicate,
|
||||
} => {
|
||||
walk.optional(subject)?;
|
||||
walk.optional(documented_constraint)?;
|
||||
walk.binding(&predicate.value)?;
|
||||
}
|
||||
d::Body::Case {
|
||||
subject, overrides, ..
|
||||
} => {
|
||||
walk.optional(subject)?;
|
||||
for entry in &overrides.value {
|
||||
walk.reference(&entry.target)?;
|
||||
walk.binding(&entry.binding)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
count().map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
}
|
||||
Ok(walk.lower)
|
||||
}
|
||||
@@ -0,0 +1,946 @@
|
||||
//! Authored model/site oracles; never use write, Index, or a second lowering.
|
||||
use super::super::{parse, parse_with_profile};
|
||||
use super::*;
|
||||
use crate::analysis as a;
|
||||
use crate::analysis_numeric::Exact;
|
||||
use crate::source_project::{ProjectLimits, SourceBundle};
|
||||
use crate::*;
|
||||
|
||||
fn id(n: usize) -> Id {
|
||||
Id::parse_str(&format!("00000000-0000-0000-0000-{n:012}")).unwrap()
|
||||
}
|
||||
fn doc(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
|
||||
id(1)
|
||||
)
|
||||
}
|
||||
fn single(body: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(body))]),
|
||||
}
|
||||
}
|
||||
fn bundle(layer: Layer) -> SourceBundle {
|
||||
let (property, work, extra, diagram) = match layer {
|
||||
Layer::Oa => ("oa_property", "oa_activity", String::new(), ""),
|
||||
Layer::Sa => (
|
||||
"sa_property",
|
||||
"sa_function",
|
||||
format!("sa_system s \"System\" id \"{}\" {{}}", id(6)),
|
||||
"layer Sa;",
|
||||
),
|
||||
};
|
||||
single(&format!(
|
||||
"node n id \"{}\" {{ diagram @d; element @v; }}\n{property} v \"Value\" id \"{}\" {{ subject @w; type Number; base 1; }}\n{work} w \"Work\" id \"{}\" {{}}\ndiagram d \"Diagram\" id \"{}\" {{ {diagram} }}\n{extra}",
|
||||
id(4),
|
||||
id(3),
|
||||
id(2),
|
||||
id(5)
|
||||
))
|
||||
}
|
||||
fn number(n: i64) -> a::Expr {
|
||||
a::Expr::Number(Exact::from_integer(n))
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
fn expected(layer: Layer) -> Project {
|
||||
let element = |n, alias: &str, name: &str, body| Element {
|
||||
id: id(n),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body,
|
||||
};
|
||||
let mut elements = BTreeMap::from([
|
||||
(
|
||||
id(2),
|
||||
element(
|
||||
2,
|
||||
"w",
|
||||
"Work",
|
||||
match layer {
|
||||
Layer::Oa => ElementBody::Oa(OaBody::Activity),
|
||||
Layer::Sa => ElementBody::Sa(SaBody::Function),
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
id(3),
|
||||
element(
|
||||
3,
|
||||
"v",
|
||||
"Value",
|
||||
ElementBody::shared_in(
|
||||
layer,
|
||||
SharedBody::EngineeringProperty {
|
||||
subject: a::AnalysisSubject::Element(id(2)),
|
||||
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
base: a::Binding::Expression(number(1)),
|
||||
display_unit: None,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
if layer == Layer::Sa {
|
||||
elements.insert(
|
||||
id(6),
|
||||
element(6, "s", "System", ElementBody::Sa(SaBody::System)),
|
||||
);
|
||||
}
|
||||
Project {
|
||||
schema_version: 8,
|
||||
id: id(1),
|
||||
alias: "p".into(),
|
||||
name: "Project".into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements,
|
||||
relations: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
diagrams: BTreeMap::from([(
|
||||
id(5),
|
||||
Diagram {
|
||||
id: id(5),
|
||||
alias: "d".into(),
|
||||
name: "Diagram".into(),
|
||||
description: String::new(),
|
||||
layer,
|
||||
projection: ProjectionKind::Overview,
|
||||
view_options: crate::view_options::ViewOptions {
|
||||
hidden_edges: Vec::new(),
|
||||
relation_kinds: crate::view_options::RelationKinds::All,
|
||||
work_categories: crate::view_options::CategorySelection::All,
|
||||
participant_categories: crate::view_options::CategorySelection::All,
|
||||
coverage: crate::view_options::Coverage::All,
|
||||
work_contexts: crate::view_options::ContextSelection::All,
|
||||
exchange_labels: crate::view_options::ExchangeLabels::Name,
|
||||
},
|
||||
routes: Vec::new(),
|
||||
nodes: BTreeMap::from([(
|
||||
id(4),
|
||||
NodePlacement {
|
||||
id: id(4),
|
||||
alias: "n".into(),
|
||||
element: id(3),
|
||||
position: Position { x: 0., y: 0. },
|
||||
size: Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
},
|
||||
collapsed: false,
|
||||
port_anchors: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
},
|
||||
)]),
|
||||
}
|
||||
}
|
||||
fn accepted(bundle: &SourceBundle) -> ResolvedMixedProject<'_> {
|
||||
resolve(parse(bundle, ProjectLimits::default()).unwrap()).unwrap_or_else(|e| panic!("{e:?}"))
|
||||
}
|
||||
fn rejected(bundle: &SourceBundle) -> Failure {
|
||||
match resolve(parse(bundle, ProjectLimits::default()).unwrap()) {
|
||||
Ok(_) => panic!("unexpected witness"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
fn span(source: &str, context: &str, offset: usize, len: usize) -> Span {
|
||||
assert_eq!(
|
||||
source.matches(context).count(),
|
||||
1,
|
||||
"ambiguous oracle {context}"
|
||||
);
|
||||
let start = source.find(context).unwrap() + offset;
|
||||
Span {
|
||||
start,
|
||||
end: start + len,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_oa_sa_forward_model_warning_and_original_authority() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let bundle = bundle(layer);
|
||||
let parsed = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
let usage = parsed.budget.usage;
|
||||
let numeric = parsed.budget.numeric_units();
|
||||
let resolved = resolve(parsed).unwrap_or_else(|e| panic!("{e:?}"));
|
||||
assert_eq!(resolved.project(), &expected(layer));
|
||||
assert_eq!(resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(resolved.syntax.budget.numeric_units(), numeric);
|
||||
assert!(std::ptr::eq(resolved.syntax.bundle, &bundle));
|
||||
assert_eq!(resolved.syntax.files[0].path, "main.arcadia");
|
||||
assert!(std::ptr::eq(
|
||||
resolved.syntax.files[0].source.as_ptr(),
|
||||
bundle.files["main.arcadia"].as_ptr()
|
||||
));
|
||||
assert_eq!(resolved.warnings().len(), 1);
|
||||
let warning = &resolved.warnings()[0];
|
||||
assert_eq!(
|
||||
(&warning.path, warning.diagnostic.object_id),
|
||||
(&bundle.entry, Some(id(2)))
|
||||
);
|
||||
assert_eq!(warning.diagnostic.severity, validation::Severity::Warning);
|
||||
assert_eq!(warning.diagnostic.code, "unallocated_leaf");
|
||||
assert_eq!(warning.diagnostic.field.as_deref(), Some("allocation"));
|
||||
assert_eq!(
|
||||
warning.diagnostic.message,
|
||||
"Allocate leaf 'Work' to a performer."
|
||||
);
|
||||
let declaration = format!(
|
||||
"{} w \"Work\" id \"{}\" {{}}",
|
||||
if layer == Layer::Oa {
|
||||
"oa_activity"
|
||||
} else {
|
||||
"sa_function"
|
||||
},
|
||||
id(2)
|
||||
);
|
||||
assert_eq!(
|
||||
warning.diagnostic.span,
|
||||
span(
|
||||
&bundle.files["main.arcadia"],
|
||||
&declaration,
|
||||
0,
|
||||
declaration.len()
|
||||
)
|
||||
);
|
||||
assert_eq!(resolved.expressions.len(), 1);
|
||||
assert_eq!(resolved.occurrences.len(), 1);
|
||||
assert_eq!(
|
||||
resolved.expressions[0].span,
|
||||
span(&bundle.files["main.arcadia"], "base 1;", 5, 1)
|
||||
);
|
||||
assert_eq!(resolved.static_limits, a::AdmissionLimits::REFERENCE);
|
||||
assert!(resolved.static_usage.static_events > 0);
|
||||
assert!(resolved.storage.live.entries > resolved.expressions.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_global_arena_scoped_calls_and_coincident_file_offsets() {
|
||||
let mut bundle = bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main
|
||||
.replace(
|
||||
"base 1;",
|
||||
"base @f(y: 2, x: @g(x: 3, y: 4)) + @f(x: 5, y: 6);",
|
||||
)
|
||||
.replace(
|
||||
"node n",
|
||||
"include \"b.arcadia\"; include \"a.arcadia\";\nnode n",
|
||||
);
|
||||
for (file, alias, name, n, op) in [
|
||||
("a.arcadia", "f", "F", 10, "+"),
|
||||
("b.arcadia", "g", "G", 11, "-"),
|
||||
] {
|
||||
bundle.files.insert(file.into(), format!("calculation {alias} \"{name}\" id \"{}\" {{\ninput x id \"{}\" : Number;\ninput y id \"{}\" : Number;\nresult Number; body $x {op} $y;\n}}\n", id(n), id(1), id(12)));
|
||||
}
|
||||
let resolved = accepted(&bundle);
|
||||
let mut model = expected(Layer::Oa);
|
||||
for (alias, name, n, op) in [
|
||||
("f", "F", 10, a::BinaryOp::Add),
|
||||
("g", "G", 11, a::BinaryOp::Subtract),
|
||||
] {
|
||||
model.definitions.insert(
|
||||
id(n),
|
||||
a::Definition {
|
||||
id: id(n),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
|
||||
parameters: [(1, "x"), (12, "y")]
|
||||
.into_iter()
|
||||
.map(|(n, name)| a::Parameter {
|
||||
id: a::ParameterId(id(n)),
|
||||
name: name.into(),
|
||||
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
})
|
||||
.collect(),
|
||||
result: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
body: a::Binding::Expression(binary(
|
||||
op,
|
||||
a::Expr::Parameter(a::ParameterId(id(1))),
|
||||
a::Expr::Parameter(a::ParameterId(id(12))),
|
||||
)),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
let call = |n, x, y| a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(id(n)),
|
||||
arguments: BTreeMap::from([(a::ParameterId(id(1)), x), (a::ParameterId(id(12)), y)]),
|
||||
};
|
||||
let Some(SharedBody::EngineeringProperty { base, .. }) =
|
||||
model.elements.get_mut(&id(3)).unwrap().body.shared_mut()
|
||||
else {
|
||||
panic!("property")
|
||||
};
|
||||
*base = a::Binding::Expression(binary(
|
||||
a::BinaryOp::Add,
|
||||
call(10, call(11, number(3), number(4)), number(2)),
|
||||
call(10, number(5), number(6)),
|
||||
));
|
||||
assert_eq!(resolved.project(), &model);
|
||||
assert_eq!(
|
||||
resolved
|
||||
.syntax
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| f.path)
|
||||
.collect::<Vec<_>>(),
|
||||
["a.arcadia", "b.arcadia", "main.arcadia"]
|
||||
);
|
||||
use lower::{Child as C, Field as F, Role as R, Target as T};
|
||||
let mut expressions = Vec::new();
|
||||
for (file, owner, op) in [(0, 10, "+"), (1, 11, "-")] {
|
||||
let source = resolved.syntax.files[file].source;
|
||||
let root = expressions.len();
|
||||
for (text, parent) in [
|
||||
(format!("$x {op} $y"), None),
|
||||
("$x".into(), Some((root, C::Left))),
|
||||
("$y".into(), Some((root, C::Right))),
|
||||
] {
|
||||
expressions.push(lower::ExpressionSite {
|
||||
file,
|
||||
owner: id(owner),
|
||||
field: F::Body,
|
||||
parent,
|
||||
span: span(source, &text, 0, text.len()),
|
||||
});
|
||||
}
|
||||
}
|
||||
let source = &bundle.files["main.arcadia"];
|
||||
for (context, offset, len, parent) in [
|
||||
("@f(y: 2, x: @g(x: 3, y: 4)) + @f(x: 5, y: 6)", 0, 44, None),
|
||||
("@f(y: 2, x: @g(x: 3, y: 4))", 0, 27, Some((6, C::Left))),
|
||||
("y: 2", 3, 1, Some((7, C::Argument(a::ParameterId(id(12)))))),
|
||||
(
|
||||
"@g(x: 3, y: 4)",
|
||||
0,
|
||||
14,
|
||||
Some((7, C::Argument(a::ParameterId(id(1))))),
|
||||
),
|
||||
("x: 3", 3, 1, Some((9, C::Argument(a::ParameterId(id(1)))))),
|
||||
("y: 4", 3, 1, Some((9, C::Argument(a::ParameterId(id(12)))))),
|
||||
("@f(x: 5, y: 6)", 0, 14, Some((6, C::Right))),
|
||||
("x: 5", 3, 1, Some((12, C::Argument(a::ParameterId(id(1)))))),
|
||||
(
|
||||
"y: 6",
|
||||
3,
|
||||
1,
|
||||
Some((12, C::Argument(a::ParameterId(id(12))))),
|
||||
),
|
||||
] {
|
||||
expressions.push(lower::ExpressionSite {
|
||||
file: 2,
|
||||
owner: id(3),
|
||||
field: F::Base,
|
||||
parent,
|
||||
span: span(source, context, offset, len),
|
||||
});
|
||||
}
|
||||
// Exact arena order, every parent edge and full original span, not counts only.
|
||||
assert_eq!(resolved.expressions, expressions);
|
||||
let mut occurrences = Vec::new();
|
||||
for (file, owner, root) in [(0, 10, 0), (1, 11, 3)] {
|
||||
let source = resolved.syntax.files[file].source;
|
||||
for (n, name, expression, role, field) in [
|
||||
(
|
||||
1,
|
||||
"x",
|
||||
None,
|
||||
R::ParameterDeclaration,
|
||||
F::Input(a::ParameterId(id(1))),
|
||||
),
|
||||
(
|
||||
12,
|
||||
"y",
|
||||
None,
|
||||
R::ParameterDeclaration,
|
||||
F::Input(a::ParameterId(id(12))),
|
||||
),
|
||||
(1, "x", Some(root + 1), R::ParameterUse, F::Body),
|
||||
(12, "y", Some(root + 2), R::ParameterUse, F::Body),
|
||||
] {
|
||||
let context = if expression.is_none() {
|
||||
format!("input {name}")
|
||||
} else {
|
||||
format!("${name}")
|
||||
};
|
||||
occurrences.push(lower::Occurrence {
|
||||
file,
|
||||
owner: id(owner),
|
||||
field,
|
||||
expression,
|
||||
role,
|
||||
target: T::Parameter {
|
||||
calculation: id(owner),
|
||||
parameter: a::ParameterId(id(n)),
|
||||
},
|
||||
span: span(
|
||||
source,
|
||||
&context,
|
||||
if expression.is_none() { 6 } else { 1 },
|
||||
1,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (context, offset, expression, role, target, field) in [
|
||||
(
|
||||
"subject @w",
|
||||
9,
|
||||
None,
|
||||
R::Reference,
|
||||
T::Global(id(2)),
|
||||
F::Subject,
|
||||
),
|
||||
(
|
||||
"@f(y:",
|
||||
1,
|
||||
Some(7),
|
||||
R::CallTarget,
|
||||
T::Global(id(10)),
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"y: 2",
|
||||
0,
|
||||
Some(7),
|
||||
R::ArgumentLabel,
|
||||
T::Parameter {
|
||||
calculation: id(10),
|
||||
parameter: a::ParameterId(id(12)),
|
||||
},
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"x: @g",
|
||||
0,
|
||||
Some(7),
|
||||
R::ArgumentLabel,
|
||||
T::Parameter {
|
||||
calculation: id(10),
|
||||
parameter: a::ParameterId(id(1)),
|
||||
},
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"@g(x:",
|
||||
1,
|
||||
Some(9),
|
||||
R::CallTarget,
|
||||
T::Global(id(11)),
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"x: 3",
|
||||
0,
|
||||
Some(9),
|
||||
R::ArgumentLabel,
|
||||
T::Parameter {
|
||||
calculation: id(11),
|
||||
parameter: a::ParameterId(id(1)),
|
||||
},
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"y: 4",
|
||||
0,
|
||||
Some(9),
|
||||
R::ArgumentLabel,
|
||||
T::Parameter {
|
||||
calculation: id(11),
|
||||
parameter: a::ParameterId(id(12)),
|
||||
},
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"@f(x: 5",
|
||||
1,
|
||||
Some(12),
|
||||
R::CallTarget,
|
||||
T::Global(id(10)),
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"x: 5",
|
||||
0,
|
||||
Some(12),
|
||||
R::ArgumentLabel,
|
||||
T::Parameter {
|
||||
calculation: id(10),
|
||||
parameter: a::ParameterId(id(1)),
|
||||
},
|
||||
F::Base,
|
||||
),
|
||||
(
|
||||
"y: 6",
|
||||
0,
|
||||
Some(12),
|
||||
R::ArgumentLabel,
|
||||
T::Parameter {
|
||||
calculation: id(10),
|
||||
parameter: a::ParameterId(id(12)),
|
||||
},
|
||||
F::Base,
|
||||
),
|
||||
] {
|
||||
occurrences.push(lower::Occurrence {
|
||||
file: 2,
|
||||
owner: id(3),
|
||||
field,
|
||||
expression,
|
||||
role,
|
||||
target,
|
||||
span: span(source, context, offset, 1),
|
||||
});
|
||||
}
|
||||
assert_eq!(resolved.occurrences, occurrences);
|
||||
assert_eq!(resolved.expressions[0].span, resolved.expressions[3].span);
|
||||
assert_ne!(
|
||||
resolved.occurrences[0].target,
|
||||
resolved.occurrences[4].target
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_alias_first_entry_first_and_canonical_occurrence_owners() {
|
||||
for reverse in [false, true] {
|
||||
let ordinary = format!("oa_activity duplicate \"Ordinary\" id \"{}\" {{}}", id(2));
|
||||
let typed = format!(
|
||||
"calculation duplicate \"Typed\" id \"{}\" {{ result Number; body 1; }}",
|
||||
id(2)
|
||||
);
|
||||
let (first, second) = if reverse {
|
||||
(&typed, &ordinary)
|
||||
} else {
|
||||
(&ordinary, &typed)
|
||||
};
|
||||
let bundle = single(&format!("{first}\n{second}"));
|
||||
let failure = rejected(&bundle);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Duplicate global alias."
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span.start,
|
||||
bundle.files["main.arcadia"].find(second).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].related[0].span,
|
||||
span(&bundle.files["main.arcadia"], first, 0, first.len())
|
||||
);
|
||||
}
|
||||
let mut bundle = single("include \"a.arcadia\";");
|
||||
bundle.files.insert(
|
||||
"a.arcadia".into(),
|
||||
format!(
|
||||
"calculation p \"Conflict\" id \"{}\" {{ result Number; body 1; }}",
|
||||
id(1)
|
||||
),
|
||||
);
|
||||
let failure = rejected(&bundle);
|
||||
assert_eq!(failure.diagnostics[0].path, "a.arcadia");
|
||||
assert_eq!(failure.diagnostics[0].related[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Duplicate global alias."
|
||||
);
|
||||
bundle
|
||||
.files
|
||||
.get_mut("a.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(12..13, "other");
|
||||
assert_eq!(
|
||||
rejected(&bundle).diagnostics[0].diagnostic.message,
|
||||
"Duplicate global identity."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_static_architecture_and_unicode_warning_mapping() {
|
||||
let mut bundle = bundle(Layer::Oa);
|
||||
let source = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*source = format!("// 雪\r\n{}", source.replace('\n', "\r\n"));
|
||||
let resolved = accepted(&bundle);
|
||||
let warning = &resolved.warnings()[0].diagnostic;
|
||||
assert_eq!(warning.line, 6);
|
||||
assert_eq!(warning.column, 1);
|
||||
let mut bad = bundle.clone();
|
||||
*bad.files.get_mut("main.arcadia").unwrap() =
|
||||
bad.files["main.arcadia"].replace("base 1;", "base true + 1;");
|
||||
let failure = rejected(&bad);
|
||||
assert_eq!(failure.diagnostics.len(), 1); // static short circuit omits architecture warning
|
||||
let issue = &failure.diagnostics[0].diagnostic;
|
||||
assert_eq!(issue.code, "analysis_type");
|
||||
assert_eq!(issue.object_id, Some(id(3)));
|
||||
assert_eq!(issue.field.as_deref(), Some("base"));
|
||||
assert_eq!(
|
||||
issue.span,
|
||||
span(&bad.files["main.arcadia"], "true + 1", 0, 8)
|
||||
);
|
||||
let mut bad = bundle.clone();
|
||||
*bad.files.get_mut("main.arcadia").unwrap() = bad.files["main.arcadia"]
|
||||
.replace("element @v;", "element @v; size { width 0; height 94; };");
|
||||
let failure = rejected(&bad);
|
||||
assert!(failure.diagnostics.iter().any(|d| d.diagnostic.code == "invalid_bounds" && d.diagnostic.object_id == Some(id(4))));
|
||||
assert!(
|
||||
failure
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.code == "unallocated_leaf")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simultaneous_catalog_storage_is_not_individually_reset() {
|
||||
let bundle = single("");
|
||||
// Ten raw tokens + one file slot + one syntax project =12 retained slots.
|
||||
// Complete catalog: alias projection1, owner registry1, lower aliases1/IDs1.
|
||||
// Each pair fits at14; their simultaneously live total requires16.
|
||||
for (cap, expected) in [(15, false), (16, true)] {
|
||||
let profile = super::super::super::budget::Limits {
|
||||
entries: cap,
|
||||
..super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed = parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap();
|
||||
match resolve(parsed) {
|
||||
Ok(value) => {
|
||||
assert!(expected);
|
||||
assert_eq!(value.storage.live.entries, 12);
|
||||
}
|
||||
Err(failure) => {
|
||||
assert!(!expected);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapped_warning_path_copies_are_admitted_before_retention() {
|
||||
let mut bundle = single("");
|
||||
let source = bundle
|
||||
.files
|
||||
.remove("main.arcadia")
|
||||
.unwrap()
|
||||
.replace("\"Project\"", "\"\"");
|
||||
bundle.entry = format!("{}.arcadia", "a".repeat(180));
|
||||
bundle.files.insert(bundle.entry.clone(), source.clone());
|
||||
let roomy = accepted(&bundle);
|
||||
assert_eq!(roomy.warnings().len(), 1);
|
||||
assert_eq!(roomy.warnings()[0].diagnostic.code, "unnamed");
|
||||
assert_eq!(roomy.warnings()[0].path, bundle.entry);
|
||||
let profile = super::super::super::budget::Limits {
|
||||
bytes: source.len(),
|
||||
..super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed = parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap();
|
||||
let failure = match resolve(parsed) {
|
||||
Ok(_) => panic!("truncated warnings or uncharged copied path"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_steps_add_once_to_inherited_typed_reference_budget() {
|
||||
let body = format!(
|
||||
"oa_process q \"Sequence\" id \"{}\" {{ steps [@o, @r]; }}\noa_occurrence o \"First\" id \"{}\" {{ parent @q; body {{ Control {{ kind Choice; }}; }}; }}\noa_property v \"Value\" id \"{}\" {{ subject @q; type Number; base 1; }}\noa_occurrence r \"Second\" id \"{}\" {{ parent @q; body {{ Control {{ kind Choice; }}; }}; }}",
|
||||
id(2),
|
||||
id(3),
|
||||
id(4),
|
||||
id(5)
|
||||
);
|
||||
let bundle = single(&body);
|
||||
let limits = ProjectLimits {
|
||||
model: language::ParseLimits {
|
||||
max_step_references: 3,
|
||||
..language::ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let parsed = parse(&bundle, limits).unwrap();
|
||||
let before = parsed.budget.usage;
|
||||
let numeric = parsed.budget.numeric_units();
|
||||
assert_eq!(before.references, 1); // property.subject; parent is excluded
|
||||
let value = resolve(parsed).unwrap_or_else(|e| panic!("{e:?}"));
|
||||
assert_eq!(
|
||||
value.syntax.budget.usage,
|
||||
super::super::super::budget::Usage {
|
||||
references: 3,
|
||||
..before
|
||||
}
|
||||
);
|
||||
assert_eq!(value.syntax.budget.numeric_units(), numeric);
|
||||
assert_eq!(
|
||||
value.project.elements[&id(2)].steps().unwrap(),
|
||||
&[id(3), id(5)]
|
||||
);
|
||||
let tighter = ProjectLimits {
|
||||
model: language::ParseLimits {
|
||||
max_step_references: 2,
|
||||
..limits.model
|
||||
},
|
||||
..limits
|
||||
};
|
||||
let failure = match resolve(parse(&bundle, tighter).unwrap()) {
|
||||
Ok(_) => panic!("deduplicated or reset active references"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Source8 references limit exceeded."
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span.start,
|
||||
bundle.files["main.arcadia"].find("@o, @r").unwrap() + 4
|
||||
);
|
||||
let mut repeated = bundle.clone();
|
||||
*repeated.files.get_mut("main.arcadia").unwrap() =
|
||||
repeated.files["main.arcadia"].replace("@o, @r", "@o, @o");
|
||||
let failure = match resolve(parse(&repeated, tighter).unwrap()) {
|
||||
Ok(_) => panic!("repeated item was not reserved"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Source8 references limit exceeded."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_counts_branch_and_context_chains_but_excludes_historical_and_scalars() {
|
||||
// Intentionally malformed model shapes are permitted by syntax. Exercise
|
||||
// the selector independently: it must not become a second shape decoder.
|
||||
let bundle = single(&format!(
|
||||
"oa_occurrence branch \"Branch\" id \"{}\" {{ parent @owner; body {{ Branch {{ occurrences [@x, @x]; guard nonsense; }}; }}; }}\ncontextual_link link \"Link\" id \"{}\" {{ owner @owner; source {{ references [@a, @a]; occurrence @x; }}; target {{ references [@b]; occurrence @y; }}; purpose {{ references [@excluded]; }}; }}\ntrace t \"Trace\" id \"{}\" {{ oa @x; sa @y; baseline {{ steps [@excluded]; references [@excluded]; }}; }}\noa_activity other \"Other\" id \"{}\" {{ parent @owner; references [@excluded]; }}",
|
||||
id(2),
|
||||
id(3),
|
||||
id(4),
|
||||
id(5)
|
||||
));
|
||||
let mut parsed = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(parsed.budget.usage.references, 0);
|
||||
for d in &parsed.files[0].ordinary.declarations {
|
||||
active_references(d, &mut parsed.budget).unwrap();
|
||||
}
|
||||
assert_eq!(parsed.budget.usage.references, 5); // branch2 + source2 + target1
|
||||
let profile = super::super::super::budget::Limits {
|
||||
references: 4,
|
||||
..super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let mut tight = parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap();
|
||||
active_references(&tight.files[0].ordinary.declarations[0], &mut tight.budget).unwrap();
|
||||
let error =
|
||||
active_references(&tight.files[0].ordinary.declarations[1], &mut tight.budget).unwrap_err();
|
||||
assert_eq!(
|
||||
error.offset,
|
||||
bundle.files["main.arcadia"].find("@b").unwrap()
|
||||
);
|
||||
assert_eq!(error.message, "Source8 references limit exceeded.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_nested_record_policy_cannot_be_replaced_at_resolution() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body $x; }}",
|
||||
id(2),
|
||||
id(1)
|
||||
));
|
||||
// Both policies admit the two declaration records. One scoped parameter
|
||||
// and its body expression add two records at complete model admission:4.
|
||||
let tight = ProjectLimits {
|
||||
model: language::ParseLimits {
|
||||
max_records: 3,
|
||||
..language::ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let loose = ProjectLimits {
|
||||
model: language::ParseLimits {
|
||||
max_records: 4,
|
||||
..tight.model
|
||||
},
|
||||
..tight
|
||||
};
|
||||
let first = parse(&bundle, tight).unwrap();
|
||||
let second = parse(&bundle, loose).unwrap();
|
||||
assert_eq!(first.budget.usage, second.budget.usage);
|
||||
let failure = match resolve(first) {
|
||||
Ok(_) => panic!("lost retained nested-record policy"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.code, "model_limit");
|
||||
let value = resolve(second).unwrap_or_else(|e| panic!("{e:?}"));
|
||||
assert_eq!(value.syntax.limits.model.max_records, 4);
|
||||
assert_eq!(value.project.definitions.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_ordinary_construction_preserves_legacy_scratch_and_existing_errors() {
|
||||
let bundle = single(&format!("oa_activity w \"Work\" id \"{}\" {{}}", id(2)));
|
||||
let parsed = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
let file = &parsed.files[0];
|
||||
let mut old = file.ordinary.header.as_ref().unwrap().project.clone();
|
||||
old.schema_version = 3;
|
||||
let aliases = BTreeMap::from([("p".into(), id(1)), ("w".into(), id(2))]);
|
||||
let migrated = language::resolve_declarations(
|
||||
old,
|
||||
&file.ordinary.declarations,
|
||||
&aliases,
|
||||
language::ParseLimits::UNLIMITED,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(migrated.project.schema_version, SCHEMA_VERSION);
|
||||
assert_eq!(
|
||||
migrated.project.elements[&id(2)],
|
||||
Element {
|
||||
id: id(2),
|
||||
alias: "w".into(),
|
||||
name: "Work".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::Oa(OaBody::Activity)
|
||||
}
|
||||
);
|
||||
let scenario = single(&format!("oa_scenario q \"Scenario\" id \"{}\" {{}}", id(2)));
|
||||
let value = accepted(&scenario);
|
||||
assert!(
|
||||
matches!(value.project.elements[&id(2)].shared(), Some(SharedBody::Scenario { viewpoint: ScenarioViewpoint::MixedLegacy, steps, .. }) if steps.is_empty())
|
||||
);
|
||||
let missing = single(&format!(
|
||||
"node n id \"{}\" {{ diagram @d; }} diagram d \"D\" id \"{}\" {{}}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
assert_eq!(
|
||||
rejected(&missing).diagnostics[0].diagnostic.message,
|
||||
"Missing required field 'element'."
|
||||
);
|
||||
let conflict = single(&format!(
|
||||
"oa_activity outer \"Outer\" id \"{}\" {{ oa_activity child \"Child\" id \"{}\" {{ parent none; }} }}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
assert_eq!(
|
||||
rejected(&conflict).diagnostics[0].diagnostic.code,
|
||||
"conflicting_parent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherited_storage_failure_and_success_drop_deep_syntax_on_128k_stack() {
|
||||
let run = || {
|
||||
let expression = format!("{}1{}", "abs(".repeat(127), ")".repeat(127));
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body {expression}; }}",
|
||||
id(2)
|
||||
));
|
||||
let value = accepted(&bundle);
|
||||
assert_eq!(value.expressions.len(), 128);
|
||||
drop(value);
|
||||
let profile = super::super::super::budget::Limits {
|
||||
entries: 1000,
|
||||
..super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed = parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap();
|
||||
match resolve(parsed) {
|
||||
Ok(_) => panic!("deep live lower reservation unexpectedly fit"),
|
||||
Err(e) => assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
),
|
||||
}
|
||||
};
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(run)
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scoped_parameter_faults_keep_real_file_and_unknown_fields_use_owner_fallback() {
|
||||
let mut bundle = single("include \"a.arcadia\"; include \"b.arcadia\";");
|
||||
for (file, alias, n) in [("a.arcadia", "f", 10), ("b.arcadia", "g", 11)] {
|
||||
bundle.files.insert(file.into(), format!("calculation {alias} \"F\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body $missing; }}", id(n), id(1)));
|
||||
}
|
||||
let parsed = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
for file in 0..2 {
|
||||
let declaration = &parsed.files[file].typed[0];
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Typed(0),
|
||||
span: declaration.span,
|
||||
};
|
||||
for field in [
|
||||
"parameters".into(),
|
||||
format!("parameters.{}.name", id(1)),
|
||||
format!("unrelated.{}.type", id(1)),
|
||||
] {
|
||||
assert_eq!(
|
||||
field_span(&parsed.files[file], owner, Some(&field)),
|
||||
declaration.span
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
field_span(&parsed.files[file], owner, Some("body")),
|
||||
span(parsed.files[file].source, "$missing", 0, 8)
|
||||
);
|
||||
}
|
||||
let failure = match resolve(parsed) {
|
||||
Ok(_) => panic!("unknown scoped parameter"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics[0].path, "a.arcadia");
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span,
|
||||
span(&bundle.files["a.arcadia"], "$missing", 1, 7)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_call_borrows_large_signature_and_refuses_at_single_admission() {
|
||||
let inputs = (0..40)
|
||||
.map(|n| format!("input p{n} id \"{}\" : Number;", id(100 + n)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let bundle = single(&format!(
|
||||
"calculation f \"F\" id \"{}\" {{ {inputs} result Number; body $p0; }}\ncalculation g \"G\" id \"{}\" {{ result Number; body @f(p0: 1); }}",
|
||||
id(10),
|
||||
id(11)
|
||||
));
|
||||
let parsed = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
let mut storage = Ledger::default();
|
||||
let counted = accounting::syntax(&parsed, &mut storage).unwrap();
|
||||
assert_eq!(counted.arguments, 1);
|
||||
assert_eq!(counted.nodes, 3); // f's $p0; g's call and literal
|
||||
assert_eq!(counted.occurrences, 43); // forty declarations, use, target, label
|
||||
let failure = match resolve(parsed) {
|
||||
Ok(_) => panic!("incomplete call admitted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.code, "analysis_arguments");
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.object_id, Some(id(11)));
|
||||
}
|
||||
@@ -180,14 +180,96 @@ fn check_ids(out: &mut Vec<Issue>, owner: Id, field: &str, ids: &[Id]) {
|
||||
}
|
||||
|
||||
pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
validate_with_usage(p).0
|
||||
}
|
||||
|
||||
/// Runs the shared static/architectural route once and retains successful static
|
||||
/// metadata without retaining a Project-borrowing environment. Architectural
|
||||
/// issue production is outside the static meter and any caller auxiliary ledger;
|
||||
/// callers must reserve separately before mapping or retaining these issues.
|
||||
pub(crate) fn validate_with_usage(
|
||||
p: &Project,
|
||||
) -> (
|
||||
Vec<Issue>,
|
||||
Option<(
|
||||
crate::analysis::AdmissionLimits,
|
||||
crate::analysis::AdmissionUsage,
|
||||
)>,
|
||||
) {
|
||||
// Bound analysis trees before architectural visitors can clone an element.
|
||||
// Admission itself never enters this public validator recursively.
|
||||
if let Err(failure) = crate::analysis::admit_structure_checked(p, crate::analysis::AdmissionLimits::REFERENCE) {
|
||||
let mut out = Vec::new();
|
||||
error(&mut out, failure.object, &failure.code, &failure.field, failure.message);
|
||||
return out;
|
||||
let environment = match crate::analysis::admit_structure_checked(
|
||||
p,
|
||||
crate::analysis::AdmissionLimits::REFERENCE,
|
||||
) {
|
||||
Ok(environment) => environment,
|
||||
Err(failure) => {
|
||||
let mut out = Vec::new();
|
||||
error(
|
||||
&mut out,
|
||||
failure.object,
|
||||
&failure.code,
|
||||
&failure.field,
|
||||
failure.message,
|
||||
);
|
||||
return (out, None);
|
||||
}
|
||||
};
|
||||
let metadata = (
|
||||
environment.admission_limits(),
|
||||
environment.admission_usage(),
|
||||
);
|
||||
drop(environment);
|
||||
(validate_structure(p), Some(metadata))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod shared_admission_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn successful_static_metadata_survives_architectural_warnings() {
|
||||
let mut project = Project::empty();
|
||||
project.name.clear();
|
||||
let (issues, metadata) = validate_with_usage(&project);
|
||||
let (limits, usage) = metadata.expect("static admission succeeds");
|
||||
assert_eq!(limits, crate::analysis::AdmissionLimits::REFERENCE);
|
||||
assert!(usage.static_events > 0);
|
||||
assert_eq!(usage.stored_expression_nodes, 0);
|
||||
assert!(issues.iter().any(|issue| issue.code == "unnamed"
|
||||
&& issue.element == Some(project.id)
|
||||
&& issue.severity == Severity::Warning));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_failure_returns_no_metadata_and_skips_architectural_warnings() {
|
||||
use crate::analysis::{
|
||||
AnalysisType, Binding, CalculationDefinition, Definition, DefinitionBody, ScalarType,
|
||||
};
|
||||
let mut project = Project::empty();
|
||||
project.name.clear();
|
||||
let key = Id::from_u128(10);
|
||||
project.definitions.insert(
|
||||
key,
|
||||
Definition {
|
||||
id: Id::from_u128(11),
|
||||
alias: "f".into(),
|
||||
name: "F".into(),
|
||||
description: String::new(),
|
||||
body: DefinitionBody::Calculation(CalculationDefinition {
|
||||
parameters: Vec::new(),
|
||||
result: AnalysisType::Scalar(ScalarType::Number),
|
||||
body: Binding::Unbound,
|
||||
}),
|
||||
},
|
||||
);
|
||||
let (issues, metadata) = validate_with_usage(&project);
|
||||
assert!(metadata.is_none());
|
||||
assert_eq!(issues.len(), 1);
|
||||
assert_eq!(issues[0].severity, Severity::Error);
|
||||
assert_eq!(issues[0].element, Some(key));
|
||||
assert_eq!(issues[0].code, "identity_mismatch");
|
||||
}
|
||||
validate_structure(p)
|
||||
}
|
||||
|
||||
/// Architecture/header validation without recursively re-entering analysis admission.
|
||||
|
||||
Reference in New Issue
Block a user