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,55 +237,242 @@ impl<'s> Index<'s> {
for element in lowered.elements.values() {
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,
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,
occurrences: BTreeMap::new(),
expressions: BTreeMap::new(),
};
let mut owners = BTreeMap::new();
let mut physical = BTreeSet::new();
for unit in units {
for declaration in &unit.declarations {
let owner = declaration.header.identity.value.ok_or_else(|| {
error(
declaration.span,
"Missing ID requires source stabilization.",
)
})?;
let location = Location {
file: unit.file,
span: declaration.span,
};
checked_source(sources, location)?;
budget.lowering_entries(owners.len() + 1, declaration.span)?;
if owners.insert(owner, location).is_some() {
return Err(error(declaration.span, "Duplicate source owner."));
}
let key = OccurrenceKey {
owner,
field: None,
path: None,
role: TokenRole::Declaration,
target: Target::Global(owner),
};
index.insert(
key,
Location {
file: unit.file,
span: declaration.header.alias.span,
},
witnesses,
&mut physical,
budget,
)?;
for (file, declaration) in declarations.clone() {
sources.at.set(Some(file));
let owner = declaration.header.identity.value.ok_or_else(|| {
error(
declaration.span,
"Missing ID requires source stabilization.",
)
})?;
let location = Location {
file,
span: declaration.span,
};
sources.checked(location)?;
budget.lowering_entries(owners.len() + 1, declaration.span)?;
storage.reserve(budget, 1, 0, declaration.span)?;
if owners.insert(owner, location).is_some() {
return Err(error(declaration.span, "Duplicate source owner."));
}
let key = OccurrenceKey {
owner,
field: None,
path: None,
role: TokenRole::Declaration,
target: Target::Global(owner),
};
index.insert(
key,
Location {
file,
span: declaration.header.alias.span,
},
sources,
&mut physical,
budget,
storage,
)?;
}
// Parent-before-child is part of the lowering contract. One forward
// pass rejects forward, cyclic, cross-file/owner/field and duplicate
// paths. Each node allocates one shared edge, not a copied path vector.
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!(
site.field,
Field::Body | Field::Base | Field::Predicate | Field::Override(_)
@@ -299,7 +486,7 @@ impl<'s> Index<'s> {
file: site.file,
span: site.span,
};
checked_source(sources, location)?;
sources.checked(location)?;
let owner = owners
.get(&site.owner)
.ok_or_else(|| error(site.span, "Unknown expression owner."))?;
@@ -311,7 +498,7 @@ impl<'s> Index<'s> {
"Expression parent must precede its child.",
));
}
let ancestor = &lowered.expressions[parent];
let ancestor = &expressions[parent];
if ancestor.owner != site.owner
|| ancestor.field != site.field
|| ancestor.file != site.file
@@ -336,6 +523,7 @@ impl<'s> Index<'s> {
}
let depth = paths[parent].1 + 1;
budget.frame(depth, site.span)?;
storage.reserve(budget, 1, 0, site.span)?;
(
Path(Some(Arc::new(Step {
parent: paths[parent].0.path.clone(),
@@ -352,12 +540,14 @@ impl<'s> Index<'s> {
field: site.field,
path,
};
storage.reserve(budget, 2, 0, site.span)?;
if index.expressions.insert(key.clone(), location).is_some() {
return Err(error(site.span, "Duplicate qualified expression path."));
}
paths.push((key, depth));
}
for occurrence in &lowered.occurrences {
for occurrence in occurrences {
sources.at.set(Some(occurrence.file));
let location = Location {
file: occurrence.file,
span: occurrence.span,
@@ -368,8 +558,7 @@ impl<'s> Index<'s> {
contained(*owner, location)?;
let path = match occurrence.expression {
Some(site) => {
let expression = lowered
.expressions
let expression = expressions
.get(site)
.ok_or_else(|| error(occurrence.span, "Unknown occurrence expression."))?;
if expression.owner != occurrence.owner
@@ -420,20 +609,28 @@ impl<'s> Index<'s> {
role: TokenRole::Use(occurrence.role),
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
// merely to a parameter UUID/name present somewhere in the catalog.
let mut calls = BTreeMap::new();
for (key, at) in &index.occurrences {
sources.at.set(Some(at.file));
if key.role == TokenRole::Use(Role::CallTarget) {
let site = (key.owner, key.field, key.path.clone());
storage.reserve(budget, 1, 0, at.span)?;
if calls.insert(site, key.target).is_some() {
return Err(error(at.span, "Multiple callees at one expression site."));
}
}
}
for (key, at) in &index.occurrences {
sources.at.set(Some(at.file));
if let (TokenRole::Use(Role::ArgumentLabel), Target::Parameter { calculation, .. }) =
(key.role, key.target)
&& 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 {
let key = &paths[parent].0;
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)
}
@@ -477,12 +680,16 @@ impl<'s> Index<'s> {
/// with indexed span lookup; it does not lex or copy source/path strings.
fn check_syntax_edges<'a>(
&self,
units: &'a [Unit],
lowered: &Lowered,
declarations: impl Iterator<Item = (usize, &'a declarations::Declaration)> + Clone,
expressions: &[lower::ExpressionSite],
sources: &Original<'_>,
budget: &Budget,
storage: &mut Storage<'_>,
) -> Result<(), Error> {
let mut globals = 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 {
match target {
Target::Global(id) => {
@@ -497,7 +704,9 @@ impl<'s> Index<'s> {
}
}
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
.insert(
(
@@ -518,39 +727,43 @@ impl<'s> Index<'s> {
let mut root = |file, owner, field, binding: &'a super::Binding| -> Result<(), Error> {
if let super::Binding::Expression(expression) = binding {
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));
}
Ok(())
};
for unit in units {
for declaration in &unit.declarations {
let owner = declaration.header.identity.value.expect("validated owner");
match &declaration.body {
declarations::Body::Calculation { body, .. } => {
root(unit.file, owner, Field::Body, &body.value)?
}
declarations::Body::Property { base, .. } => {
root(unit.file, owner, Field::Base, &base.value)?
}
declarations::Body::Check { predicate, .. } => {
root(unit.file, owner, Field::Predicate, &predicate.value)?
}
declarations::Body::Case { overrides, .. } => {
for entry in &overrides.value {
let target =
*globals.get(entry.target.name.as_str()).ok_or_else(|| {
error(entry.target.span, "Unknown syntax override target.")
})?;
root(unit.file, owner, Field::Override(target), &entry.binding)?;
}
}
_ => {}
for (file, declaration) in declarations.clone() {
sources.at.set(Some(file));
let owner = declaration.header.identity.value.expect("validated owner");
match &declaration.body {
declarations::Body::Calculation { body, .. } => {
root(file, owner, Field::Body, &body.value)?
}
declarations::Body::Property { base, .. } => {
root(file, owner, Field::Base, &base.value)?
}
declarations::Body::Check { predicate, .. } => {
root(file, owner, Field::Predicate, &predicate.value)?
}
declarations::Body::Case { overrides, .. } => {
for entry in &overrides.value {
let target = *globals.get(entry.target.name.as_str()).ok_or_else(|| {
error(entry.target.span, "Unknown syntax override target.")
})?;
root(file, owner, Field::Override(target), &entry.binding)?;
}
}
_ => {}
}
}
let mut visited = 0usize;
while let Some((file, owner, field, expression, parent)) = work.pop() {
budget.lowering_entries(visited + 1, expression.span)?;
storage.release(1);
sources.at.set(Some(file));
if storage.0.is_none() {
budget.lowering_entries(visited + 1, expression.span)?;
}
visited += 1;
let site = *sites
.get(&(
@@ -566,7 +779,7 @@ impl<'s> Index<'s> {
"Parsed expression is absent from the source arena.",
)
})?;
if lowered.expressions[site].parent != parent {
if expressions[site].parent != parent {
return Err(error(
expression.span,
"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> {
budget.lowering_entries(work.len() + 1, value.span)?;
storage.reserve(budget, 1, 0, value.span)?;
work.push((file, owner, field, value, Some((site, edge))));
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(
Span::default(),
"Arena contains expressions absent from parsed syntax.",
@@ -636,20 +857,24 @@ impl<'s> Index<'s> {
&mut self,
key: OccurrenceKey,
location: Location,
witnesses: &BTreeMap<usize, Vec<lex::Token>>,
sources: &Original<'_>,
physical: &mut BTreeSet<(usize, usize, usize)>,
budget: &Budget,
storage: &mut Storage<'_>,
) -> Result<(), Error> {
budget.lowering_occurrences(self.occurrences.len() + 1, location.span)?;
let text = checked_source(self.sources, location)?;
if storage.0.is_none() {
budget.lowering_occurrences(self.occurrences.len() + 1, location.span)?;
}
storage.reserve(budget, 2, 0, location.span)?;
let text = sources.checked(location)?;
if self.names.get(&key.target).is_none_or(|name| name != text) {
return Err(error(
location.span,
"Token differs from its qualified target name.",
));
}
let tokens = witnesses
.get(&location.file)
let tokens = sources
.tokens(location.file)
.ok_or_else(|| error(location.span, "Missing lexical witness."))?;
let token = tokens
.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.
//! 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::{
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
declarations as d, error, lower,
@@ -9,7 +9,7 @@ use crate::language::{Ast, DeclKind};
use crate::source_project::{SourceDiagnostic, SourceLocation};
use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
use std::collections::BTreeMap;
mod accounting;
pub(in crate::language::analysis_source) mod accounting;
use accounting::{Ledger, Slots, add};
/// 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)]
pub(in crate::language::analysis_source) struct Failure {
pub diagnostics: Vec<SourceDiagnostic>,
@@ -3,7 +3,7 @@ use super::*;
use crate::language::{EditKind, EditValue};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct Slots {
pub(in crate::language::analysis_source) struct Slots {
pub entries: usize,
pub occurrences: 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)]
pub(super) struct Ledger {
pub(in crate::language::analysis_source) struct Ledger {
pub live: Slots,
}
impl Ledger {
@@ -250,9 +250,8 @@ fn complete_oa_sa_forward_model_warning_and_original_authority() {
}
}
#[test]
fn complete_global_arena_scoped_calls_and_coincident_file_offsets() {
let mut bundle = bundle(Layer::Oa);
fn rich_bundle(layer: Layer) -> SourceBundle {
let mut bundle = bundle(layer);
let main = bundle.files.get_mut("main.arcadia").unwrap();
*main = main
.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)));
}
let resolved = accepted(&bundle);
let mut model = expected(Layer::Oa);
bundle
}
fn rich_model(layer: Layer) -> Project {
let mut model = expected(layer);
for (alias, name, n, op) in [
("f", "F", 10, a::BinaryOp::Add),
("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, 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
@@ -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.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"),
);
}