Index admitted mixed typed source without losing bundle authority

This commit is contained in:
2026-09-24 14:34:22 -07:00
parent aaec78fcc9
commit a18b7966e5
7 changed files with 964 additions and 99 deletions
@@ -237,16 +237,201 @@ impl<'s> Index<'s> {
for element in lowered.elements.values() { for element in lowered.elements.values() {
insert_name(Target::Global(element.id), &element.alias)?; insert_name(Target::Global(element.id), &element.alias)?;
} }
let mut index = Self { let original = Original {
kind: OriginalKind::Typed { sources, witnesses },
at: std::cell::Cell::new(None),
};
let data = IndexData::build(
names,
units
.iter()
.flat_map(|u| u.declarations.iter().map(move |d| (u.file, d))),
&lowered.expressions,
&lowered.occurrences,
&original,
budget,
&mut Storage(None),
)?;
Ok(Self {
sources, sources,
names: data.names,
occurrences: data.occurrences,
expressions: data.expressions,
})
}
}
use super::mixed::project::{
IndexInput,
accounting::{Ledger, Slots},
};
/// Owned maps shared by the two nominal provenance wrappers. No edit authority.
#[derive(Debug)]
pub(super) struct IndexData {
pub(super) names: BTreeMap<Target, String>,
pub(super) occurrences: BTreeMap<OccurrenceKey, Location>,
pub(super) expressions: BTreeMap<ExpressionKey, Location>,
}
// Only project::index can mint IndexInput: its private seal is never exported.
pub(super) fn mixed_data(
input: IndexInput<'_>,
budget: &mut Budget,
ledger: &mut Ledger,
) -> Result<IndexData, lower::Failure> {
let original = Original {
kind: OriginalKind::Mixed(&input),
at: std::cell::Cell::new(None),
};
let mut storage = Storage(Some(ledger));
let result = (|| {
budget.active()?;
let mut names = BTreeMap::new();
let mut insert = |target, name: &str| -> Result<(), Error> {
storage.reserve(budget, 1, name.len(), Span::default())?;
if names.insert(target, name.to_owned()).is_some() {
return Err(error(
Span::default(),
"Duplicate qualified catalog identity.",
));
}
Ok(())
};
let p = input.project;
insert(Target::Global(p.id), &p.alias)?;
for d in p.definitions.values() {
insert(Target::Global(d.id), &d.alias)?;
if let a::DefinitionBody::Calculation(c) = &d.body {
for parameter in &c.parameters {
insert(
Target::Parameter {
calculation: d.id,
parameter: parameter.id,
},
&parameter.name,
)?;
}
}
}
for v in p.elements.values() {
insert(Target::Global(v.id), &v.alias)?;
}
for v in p.relations.values() {
insert(Target::Global(v.id), &v.alias)?;
}
for v in p.diagrams.values() {
insert(Target::Global(v.id), &v.alias)?;
for n in v.nodes.values() {
insert(Target::Global(n.id), &n.alias)?;
}
}
for v in p.artifact_links.values() {
insert(Target::Global(v.id), &v.alias)?;
}
for v in p.traces.values() {
insert(Target::Global(v.id), &v.alias)?;
}
IndexData::build(
names,
input.declarations(),
input.expressions,
input.occurrences,
&original,
budget,
&mut storage,
)
})();
result.map_err(|error| lower::Failure {
file: original.at.get(),
error,
})
}
struct Storage<'a>(Option<&'a mut Ledger>);
impl Storage<'_> {
fn reserve(
&mut self,
budget: &Budget,
entries: usize,
text: usize,
at: Span,
) -> Result<(), Error> {
if let Some(ledger) = &mut self.0 {
ledger.reserve(
budget,
Slots {
entries,
occurrences: 0,
text,
},
at,
)?;
}
Ok(())
}
fn release(&mut self, entries: usize) {
if let Some(ledger) = &mut self.0 {
ledger.release(Slots::entries(entries));
}
}
}
struct Original<'a> {
kind: OriginalKind<'a>,
at: std::cell::Cell<Option<usize>>,
}
enum OriginalKind<'a> {
Typed {
sources: &'a BTreeMap<usize, String>,
witnesses: &'a BTreeMap<usize, Vec<lex::Token>>,
},
Mixed(&'a IndexInput<'a>),
}
impl Original<'_> {
fn checked(&self, location: Location) -> Result<&str, Error> {
self.at.set(Some(location.file));
if location.span.start >= location.span.end {
return Err(error(location.span, "Empty or reversed source span."));
}
let source = match &self.kind {
OriginalKind::Typed { sources, .. } => sources.get(&location.file).map(String::as_str),
OriginalKind::Mixed(input) => input.source(location.file),
};
source
.and_then(|s| s.get(location.span.start..location.span.end))
.ok_or_else(|| {
error(
location.span,
"Source span is outside its original UTF-8 file.",
)
})
}
fn tokens(&self, file: usize) -> Option<&[lex::Token]> {
match &self.kind {
OriginalKind::Typed { witnesses, .. } => witnesses.get(&file).map(Vec::as_slice),
OriginalKind::Mixed(input) => input.tokens(file),
}
}
}
impl IndexData {
fn build<'a>(
names: BTreeMap<Target, String>,
declarations: impl Iterator<Item = (usize, &'a declarations::Declaration)> + Clone,
expressions: &[lower::ExpressionSite],
occurrences: &[lower::Occurrence],
sources: &Original<'_>,
budget: &mut Budget,
storage: &mut Storage<'_>,
) -> Result<Self, Error> {
let mut index = Self {
names, names,
occurrences: BTreeMap::new(), occurrences: BTreeMap::new(),
expressions: BTreeMap::new(), expressions: BTreeMap::new(),
}; };
let mut owners = BTreeMap::new(); let mut owners = BTreeMap::new();
let mut physical = BTreeSet::new(); let mut physical = BTreeSet::new();
for unit in units { for (file, declaration) in declarations.clone() {
for declaration in &unit.declarations { sources.at.set(Some(file));
let owner = declaration.header.identity.value.ok_or_else(|| { let owner = declaration.header.identity.value.ok_or_else(|| {
error( error(
declaration.span, declaration.span,
@@ -254,11 +439,12 @@ impl<'s> Index<'s> {
) )
})?; })?;
let location = Location { let location = Location {
file: unit.file, file,
span: declaration.span, span: declaration.span,
}; };
checked_source(sources, location)?; sources.checked(location)?;
budget.lowering_entries(owners.len() + 1, declaration.span)?; budget.lowering_entries(owners.len() + 1, declaration.span)?;
storage.reserve(budget, 1, 0, declaration.span)?;
if owners.insert(owner, location).is_some() { if owners.insert(owner, location).is_some() {
return Err(error(declaration.span, "Duplicate source owner.")); return Err(error(declaration.span, "Duplicate source owner."));
} }
@@ -272,20 +458,21 @@ impl<'s> Index<'s> {
index.insert( index.insert(
key, key,
Location { Location {
file: unit.file, file,
span: declaration.header.alias.span, span: declaration.header.alias.span,
}, },
witnesses, sources,
&mut physical, &mut physical,
budget, budget,
storage,
)?; )?;
} }
}
// Parent-before-child is part of the lowering contract. One forward // Parent-before-child is part of the lowering contract. One forward
// pass rejects forward, cyclic, cross-file/owner/field and duplicate // pass rejects forward, cyclic, cross-file/owner/field and duplicate
// paths. Each node allocates one shared edge, not a copied path vector. // paths. Each node allocates one shared edge, not a copied path vector.
let mut paths: Vec<(ExpressionKey, usize)> = Vec::new(); let mut paths: Vec<(ExpressionKey, usize)> = Vec::new();
for (position, site) in lowered.expressions.iter().enumerate() { for (position, site) in expressions.iter().enumerate() {
sources.at.set(Some(site.file));
if !matches!( if !matches!(
site.field, site.field,
Field::Body | Field::Base | Field::Predicate | Field::Override(_) Field::Body | Field::Base | Field::Predicate | Field::Override(_)
@@ -299,7 +486,7 @@ impl<'s> Index<'s> {
file: site.file, file: site.file,
span: site.span, span: site.span,
}; };
checked_source(sources, location)?; sources.checked(location)?;
let owner = owners let owner = owners
.get(&site.owner) .get(&site.owner)
.ok_or_else(|| error(site.span, "Unknown expression owner."))?; .ok_or_else(|| error(site.span, "Unknown expression owner."))?;
@@ -311,7 +498,7 @@ impl<'s> Index<'s> {
"Expression parent must precede its child.", "Expression parent must precede its child.",
)); ));
} }
let ancestor = &lowered.expressions[parent]; let ancestor = &expressions[parent];
if ancestor.owner != site.owner if ancestor.owner != site.owner
|| ancestor.field != site.field || ancestor.field != site.field
|| ancestor.file != site.file || ancestor.file != site.file
@@ -336,6 +523,7 @@ impl<'s> Index<'s> {
} }
let depth = paths[parent].1 + 1; let depth = paths[parent].1 + 1;
budget.frame(depth, site.span)?; budget.frame(depth, site.span)?;
storage.reserve(budget, 1, 0, site.span)?;
( (
Path(Some(Arc::new(Step { Path(Some(Arc::new(Step {
parent: paths[parent].0.path.clone(), parent: paths[parent].0.path.clone(),
@@ -352,12 +540,14 @@ impl<'s> Index<'s> {
field: site.field, field: site.field,
path, path,
}; };
storage.reserve(budget, 2, 0, site.span)?;
if index.expressions.insert(key.clone(), location).is_some() { if index.expressions.insert(key.clone(), location).is_some() {
return Err(error(site.span, "Duplicate qualified expression path.")); return Err(error(site.span, "Duplicate qualified expression path."));
} }
paths.push((key, depth)); paths.push((key, depth));
} }
for occurrence in &lowered.occurrences { for occurrence in occurrences {
sources.at.set(Some(occurrence.file));
let location = Location { let location = Location {
file: occurrence.file, file: occurrence.file,
span: occurrence.span, span: occurrence.span,
@@ -368,8 +558,7 @@ impl<'s> Index<'s> {
contained(*owner, location)?; contained(*owner, location)?;
let path = match occurrence.expression { let path = match occurrence.expression {
Some(site) => { Some(site) => {
let expression = lowered let expression = expressions
.expressions
.get(site) .get(site)
.ok_or_else(|| error(occurrence.span, "Unknown occurrence expression."))?; .ok_or_else(|| error(occurrence.span, "Unknown occurrence expression."))?;
if expression.owner != occurrence.owner if expression.owner != occurrence.owner
@@ -420,20 +609,28 @@ impl<'s> Index<'s> {
role: TokenRole::Use(occurrence.role), role: TokenRole::Use(occurrence.role),
target: occurrence.target, target: occurrence.target,
}; };
index.insert(key, location, witnesses, &mut physical, budget)?; index.insert(key, location, sources, &mut physical, budget, storage)?;
} }
let owner_count = owners.len();
let physical_count = physical.len();
drop(owners);
drop(physical);
storage.release(owner_count + physical_count);
// Argument labels belong to this precise call site and its callee, not // Argument labels belong to this precise call site and its callee, not
// merely to a parameter UUID/name present somewhere in the catalog. // merely to a parameter UUID/name present somewhere in the catalog.
let mut calls = BTreeMap::new(); let mut calls = BTreeMap::new();
for (key, at) in &index.occurrences { for (key, at) in &index.occurrences {
sources.at.set(Some(at.file));
if key.role == TokenRole::Use(Role::CallTarget) { if key.role == TokenRole::Use(Role::CallTarget) {
let site = (key.owner, key.field, key.path.clone()); let site = (key.owner, key.field, key.path.clone());
storage.reserve(budget, 1, 0, at.span)?;
if calls.insert(site, key.target).is_some() { if calls.insert(site, key.target).is_some() {
return Err(error(at.span, "Multiple callees at one expression site.")); return Err(error(at.span, "Multiple callees at one expression site."));
} }
} }
} }
for (key, at) in &index.occurrences { for (key, at) in &index.occurrences {
sources.at.set(Some(at.file));
if let (TokenRole::Use(Role::ArgumentLabel), Target::Parameter { calculation, .. }) = if let (TokenRole::Use(Role::ArgumentLabel), Target::Parameter { calculation, .. }) =
(key.role, key.target) (key.role, key.target)
&& calls.get(&(key.owner, key.field, key.path.clone())) && calls.get(&(key.owner, key.field, key.path.clone()))
@@ -445,7 +642,8 @@ impl<'s> Index<'s> {
)); ));
} }
} }
for (position, site) in lowered.expressions.iter().enumerate() { for (position, site) in expressions.iter().enumerate() {
sources.at.set(Some(site.file));
if let Some((parent, Child::Argument(parameter))) = site.parent { if let Some((parent, Child::Argument(parameter))) = site.parent {
let key = &paths[parent].0; let key = &paths[parent].0;
let Some(Target::Global(calculation)) = let Some(Target::Global(calculation)) =
@@ -468,7 +666,12 @@ impl<'s> Index<'s> {
} }
} }
} }
index.check_syntax_edges(units, lowered, budget)?; let path_count = paths.len();
let call_count = calls.len();
drop(paths);
drop(calls);
storage.release(path_count + call_count);
index.check_syntax_edges(declarations, expressions, sources, budget, storage)?;
Ok(index) Ok(index)
} }
@@ -477,12 +680,16 @@ impl<'s> Index<'s> {
/// with indexed span lookup; it does not lex or copy source/path strings. /// with indexed span lookup; it does not lex or copy source/path strings.
fn check_syntax_edges<'a>( fn check_syntax_edges<'a>(
&self, &self,
units: &'a [Unit], declarations: impl Iterator<Item = (usize, &'a declarations::Declaration)> + Clone,
lowered: &Lowered, expressions: &[lower::ExpressionSite],
sources: &Original<'_>,
budget: &Budget, budget: &Budget,
storage: &mut Storage<'_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut globals = BTreeMap::new(); let mut globals = BTreeMap::new();
let mut parameters = BTreeMap::new(); let mut parameters = BTreeMap::new();
sources.at.set(None);
storage.reserve(budget, self.names.len(), 0, Span::default())?;
for (target, name) in &self.names { for (target, name) in &self.names {
match target { match target {
Target::Global(id) => { Target::Global(id) => {
@@ -497,7 +704,9 @@ impl<'s> Index<'s> {
} }
} }
let mut sites = BTreeMap::new(); let mut sites = BTreeMap::new();
for (position, site) in lowered.expressions.iter().enumerate() { for (position, site) in expressions.iter().enumerate() {
sources.at.set(Some(site.file));
storage.reserve(budget, 1, 0, site.span)?;
if sites if sites
.insert( .insert(
( (
@@ -518,39 +727,43 @@ impl<'s> Index<'s> {
let mut root = |file, owner, field, binding: &'a super::Binding| -> Result<(), Error> { let mut root = |file, owner, field, binding: &'a super::Binding| -> Result<(), Error> {
if let super::Binding::Expression(expression) = binding { if let super::Binding::Expression(expression) = binding {
budget.lowering_entries(work.len() + 1, expression.span)?; budget.lowering_entries(work.len() + 1, expression.span)?;
sources.at.set(Some(file));
storage.reserve(budget, 1, 0, expression.span)?;
work.push((file, owner, field, expression, None)); work.push((file, owner, field, expression, None));
} }
Ok(()) Ok(())
}; };
for unit in units { for (file, declaration) in declarations.clone() {
for declaration in &unit.declarations { sources.at.set(Some(file));
let owner = declaration.header.identity.value.expect("validated owner"); let owner = declaration.header.identity.value.expect("validated owner");
match &declaration.body { match &declaration.body {
declarations::Body::Calculation { body, .. } => { declarations::Body::Calculation { body, .. } => {
root(unit.file, owner, Field::Body, &body.value)? root(file, owner, Field::Body, &body.value)?
} }
declarations::Body::Property { base, .. } => { declarations::Body::Property { base, .. } => {
root(unit.file, owner, Field::Base, &base.value)? root(file, owner, Field::Base, &base.value)?
} }
declarations::Body::Check { predicate, .. } => { declarations::Body::Check { predicate, .. } => {
root(unit.file, owner, Field::Predicate, &predicate.value)? root(file, owner, Field::Predicate, &predicate.value)?
} }
declarations::Body::Case { overrides, .. } => { declarations::Body::Case { overrides, .. } => {
for entry in &overrides.value { for entry in &overrides.value {
let target = let target = *globals.get(entry.target.name.as_str()).ok_or_else(|| {
*globals.get(entry.target.name.as_str()).ok_or_else(|| {
error(entry.target.span, "Unknown syntax override target.") error(entry.target.span, "Unknown syntax override target.")
})?; })?;
root(unit.file, owner, Field::Override(target), &entry.binding)?; root(file, owner, Field::Override(target), &entry.binding)?;
} }
} }
_ => {} _ => {}
} }
} }
}
let mut visited = 0usize; let mut visited = 0usize;
while let Some((file, owner, field, expression, parent)) = work.pop() { while let Some((file, owner, field, expression, parent)) = work.pop() {
storage.release(1);
sources.at.set(Some(file));
if storage.0.is_none() {
budget.lowering_entries(visited + 1, expression.span)?; budget.lowering_entries(visited + 1, expression.span)?;
}
visited += 1; visited += 1;
let site = *sites let site = *sites
.get(&( .get(&(
@@ -566,7 +779,7 @@ impl<'s> Index<'s> {
"Parsed expression is absent from the source arena.", "Parsed expression is absent from the source arena.",
) )
})?; })?;
if lowered.expressions[site].parent != parent { if expressions[site].parent != parent {
return Err(error( return Err(error(
expression.span, expression.span,
"Arena parent/child edge differs from original syntax.", "Arena parent/child edge differs from original syntax.",
@@ -574,6 +787,7 @@ impl<'s> Index<'s> {
} }
let mut child = |value: &'a super::Expression, edge| -> Result<(), Error> { let mut child = |value: &'a super::Expression, edge| -> Result<(), Error> {
budget.lowering_entries(work.len() + 1, value.span)?; budget.lowering_entries(work.len() + 1, value.span)?;
storage.reserve(budget, 1, 0, value.span)?;
work.push((file, owner, field, value, Some((site, edge)))); work.push((file, owner, field, value, Some((site, edge))));
Ok(()) Ok(())
}; };
@@ -623,7 +837,14 @@ impl<'s> Index<'s> {
_ => {} _ => {}
} }
} }
if visited != lowered.expressions.len() { sources.at.set(None);
let scratch = globals.len() + parameters.len() + sites.len();
drop(globals);
drop(parameters);
drop(sites);
drop(work);
storage.release(scratch);
if visited != expressions.len() {
return Err(error( return Err(error(
Span::default(), Span::default(),
"Arena contains expressions absent from parsed syntax.", "Arena contains expressions absent from parsed syntax.",
@@ -636,20 +857,24 @@ impl<'s> Index<'s> {
&mut self, &mut self,
key: OccurrenceKey, key: OccurrenceKey,
location: Location, location: Location,
witnesses: &BTreeMap<usize, Vec<lex::Token>>, sources: &Original<'_>,
physical: &mut BTreeSet<(usize, usize, usize)>, physical: &mut BTreeSet<(usize, usize, usize)>,
budget: &Budget, budget: &Budget,
storage: &mut Storage<'_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
if storage.0.is_none() {
budget.lowering_occurrences(self.occurrences.len() + 1, location.span)?; budget.lowering_occurrences(self.occurrences.len() + 1, location.span)?;
let text = checked_source(self.sources, location)?; }
storage.reserve(budget, 2, 0, location.span)?;
let text = sources.checked(location)?;
if self.names.get(&key.target).is_none_or(|name| name != text) { if self.names.get(&key.target).is_none_or(|name| name != text) {
return Err(error( return Err(error(
location.span, location.span,
"Token differs from its qualified target name.", "Token differs from its qualified target name.",
)); ));
} }
let tokens = witnesses let tokens = sources
.get(&location.file) .tokens(location.file)
.ok_or_else(|| error(location.span, "Missing lexical witness."))?; .ok_or_else(|| error(location.span, "Missing lexical witness."))?;
let token = tokens let token = tokens
.binary_search_by_key(&location.span.start, |token| token.span.start) .binary_search_by_key(&location.span.start, |token| token.span.start)
@@ -1,5 +1,5 @@
//! Private complete-Project admission from the actual complete syntax witness. //! Private complete-Project admission from the actual complete syntax witness.
//! No Index, update authority, public source8 dispatch or replacement policy. //! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy.
use super::super::{ use super::super::{
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span, Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
declarations as d, error, lower, declarations as d, error, lower,
@@ -9,7 +9,7 @@ use crate::language::{Ast, DeclKind};
use crate::source_project::{SourceDiagnostic, SourceLocation}; use crate::source_project::{SourceDiagnostic, SourceLocation};
use crate::{Id, Kind, Project, RelationKind, analysis, language, validation}; use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
use std::collections::BTreeMap; use std::collections::BTreeMap;
mod accounting; pub(in crate::language::analysis_source) mod accounting;
use accounting::{Ledger, Slots, add}; use accounting::{Ledger, Slots, add};
/// Read-only authority: every component originates in the consumed syntax. /// Read-only authority: every component originates in the consumed syntax.
@@ -32,6 +32,105 @@ impl ResolvedMixedProject<'_> {
} }
} }
/// Complete original bundle authority with a typed-only occurrence view.
/// Ordinary globals are catalogued, but their authored occurrences are not indexed.
/// This owner cannot be converted into the legacy typed-only edit authority.
pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> {
resolved: ResolvedMixedProject<'s>,
data: super::super::index::IndexData,
}
impl<'s> ResolvedMixedProject<'s> {
pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> {
let input = IndexInput {
files: &self.syntax.files,
project: &self.project,
expressions: &self.expressions,
occurrences: &self.occurrences,
};
let data =
super::super::index::mixed_data(input, &mut self.syntax.budget, &mut self.storage)
.map_err(|mut failure| {
// An invariant without a canonical file has no defensible source
// offset. Use the entry start explicitly, never a foreign span.
failure.file = failure.file.filter(|&f| f < self.syntax.files.len());
if failure.file.is_none_or(|file| {
self.syntax.files[file]
.source
.get(failure.error.span.start..failure.error.span.end)
.is_none()
}) {
failure.error.span = Span::default();
}
Failure::typed(&self.syntax, failure.file, failure.error)
})?;
Ok(IndexedMixedProject {
resolved: self,
data,
})
}
}
impl IndexedMixedProject<'_> {
pub fn project(&self) -> &Project {
self.resolved.project()
}
pub fn warnings(&self) -> &[SourceDiagnostic] {
self.resolved.warnings()
}
pub fn bundle(&self) -> &crate::source_project::SourceBundle {
self.resolved.syntax.bundle
}
/// Canonical file key, canonical path, and the complete original bytes.
pub fn files(&self) -> impl Iterator<Item = (usize, &str, &str)> {
self.resolved
.syntax
.files
.iter()
.enumerate()
.map(|(key, f)| (key, f.path, f.source))
}
pub fn names(&self) -> &BTreeMap<lower::Target, String> {
&self.data.names
}
pub fn occurrences(
&self,
) -> &BTreeMap<super::super::index::OccurrenceKey, super::super::index::Location> {
&self.data.occurrences
}
pub fn expressions(
&self,
) -> &BTreeMap<super::super::index::ExpressionKey, super::super::index::Location> {
&self.data.expressions
}
}
/// Constructor-only borrowed adapter. Its private file table prevents fabrication;
/// no API returns it and no alternative source/token table can be installed.
pub(in crate::language::analysis_source) struct IndexInput<'a> {
files: &'a [FileSyntax<'a>],
pub(in crate::language::analysis_source) project: &'a Project,
pub(in crate::language::analysis_source) expressions: &'a [lower::ExpressionSite],
pub(in crate::language::analysis_source) occurrences: &'a [lower::Occurrence],
}
impl IndexInput<'_> {
pub(in crate::language::analysis_source) fn declarations(
&self,
) -> impl Iterator<Item = (usize, &d::Declaration)> + Clone {
self.files
.iter()
.enumerate()
.flat_map(|(file, f)| f.typed.iter().map(move |d| (file, d)))
}
pub(in crate::language::analysis_source) fn source(&self, file: usize) -> Option<&str> {
self.files.get(file).map(|f| f.source)
}
pub(in crate::language::analysis_source) fn tokens(
&self,
file: usize,
) -> Option<&[super::super::lex::Token]> {
self.files.get(file).map(|f| f.tokens.as_slice())
}
}
#[derive(Debug)] #[derive(Debug)]
pub(in crate::language::analysis_source) struct Failure { pub(in crate::language::analysis_source) struct Failure {
pub diagnostics: Vec<SourceDiagnostic>, pub diagnostics: Vec<SourceDiagnostic>,
@@ -3,7 +3,7 @@ use super::*;
use crate::language::{EditKind, EditValue}; use crate::language::{EditKind, EditValue};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct Slots { pub(in crate::language::analysis_source) struct Slots {
pub entries: usize, pub entries: usize,
pub occurrences: usize, pub occurrences: usize,
pub text: usize, pub text: usize,
@@ -39,7 +39,7 @@ pub(super) fn mul(a: usize, b: usize, at: Span) -> Result<usize, super::super::s
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(super) struct Ledger { pub(in crate::language::analysis_source) struct Ledger {
pub live: Slots, pub live: Slots,
} }
impl Ledger { impl Ledger {
@@ -250,9 +250,8 @@ fn complete_oa_sa_forward_model_warning_and_original_authority() {
} }
} }
#[test] fn rich_bundle(layer: Layer) -> SourceBundle {
fn complete_global_arena_scoped_calls_and_coincident_file_offsets() { let mut bundle = bundle(layer);
let mut bundle = bundle(Layer::Oa);
let main = bundle.files.get_mut("main.arcadia").unwrap(); let main = bundle.files.get_mut("main.arcadia").unwrap();
*main = main *main = main
.replace( .replace(
@@ -269,8 +268,10 @@ fn complete_global_arena_scoped_calls_and_coincident_file_offsets() {
] { ] {
bundle.files.insert(file.into(), format!("calculation {alias} \"{name}\" id \"{}\" {{\ninput x id \"{}\" : Number;\ninput y id \"{}\" : Number;\nresult Number; body $x {op} $y;\n}}\n", id(n), id(1), id(12))); bundle.files.insert(file.into(), format!("calculation {alias} \"{name}\" id \"{}\" {{\ninput x id \"{}\" : Number;\ninput y id \"{}\" : Number;\nresult Number; body $x {op} $y;\n}}\n", id(n), id(1), id(12)));
} }
let resolved = accepted(&bundle); bundle
let mut model = expected(Layer::Oa); }
fn rich_model(layer: Layer) -> Project {
let mut model = expected(layer);
for (alias, name, n, op) in [ for (alias, name, n, op) in [
("f", "F", 10, a::BinaryOp::Add), ("f", "F", 10, a::BinaryOp::Add),
("g", "G", 11, a::BinaryOp::Subtract), ("g", "G", 11, a::BinaryOp::Subtract),
@@ -315,6 +316,14 @@ fn complete_global_arena_scoped_calls_and_coincident_file_offsets() {
call(10, call(11, number(3), number(4)), number(2)), call(10, call(11, number(3), number(4)), number(2)),
call(10, number(5), number(6)), call(10, number(5), number(6)),
)); ));
model
}
#[test]
fn complete_global_arena_scoped_calls_and_coincident_file_offsets() {
let bundle = rich_bundle(Layer::Oa);
let resolved = accepted(&bundle);
let model = rich_model(Layer::Oa);
assert_eq!(resolved.project(), &model); assert_eq!(resolved.project(), &model);
assert_eq!( assert_eq!(
resolved resolved
@@ -1021,3 +1030,5 @@ fn incomplete_call_borrows_large_signature_and_refuses_at_single_admission() {
assert_eq!(failure.diagnostics[0].diagnostic.code, "analysis_arguments"); assert_eq!(failure.diagnostics[0].diagnostic.code, "analysis_arguments");
assert_eq!(failure.diagnostics[0].diagnostic.object_id, Some(id(11))); assert_eq!(failure.diagnostics[0].diagnostic.object_id, Some(id(11)));
} }
mod index_tests;
@@ -0,0 +1,479 @@
use super::*;
use crate::language::analysis_source::index::{Location, TokenRole};
use lower::{Child as C, Field as F, Role as R, Target as T};
fn parameter(n: usize) -> a::ParameterId {
a::ParameterId(id(n))
}
fn q(c: usize, p: usize) -> T {
T::Parameter {
calculation: id(c),
parameter: parameter(p),
}
}
fn arg(n: usize) -> C {
C::Argument(parameter(n))
}
fn mapped(original: &str, byte: usize, transformed: bool) -> usize {
if transformed {
8 + byte
+ original.as_bytes()[..byte]
.iter()
.filter(|&&b| b == b'\n')
.count()
} else {
byte
}
}
#[test]
fn entire_authored_index_oa_sa_original_and_unicode_crlf() {
for layer in [Layer::Oa, Layer::Sa] {
for transformed in [false, true] {
let original = rich_bundle(layer);
let mut bundle = original.clone();
if transformed {
for source in bundle.files.values_mut() {
*source = format!("// 雨\r\n{}", source.replace('\n', "\r\n"));
}
}
let resolved = accepted(&bundle);
let before = resolved.storage.live;
let usage = resolved.syntax.budget.usage;
let numeric = resolved.syntax.budget.numeric_units();
let warnings = resolved.warnings.clone();
let static_limits = resolved.static_limits;
let static_usage = resolved.static_usage;
let indexed = resolved.index().unwrap();
assert_eq!(indexed.project(), &rich_model(layer));
assert_eq!(indexed.warnings(), warnings);
assert!(std::ptr::eq(indexed.bundle(), &bundle));
assert_eq!(indexed.resolved.syntax.budget.usage, usage);
assert_eq!(indexed.resolved.syntax.budget.numeric_units(), numeric);
assert_eq!(indexed.resolved.static_limits, static_limits);
assert_eq!(indexed.resolved.static_usage, static_usage);
assert_eq!(
indexed.files().collect::<Vec<_>>(),
bundle
.files
.iter()
.enumerate()
.map(|(i, (p, s))| (i, p.as_str(), s.as_str()))
.collect::<Vec<_>>()
);
let mut names: BTreeMap<_, String> = [
(1, "p"),
(2, "w"),
(3, "v"),
(4, "n"),
(5, "d"),
(10, "f"),
(11, "g"),
]
.into_iter()
.map(|(n, s)| (T::Global(id(n)), s.into()))
.collect();
for c in [10, 11] {
for (p, s) in [(1, "x"), (12, "y")] {
names.insert(q(c, p), s.into());
}
}
if layer == Layer::Sa {
names.insert(T::Global(id(6)), "s".into());
}
assert_eq!(indexed.names(), &names);
assert_eq!(
indexed.resolved.storage.live,
before
.add(
Slots {
entries: if layer == Layer::Sa { 60 } else { 59 },
occurrences: 0,
text: names.len()
},
Span::default()
)
.unwrap()
);
let at = |file: usize, start, end| {
let source = original.files.values().nth(file).unwrap();
Location {
file,
span: Span {
start: mapped(source, start, transformed),
end: mapped(source, end, transformed),
},
}
};
let mut expressions = BTreeMap::new();
for (file, owner) in [(0, 10), (1, 11)] {
for (path, start, end) in [
(vec![], 202, 209),
(vec![C::Left], 202, 204),
(vec![C::Right], 207, 209),
] {
expressions.insert((id(owner), F::Body, path), at(file, start, end));
}
}
for (path, start, end) in [
(vec![], 290, 334),
(vec![C::Left], 290, 317),
(vec![C::Left, arg(12)], 296, 297),
(vec![C::Left, arg(1)], 302, 316),
(vec![C::Left, arg(1), arg(1)], 308, 309),
(vec![C::Left, arg(1), arg(12)], 314, 315),
(vec![C::Right], 320, 334),
(vec![C::Right, arg(1)], 326, 327),
(vec![C::Right, arg(12)], 332, 333),
] {
expressions.insert((id(3), F::Base, path), at(2, start, end));
}
assert_eq!(
indexed
.expressions()
.iter()
.map(|(k, v)| ((k.owner, k.field, k.path.edges()), *v))
.collect::<BTreeMap<_, _>>(),
expressions
);
let mut occurrences = BTreeMap::new();
for (file, owner, start) in [(0, 10, 12), (1, 11, 12), (2, 3, 206)] {
occurrences.insert(
(
id(owner),
None,
None,
TokenRole::Declaration,
T::Global(id(owner)),
),
at(file, start, start + 1),
);
}
for (file, owner) in [(0, 10), (1, 11)] {
for (p, start) in [(1, 68), (12, 128)] {
occurrences.insert(
(
id(owner),
Some(F::Input(parameter(p))),
None,
TokenRole::Use(R::ParameterDeclaration),
q(owner, p),
),
at(file, start, start + 1),
);
}
for (p, edge, start) in [(1, C::Left, 203), (12, C::Right, 208)] {
occurrences.insert(
(
id(owner),
Some(F::Body),
Some(vec![edge]),
TokenRole::Use(R::ParameterUse),
q(owner, p),
),
at(file, start, start + 1),
);
}
}
occurrences.insert(
(
id(3),
Some(F::Subject),
None,
TokenRole::Use(R::Reference),
T::Global(id(2)),
),
at(2, 269, 270),
);
for (path, role, target, start) in [
(vec![C::Left], R::CallTarget, T::Global(id(10)), 291),
(vec![C::Left], R::ArgumentLabel, q(10, 12), 293),
(vec![C::Left], R::ArgumentLabel, q(10, 1), 299),
(vec![C::Left, arg(1)], R::CallTarget, T::Global(id(11)), 303),
(vec![C::Left, arg(1)], R::ArgumentLabel, q(11, 1), 305),
(vec![C::Left, arg(1)], R::ArgumentLabel, q(11, 12), 311),
(vec![C::Right], R::CallTarget, T::Global(id(10)), 321),
(vec![C::Right], R::ArgumentLabel, q(10, 1), 323),
(vec![C::Right], R::ArgumentLabel, q(10, 12), 329),
] {
occurrences.insert(
(
id(3),
Some(F::Base),
Some(path),
TokenRole::Use(role),
target,
),
at(2, start, start + 1),
);
}
assert_eq!(
indexed
.occurrences()
.iter()
.map(|(k, v)| (
(
k.owner,
k.field,
k.path.as_ref().map(|p| p.edges()),
k.role,
k.target
),
*v
))
.collect::<BTreeMap<_, _>>(),
occurrences
);
for (key, at) in indexed.occurrences() {
let source = indexed.files().nth(at.file).unwrap().2;
assert_eq!(&source[at.span.start..at.span.end], names[&key.target]);
}
}
}
}
fn reject(value: ResolvedMixedProject<'_>, message: &str, file: Option<&str>) -> Failure {
let failure = match value.index() {
Ok(_) => panic!("corrupt index accepted: {message}"),
Err(e) => e,
};
assert_eq!(failure.diagnostics[0].diagnostic.message, message);
if let Some(file) = file {
assert_eq!(failure.diagnostics[0].path, file);
}
failure
}
#[test]
fn damage_reaches_structural_token_and_call_checks() {
let bundle = rich_bundle(Layer::Oa);
for case in 0..13 {
let mut value = accepted(&bundle);
let message = match case {
0 => {
value.expressions[1].file = 1;
"Source span crosses its containing syntax."
}
1 => {
value.expressions[1].owner = id(1);
"Unknown expression owner."
}
2 => {
value.expressions[1].field = F::Predicate;
"Expression parent crosses its owner, field or file."
}
3 => {
value.expressions[1].parent = Some((1, C::Left));
"Expression parent must precede its child."
}
4 => {
value.expressions[1].parent = Some((2, C::Left));
"Expression parent must precede its child."
}
5 => {
value.expressions[1].parent = Some((0, C::Operand));
"Arena parent/child edge differs from original syntax."
}
6 => {
value.occurrences[11].target = q(11, 1);
"Argument label crosses its called calculation."
}
7 => {
value.occurrences.remove(11);
"Argument child has no qualified label."
}
8 => {
value.expressions.pop();
"Parsed expression is absent from the source arena."
}
9 => {
value.occurrences[1].span = value.occurrences[0].span;
value.occurrences[1].target = q(10, 1);
value.occurrences[1].field = F::Input(parameter(1));
"Duplicate qualified occurrence or token ownership."
}
10 => {
value.occurrences[0].span = Span { start: 69, end: 70 };
"Token differs from its qualified target name."
}
11 => {
let token = value.syntax.files[0]
.tokens
.iter_mut()
.find(|t| t.span.start == 68)
.unwrap();
token.class = super::super::super::super::lex::Class::String;
"Occurrence is not an original whole name token."
}
_ => {
value.expressions.push(lower::ExpressionSite {
file: 0,
owner: id(10),
field: F::Base,
parent: None,
span: Span {
start: 202,
end: 204,
},
});
"Arena contains expressions absent from parsed syntax."
}
};
let file = if matches!(case, 6 | 7 | 8) {
Some("main.arcadia")
} else if case == 0 {
Some("b.arcadia")
} else if case == 12 {
None
} else {
Some("a.arcadia")
};
reject(value, message, file);
}
}
#[test]
fn new_maps_keep_baseline_and_scope_reservations_and_peak_scratch() {
let bundle = rich_bundle(Layer::Oa);
let baseline = accepted(&bundle).storage.live;
// Final new maps59; at the last occurrence, owners3+physical21+paths15
// coexist. Subsequent call peak77 and syntax peak90 are smaller than98.
for (extra, fits) in [(97, false), (98, true)] {
let profile = super::super::super::super::budget::Limits {
entries: baseline.entries + extra,
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
};
let resolved =
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
.unwrap();
assert_eq!(resolved.storage.live, baseline);
if fits {
let indexed = resolved.index().unwrap();
assert_eq!(indexed.resolved.storage.live.entries, baseline.entries + 59);
assert_eq!(
indexed.resolved.storage.live.occurrences,
baseline.occurrences
);
} else {
let failure = reject(
resolved,
"Source8 lowering entry limit exceeded.",
Some("main.arcadia"),
);
assert_eq!(
failure.diagnostics[0].diagnostic.span,
Span {
start: 329,
end: 330
}
);
}
}
}
#[test]
fn copied_name_bytes_coexist_with_retained_warning_bytes() {
let mut bundle = single("");
let source = bundle
.files
.remove("main.arcadia")
.unwrap()
.replace("\"Project\"", "\"\"");
bundle.entry = format!("{}.arcadia", "a".repeat(180));
bundle.files.insert(bundle.entry.clone(), source);
for (bytes, fits) in [(236, false), (237, true)] {
let profile = super::super::super::super::budget::Limits {
bytes,
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
};
let resolved =
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
.unwrap();
assert_eq!(resolved.storage.live.text, 236);
if fits {
assert_eq!(resolved.index().unwrap().resolved.storage.live.text, 237);
} else {
reject(resolved, "Source8 index/output byte limit exceeded.", None);
}
}
}
#[test]
fn success_and_late_failure_drop_on_small_stack() {
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn(|| {
let bundle = rich_bundle(Layer::Oa);
drop(accepted(&bundle).index().unwrap());
let deep = single(&format!(
"calculation c \"C\" id \"{}\" {{ result Number; body {}1{}; }}",
id(2),
"abs(".repeat(127),
")".repeat(127)
));
drop(accepted(&deep).index().unwrap());
let mut damaged = accepted(&deep);
damaged.expressions[127].parent = Some((126, C::Left));
reject(
damaged,
"Arena parent/child edge differs from original syntax.",
Some("main.arcadia"),
);
let mut value = accepted(&bundle);
value.expressions[1].parent = Some((0, C::Operand));
reject(
value,
"Arena parent/child edge differs from original syntax.",
Some("a.arcadia"),
);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn invalid_file_and_utf8_span_use_explicit_safe_fallback() {
let mut bundle = rich_bundle(Layer::Oa);
*bundle.files.get_mut("a.arcadia").unwrap() =
bundle.files["a.arcadia"].replace("\"F\"", "\"雨\"");
let mut value = accepted(&bundle);
value.expressions[0].file = usize::MAX;
let failure = reject(
value,
"Source span is outside its original UTF-8 file.",
Some("main.arcadia"),
);
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
let mut value = accepted(&bundle);
let byte = bundle.files["a.arcadia"].find('雨').unwrap() + 1;
value.occurrences[0].span = Span {
start: byte,
end: byte + 1,
};
let failure = reject(
value,
"Source span is outside its original UTF-8 file.",
Some("a.arcadia"),
);
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
}
#[test]
fn distinct_qualified_keys_cannot_share_one_physical_token() {
let bundle = rich_bundle(Layer::Oa);
let mut value = accepted(&bundle);
let original = &value.occurrences[2];
value.occurrences.push(lower::Occurrence {
file: original.file,
owner: original.owner,
field: original.field,
expression: original.expression,
role: R::ArgumentLabel,
target: q(11, 1),
span: original.span,
});
reject(
value,
"Duplicate qualified occurrence or token ownership.",
Some("a.arcadia"),
);
}
@@ -0,0 +1,51 @@
# Private mixed typed occurrence index
Source checkpoint on `codex/analysis-mixed-index-956atf-20260924`, based on
`aaec78fcc9b273d1485497126392afdd1fb2e54e`. This records implemented structure;
compilation and runtime acceptance have not yet occurred at this checkpoint.
The accepted contract/review are published at
`7cfdd13d3a1472e63fd34b0080b2cfc4b921b23e:harness/analysis-mixed-index-contract-20260924.md`.
Independent exhaustive fixture data and its raw-byte checker are at
`ae6df4fa1f103ff9b14963731b2edd84e38738b1:harness/analysis-mixed-index-20260924/`.
`ResolvedMixedProject::index` consumes the sealed complete-Project witness.
`IndexedMixedProject` owns it and shared `IndexData`; read-only methods expose
the Project, warnings, original bundle/canonical files, names and typed maps.
It has no conversion to the legacy numeric-source Index or its updater. The
constructor-only adapter keeps its file table private, so shared verification
does not accept caller-fabricated mixed source/token ownership.
Shared verification preserves original token, owner, structural child-path,
qualified call-label and physical occurrence checks. Mixed catalog construction
streams complete Project globals and scoped parameters once. It does not
reparse, relower, duplicate model records or repeat admission. Ordinary globals
are names/targets here; ordinary source occurrences are not indexed by this view.
Complete non-expression occurrence emission remains the sealed lowerer's
invariant, checked by an independent exhaustive output oracle.
Mixed allocations reserve against the retained aggregate ledger before growth.
New map/vector/set/Path entries charge entry slots; retained raw lowerer
occurrences keep occurrence slots. New names charge copied UTF-8 bytes. Owner
and physical-token scratch drop after occurrence checks; path/call scratch drop
before syntax-edge traversal. Shared Path nodes remain retained while map keys
own them. Legacy typed-only construction retains its original local policy.
Precompile independent review found stale diagnostic file attribution in two
late verification loops and unsafe fallback for invalid file/UTF-8 spans. The
source now updates attribution for each checked record and maps invalid
locations explicitly to a safe start span, retaining a valid file when known.
Regression cases are authored; no passing runtime result is implied.
Seven new tests cover complete OA/SA names/maps/model and Unicode/CRLF variants,
qualified corruption cases, inherited-plus-new storage and copied-name byte
boundaries, original provenance/admission metadata, and small-stack success and
late rejection. Existing rich fixture/model construction is factored without
removing its original full arena test. Actual test inventory, strict core lint,
focused checks, full private-library dispositions and formatting still require
current-source execution. Prior private source8 failures are not suppressed.
The working agreement is synchronized to current main blob
`e76647f49913b314e1543c4464f6199533909bae`. This source remains separate from the
Windows prerelease. Public source8, ordinary occurrence composition, source
updates/output admission, migration, checked save/history, GUI and API parity
remain required subsequent work.