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