Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaec78fcc9 | ||
|
|
71cadfa0cd | ||
|
|
b9a31bb848 | ||
|
|
8f0d96ca63 |
@@ -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,23 @@ 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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Private mixed Project implementation
|
||||
|
||||
Source-only checkpoint; compilation and runtime acceptance are pending. Base is `a578ac98a06772a60bec00ae287e65650680d119`. The accepted contract is `aecee2aff3f4410648a501a70cb2e6160c81adc5:harness/analysis-mixed-project-contract-20260923.md`; this note records implementation choices, not a replacement specification.
|
||||
|
||||
`mixed::project::resolve` consumes the actual complete syntax witness. The sealed result retains Project, warnings, static admission metadata, original syntax/Budget/ProjectLimits/bundle, unchanged expression/occurrence vectors and their canonical file keys. Only Project/warnings are exposed read-only. The empty Project header is copied into model ownership; the original header remains in syntax. No declarations, typed models, tokens or Exact values are reparsed to establish authority.
|
||||
|
||||
The catalog seeds the entry first and merges ordinary/typed headers by original declaration start within canonical file order. It checks alias before global ID, retaining both occurrence owners on collision. Existing typed-only catalog precedence remains unchanged. Parameter names/IDs remain calculation scoped. Ordinary construction is the shared existing loop with a pre-field hook; its legacy wrapper preserves source3 scratch and all migration/admission tails. Typed lowering shares the existing body resolver over borrowed declarations and one complete catalog. Moved model records must be disjoint before insertion.
|
||||
|
||||
## Live auxiliary storage
|
||||
|
||||
Every reservation uses checked arithmetic and tests the aggregate through the inherited Budget's noncharging entry, occurrence and owned-byte checks. No new Budget or reset occurs. Failure aborts the phase. Logical slots are not allocator capacity/RSS.
|
||||
|
||||
| Representation | Reservation and end of lifetime |
|
||||
| --- | --- |
|
||||
| Original syntax | Retained mixed metadata subtotal plus parsed syntax entries. Includes the existing conservative dropped value-end subtotal. Tokens/positions are not counted a second time. An iterative walk counts separately owned strings, with each counting-stack push reserved first. Source bytes stay separately charged once. Baseline remains live. |
|
||||
| Complete catalog | Lower aliases+global ID set: two entries/global and owned alias bytes. Scoped parameter map: one entry/calculation plus one entry/name and copied name bytes. Per-signature temporary ID set reserves one entry/parameter and releases after `Tables::typed` returns. |
|
||||
| Alias projection and diagnostic owners | One entry+copied alias bytes/global for projection; one entry/global for owner registry. Projection drops after construction, owner registry after diagnostic mapping. Both overlap the lower catalog initially. |
|
||||
| Ordinary coordination | One borrowed `(file,declaration)` entry/ordinary occurrence. One deferred-node entry before that declaration's field cloning. Release after construction/attachment returns and the views drop. |
|
||||
| Typed lowering | Bounded syntax walk counts N expressions, A supplied call arguments, O emitted occurrences. Reserve `5N+2A` entries and O occurrences before lowering. N entries are retained expression sites. Temporaries are bounded by 2N Visit/Build slots, N borrowed child slots, N value/split slots, A call-parameter slots and A seen-set slots. Hold this conservative whole-phase allowance until all Resolver temporaries die; then release only the difference from actual retained global sites. |
|
||||
| Mapped diagnostics | Before retention, reserve one output slot, copied canonical path bytes and transferred Issue code/field/message String bytes. No line cache is allocated. Owner registry and syntax/sites remain live during mapping. Failure cannot produce truncated-warning success. |
|
||||
|
||||
The call signature is borrowed from the already accounted catalog; no lowerer allocation grows with unprovided callee parameters. Unknown/incomplete calls cannot exceed the supplied-argument bound. O includes parameter declarations, scalar-type references, non-null optional references, call targets/labels and expression reference forms; headers and display text count only as text. Failed lowering drops all partial output with its reservation.
|
||||
|
||||
Model/serde allocations and architectural validation's raw Issue vector/strings/indexes remain explicitly outside this preallocation ledger. Model limits are checked on the complete Project; static analysis retains its existing independent meter. Raw Issues are never retained uncharged in an accepted result. Rejection diagnostics are small error transport outside accepted-witness storage, so exhaustion itself remains reportable.
|
||||
|
||||
The ordinary hook selects exactly Process/FunctionalChain/Scenario steps, Occurrence Branch occurrences, and ContextualLink source then target reference arrays before field resolution/cloning. Repeated items count separately; scalar parent/owner, endpoint occurrence, unrelated fields and every Trace baseline snapshot are excluded. Recognizable malformed arrays are charged before the unchanged decoder reports their shape errors.
|
||||
|
||||
## Admission, mapping and verification
|
||||
|
||||
`validation::validate_with_usage` performs one static admission, copies owned usage/limits, drops the borrowing environment, then performs one architecture validation. `validate` uses the same route and returns its Issues. Static failure preserves the original short circuit. The mixed path maps all Issues and refuses any error. Known typed fields map precisely; unsupported semantic field paths, including the current aggregate `parameters`, explicitly fall back to their owner. Exact scoped-parameter lowerer failures retain their original file and full span; UUID substrings never manufacture precision.
|
||||
|
||||
Authored tests cover whole OA/SA models and exact warnings; a complete 15-expression/18-occurrence arena oracle with repeated/reordered calls, scoped UUID reuse and coincident file offsets; catalog collisions; ordinary reference selectors/cumulative limits; simultaneous storage and diagnostic-path copying; inherited nested records; legacy scratch/default/errors; static/architecture refusal; parameter diagnostic fallback; large-signature incomplete calls; and 128 KiB success/rejection cleanup. The full model/site oracle was independently designed before implementation. Tests are unexecuted at this checkpoint.
|
||||
|
||||
Required later gates: Windows listing, focused new Project tests, complete private typed tests (including lower/index/update and small-stack controls), existing public parser/traversal/migration regressions with preserved baseline disposition, and strict all-target core Clippy. The known inherited formatting/full-suite failures must remain explicit. Source formatting is a parsing check, not typechecking or runtime evidence.
|
||||
|
||||
Index construction/verification, ordinary occurrence indexing, old-updater access, public source8 activation, stabilization, mixed edit composition, output recompile/admission, intended-Project equality and root Scenario/editor reconciliation remain deferred. `index.rs` and `update.rs` are unchanged.
|
||||
Reference in New Issue
Block a user