Index admitted mixed typed source without losing bundle authority
This commit is contained in:
@@ -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,
|
||||||
|
},
|
||||||
|
¶meter.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.
|
||||||
Reference in New Issue
Block a user