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,
|
mut project: Project,
|
||||||
declarations: &[Declaration],
|
declarations: impl IntoIterator<Item = &'a Declaration>,
|
||||||
aliases: &BTreeMap<String, Id>,
|
aliases: &BTreeMap<String, Id>,
|
||||||
limits: ParseLimits,
|
mut before_fields: impl FnMut(&Declaration) -> Result<(), Error>,
|
||||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
) -> Result<ConstructedDeclarations, (usize, Error)> {
|
||||||
let legacy = project.schema_version == 3;
|
let legacy = project.schema_version == 3;
|
||||||
let mut legacy_elements = serde_json::Map::new();
|
let mut legacy_elements = serde_json::Map::new();
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
for (index, declaration) in declarations.iter().enumerate() {
|
for (index, declaration) in declarations.into_iter().enumerate() {
|
||||||
(|| -> Result<(), Error> {
|
(|| -> Result<(), Error> {
|
||||||
|
before_fields(declaration)?;
|
||||||
let mut fields = declaration
|
let mut fields = declaration
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1899,6 +1907,23 @@ pub(crate) fn resolve_declarations(
|
|||||||
.map_err(|error| (index, error))?;
|
.map_err(|error| (index, error))?;
|
||||||
target.nodes.insert(node.id, node);
|
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 {
|
if legacy {
|
||||||
let migrated = source3_bridge::from_scratch(project, legacy_elements)
|
let migrated = source3_bridge::from_scratch(project, legacy_elements)
|
||||||
.and_then(|old| crate::persistence::v3::migrate(old, limits))
|
.and_then(|old| crate::persistence::v3::migrate(old, limits))
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ struct Symbol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Tables {
|
pub(super) struct Tables {
|
||||||
aliases: BTreeMap<String, Symbol>,
|
aliases: BTreeMap<String, Symbol>,
|
||||||
ids: BTreeSet<Id>,
|
ids: BTreeSet<Id>,
|
||||||
parameters: BTreeMap<Id, BTreeMap<String, a::ParameterId>>,
|
parameters: BTreeMap<Id, BTreeMap<String, a::ParameterId>>,
|
||||||
@@ -157,7 +157,7 @@ impl Tables {
|
|||||||
self.entries += 1;
|
self.entries += 1;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn insert(
|
pub(super) fn insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: Id,
|
id: Id,
|
||||||
alias: &str,
|
alias: &str,
|
||||||
@@ -233,7 +233,11 @@ impl Tables {
|
|||||||
Ok(())
|
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 header = &declaration.header;
|
||||||
let id = identity(&header.identity, header.alias.span)?;
|
let id = identity(&header.identity, header.alias.span)?;
|
||||||
self.insert(
|
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 {
|
let mut output = Lowered {
|
||||||
definitions: BTreeMap::new(),
|
definitions: BTreeMap::new(),
|
||||||
elements: BTreeMap::new(),
|
elements: BTreeMap::new(),
|
||||||
occurrences: Vec::new(),
|
occurrences: Vec::new(),
|
||||||
expressions: Vec::new(),
|
expressions: Vec::new(),
|
||||||
};
|
};
|
||||||
for unit in units {
|
for (file, declaration) in declarations {
|
||||||
for declaration in &unit.declarations {
|
let owner = declaration
|
||||||
let owner = declaration
|
.header
|
||||||
.header
|
.identity
|
||||||
.identity
|
.value
|
||||||
.value
|
.expect("collected stable identity");
|
||||||
.expect("collected stable identity");
|
let mut resolver = Resolver {
|
||||||
let mut resolver = Resolver {
|
tables,
|
||||||
tables: &tables,
|
output: &mut output,
|
||||||
output: &mut output,
|
budget,
|
||||||
budget,
|
file,
|
||||||
file: unit.file,
|
owner,
|
||||||
owner,
|
};
|
||||||
};
|
resolver.declaration(declaration).map_err(|error| Failure {
|
||||||
resolver.declaration(declaration).map_err(|error| Failure {
|
file: Some(file),
|
||||||
file: Some(unit.file),
|
error,
|
||||||
error,
|
})?;
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ pub(super) struct ParsedMixedSyntax<'s> {
|
|||||||
limits: ProjectLimits,
|
limits: ProjectLimits,
|
||||||
files: Vec<FileSyntax<'s>>,
|
files: Vec<FileSyntax<'s>>,
|
||||||
budget: Budget,
|
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> {
|
struct FileSyntax<'s> {
|
||||||
@@ -369,6 +372,7 @@ fn parse_with_profile(
|
|||||||
limits,
|
limits,
|
||||||
files: parsed.into_values().collect(),
|
files: parsed.into_values().collect(),
|
||||||
budget,
|
budget,
|
||||||
|
metadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,3 +402,5 @@ fn ordinary_position_slots(unit: &SourceUnit) -> usize {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
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> {
|
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.
|
// Bound analysis trees before architectural visitors can clone an element.
|
||||||
// Admission itself never enters this public validator recursively.
|
// Admission itself never enters this public validator recursively.
|
||||||
if let Err(failure) = crate::analysis::admit_structure_checked(p, crate::analysis::AdmissionLimits::REFERENCE) {
|
let environment = match crate::analysis::admit_structure_checked(
|
||||||
let mut out = Vec::new();
|
p,
|
||||||
error(&mut out, failure.object, &failure.code, &failure.field, failure.message);
|
crate::analysis::AdmissionLimits::REFERENCE,
|
||||||
return out;
|
) {
|
||||||
|
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.
|
/// 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