Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab6f96a5b | ||
|
|
8555e417ee | ||
|
|
ddf823a3d3 | ||
|
|
042e25efc2 | ||
|
|
7b06b8ea1b | ||
|
|
0c07f53ab5 | ||
|
|
520e50ce62 | ||
|
|
33b86a4d6b | ||
|
|
edf096cd33 | ||
|
|
4872141c03 | ||
|
|
29417130c7 | ||
|
|
a629ae01d4 | ||
|
|
97a8703adc | ||
|
|
6b0ef99eca | ||
|
|
8d52dab97c | ||
|
|
19a4082117 | ||
|
|
fa7a980418 | ||
|
|
d11b99bb4a | ||
|
|
18073a6f2e | ||
|
|
f1c095654a | ||
|
|
70c20c39de | ||
|
|
8159a17dd7 | ||
|
|
9e8e77e1d8 | ||
|
|
9388fcf2f0 |
+155
-47
@@ -3,10 +3,9 @@ mod raw_tokens;
|
||||
#[path = "language/semantic_source.rs"]
|
||||
mod semantic_source;
|
||||
mod source3_bridge;
|
||||
// Staged syntax foundation only. Public source8 admission waits for complete
|
||||
// declaration lowering, source updates and the root-owned resource gate.
|
||||
#[cfg(test)]
|
||||
// Mixed source8 is exposed only through the separate borrowed read-only checker.
|
||||
mod analysis_source;
|
||||
pub use analysis_source::{CheckedMixed, check_mixed_bundle};
|
||||
#[path = "migration_v2/source.rs"]
|
||||
mod v2_source;
|
||||
use crate::*;
|
||||
@@ -338,7 +337,7 @@ fn enum_field(key: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse V5, or explicitly migrate a valid V1/V2/V3/V4 source candidate. Parsing never
|
||||
/// Parse ordinary V8, or explicitly migrate a supported older source candidate. Parsing never
|
||||
/// adopts a model and never overwrites an editor draft.
|
||||
pub fn parse(source: &str) -> Result<Project, Vec<Diagnostic>> {
|
||||
parse_limited(source, ParseLimits::UNLIMITED)
|
||||
@@ -480,7 +479,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
}
|
||||
.document()
|
||||
@@ -916,7 +914,6 @@ pub(crate) fn parse_unit(
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
}
|
||||
.unit(entry, true)
|
||||
@@ -1018,6 +1015,28 @@ fn edit_positions_from_tokens(
|
||||
token: impl Fn(usize) -> Lexeme,
|
||||
count: usize,
|
||||
value_ends: Option<&BTreeMap<usize, usize>>,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
edit_positions_from_tokens_with_text(source, unit, token, count, value_ends, |_, _| Ok(()))
|
||||
}
|
||||
fn edit_positions_from_tokens_with_text(
|
||||
source: &str,
|
||||
unit: &SourceUnit,
|
||||
token: impl Fn(usize) -> Lexeme,
|
||||
count: usize,
|
||||
value_ends: Option<&BTreeMap<usize, usize>>,
|
||||
text: impl FnMut(usize, Span) -> Result<(), Box<Diagnostic>>,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
edit_positions_from_tokens_with_producers(source, unit, token, count, value_ends, None, text)
|
||||
}
|
||||
|
||||
fn edit_positions_from_tokens_with_producers(
|
||||
source: &str,
|
||||
unit: &SourceUnit,
|
||||
token: impl Fn(usize) -> Lexeme,
|
||||
count: usize,
|
||||
value_ends: Option<&BTreeMap<usize, usize>>,
|
||||
mut quota: Option<usize>,
|
||||
text: impl FnMut(usize, Span) -> Result<(), Box<Diagnostic>>,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
let mut ends: Vec<_> = (0..count).map(|at| token(at).span.end).collect();
|
||||
let mut stack = Vec::new();
|
||||
@@ -1029,12 +1048,32 @@ fn edit_positions_from_tokens(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
struct Positions<'a, F> {
|
||||
struct Positions<'a, F, H> {
|
||||
token_at: F,
|
||||
ends: Vec<usize>,
|
||||
value_ends: Option<&'a BTreeMap<usize, usize>>,
|
||||
text: H,
|
||||
source: &'a str,
|
||||
quota: &'a mut Option<usize>,
|
||||
}
|
||||
impl<F: Fn(usize) -> Lexeme> Positions<'_, F> {
|
||||
impl<F: Fn(usize) -> Lexeme, H: FnMut(usize, Span) -> Result<(), Box<Diagnostic>>>
|
||||
Positions<'_, F, H>
|
||||
{
|
||||
fn consume(&mut self, slots: usize, offset: usize) -> Result<(), Box<Diagnostic>> {
|
||||
if let Some(remaining) = self.quota {
|
||||
*remaining = remaining.checked_sub(slots).ok_or_else(|| {
|
||||
Box::new(diagnostic(
|
||||
self.source,
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
message: "Source8 projection quota underflow.".into(),
|
||||
},
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn token(&self, start: usize) -> usize {
|
||||
let mut low = 0;
|
||||
let mut high = self.ends.len();
|
||||
@@ -1048,24 +1087,30 @@ fn edit_positions_from_tokens(
|
||||
}
|
||||
low
|
||||
}
|
||||
fn fields(&self, fields: &BTreeMap<String, Ast>) -> BTreeMap<String, EditField> {
|
||||
fn fields(
|
||||
&mut self,
|
||||
fields: &BTreeMap<String, Ast>,
|
||||
) -> Result<BTreeMap<String, EditField>, Box<Diagnostic>> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|(key, ast)| {
|
||||
let value = self.value(ast);
|
||||
self.consume(1, ast.offset())?;
|
||||
let value = self.value(ast)?;
|
||||
let start = (self.token_at)(self.token(value.span.start) - 1).span.start;
|
||||
let end = (self.token_at)(self.token(value.span.end)).span.end;
|
||||
(
|
||||
(self.text)(key.len(), Span { start, end })?;
|
||||
Ok((
|
||||
key.clone(),
|
||||
EditField {
|
||||
span: Span { start, end },
|
||||
value,
|
||||
},
|
||||
)
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn value(&self, ast: &Ast) -> EditValue {
|
||||
fn value(&mut self, ast: &Ast) -> Result<EditValue, Box<Diagnostic>> {
|
||||
self.consume(1, ast.offset())?;
|
||||
let index = self.token(ast.offset());
|
||||
let span = Span {
|
||||
start: ast.offset(),
|
||||
@@ -1080,23 +1125,33 @@ fn edit_positions_from_tokens(
|
||||
}),
|
||||
};
|
||||
let kind = match ast {
|
||||
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)),
|
||||
Ast::Array(items, _) => {
|
||||
EditKind::List(items.iter().map(|item| self.value(item)).collect())
|
||||
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)?),
|
||||
Ast::Array(items, _) => EditKind::List(
|
||||
items
|
||||
.iter()
|
||||
.map(|item| self.value(item))
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
Ast::Reference(alias, _) => {
|
||||
(self.text)(alias.len(), span)?;
|
||||
EditKind::Reference(alias.clone())
|
||||
}
|
||||
Ast::Reference(alias, _) => EditKind::Reference(alias.clone()),
|
||||
Ast::HistoricalUuid(id, _) => EditKind::HistoricalUuid(*id),
|
||||
_ => EditKind::Atom,
|
||||
};
|
||||
EditValue { span, kind }
|
||||
Ok(EditValue { span, kind })
|
||||
}
|
||||
}
|
||||
let positions = Positions {
|
||||
let mut positions = Positions {
|
||||
token_at: &token,
|
||||
ends,
|
||||
value_ends,
|
||||
text,
|
||||
source,
|
||||
quota: &mut quota,
|
||||
};
|
||||
unit.declarations
|
||||
let result = unit
|
||||
.declarations
|
||||
.iter()
|
||||
.map(|declaration| {
|
||||
if declaration.id_insertion.is_some() {
|
||||
@@ -1109,6 +1164,7 @@ fn edit_positions_from_tokens(
|
||||
},
|
||||
)));
|
||||
}
|
||||
positions.consume(7, declaration.span.start)?;
|
||||
let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) {
|
||||
&["kind", "alias", "id_keyword", "id"]
|
||||
} else {
|
||||
@@ -1132,12 +1188,23 @@ fn edit_positions_from_tokens(
|
||||
start: declaration.body.start - 1,
|
||||
end: declaration.body.end + 1,
|
||||
},
|
||||
kind: EditKind::Record(positions.fields(&declaration.fields)),
|
||||
kind: EditKind::Record(positions.fields(&declaration.fields)?),
|
||||
},
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if quota.is_some_and(|remaining| remaining != 0) {
|
||||
return Err(Box::new(diagnostic(
|
||||
source,
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset: 0,
|
||||
message: "Source8 projection quota leftover.".into(),
|
||||
},
|
||||
)));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
struct Parser<'a> {
|
||||
source: &'a str,
|
||||
@@ -1145,12 +1212,10 @@ struct Parser<'a> {
|
||||
cursor: usize,
|
||||
value_depth: usize,
|
||||
limits: ParseLimits,
|
||||
#[cfg(test)]
|
||||
mixed: Option<analysis_source::mixed::State<'a>>,
|
||||
}
|
||||
impl Parser<'_> {
|
||||
fn token(&self, index: usize) -> Option<Lexeme> {
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &self.mixed {
|
||||
return mixed.token(index);
|
||||
}
|
||||
@@ -1166,7 +1231,6 @@ impl Parser<'_> {
|
||||
})
|
||||
}
|
||||
fn syntax_entry(&mut self) -> Result<(), Error> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let span = self.current().span;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
@@ -1196,11 +1260,19 @@ impl Parser<'_> {
|
||||
if self.current().class != TokenClass::String {
|
||||
return Err(self.error("Expected a double-quoted string."));
|
||||
}
|
||||
let span = self.current().span;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
let raw = &self.source[span.start..span.end];
|
||||
let value = mixed.quoted_text(raw, span)?;
|
||||
self.cursor += 1;
|
||||
return Ok(value);
|
||||
}
|
||||
let value = serde_json::from_str(self.text()).unwrap();
|
||||
self.cursor += 1;
|
||||
Ok(value)
|
||||
}
|
||||
fn identifier(&mut self) -> Result<String, Error> {
|
||||
self.own_text(self.text().len(), self.current().span)?;
|
||||
let value = self.text().to_owned();
|
||||
if !crate::valid_alias(&value) {
|
||||
return Err(self.error(std::format!("Invalid or reserved alias '{value}'.")));
|
||||
@@ -1210,9 +1282,8 @@ impl Parser<'_> {
|
||||
}
|
||||
fn optional_id(&mut self) -> Result<Id, Error> {
|
||||
if self.text() != "id" {
|
||||
#[cfg(test)]
|
||||
if self.mixed.is_some() {
|
||||
return Err(self.error("Private mixed syntax requires explicit IDs."));
|
||||
return Err(self.error("Mixed source8 requires explicit UUIDs."));
|
||||
}
|
||||
return Ok(Id::new_v4());
|
||||
}
|
||||
@@ -1230,11 +1301,26 @@ impl Parser<'_> {
|
||||
message: "Expected UUID in 8-4-4-4-12 hexadecimal form.".into(),
|
||||
});
|
||||
}
|
||||
Id::parse_str(&value).map_err(|_| Error {
|
||||
let id = Id::parse_str(&value).map_err(|_| Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
message: "Invalid UUID identity.".into(),
|
||||
})
|
||||
})?;
|
||||
let bytes = value.len();
|
||||
drop(value);
|
||||
self.release_text(bytes);
|
||||
Ok(id)
|
||||
}
|
||||
fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.own_text(bytes, span)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn release_text(&mut self, bytes: usize) {
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.release_text(bytes);
|
||||
}
|
||||
}
|
||||
fn fields(&mut self) -> Result<BTreeMap<String, Ast>, Error> {
|
||||
self.take("{")?;
|
||||
@@ -1242,6 +1328,13 @@ impl Parser<'_> {
|
||||
while self.text() != "}" {
|
||||
let offset = self.current().span.start;
|
||||
let (key, value, _) = self.field()?;
|
||||
self.own_text(
|
||||
key.len(),
|
||||
Span {
|
||||
start: offset,
|
||||
end: offset,
|
||||
},
|
||||
)?;
|
||||
if fields.insert(key.clone(), value).is_some() {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
@@ -1249,12 +1342,16 @@ impl Parser<'_> {
|
||||
message: std::format!("Duplicate field '{key}'."),
|
||||
});
|
||||
}
|
||||
let bytes = key.len();
|
||||
drop(key);
|
||||
self.release_text(bytes);
|
||||
}
|
||||
self.take("}")?;
|
||||
Ok(fields)
|
||||
}
|
||||
fn field(&mut self) -> Result<(String, Ast, Span), Error> {
|
||||
self.syntax_entry()?;
|
||||
self.own_text(self.text().len(), self.current().span)?;
|
||||
let key = self.text().to_owned();
|
||||
if !matches!(
|
||||
self.current().class,
|
||||
@@ -1271,6 +1368,9 @@ impl Parser<'_> {
|
||||
.span
|
||||
.end;
|
||||
self.take(";")?;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.projection(1, Span { start, end })?;
|
||||
}
|
||||
Ok((key, value, Span { start, end }))
|
||||
}
|
||||
|
||||
@@ -1296,7 +1396,6 @@ impl Parser<'_> {
|
||||
self.value_depth += 1;
|
||||
let result = self.value_inner();
|
||||
self.value_depth -= 1;
|
||||
#[cfg(test)]
|
||||
if let Ok(ast) = &result {
|
||||
let end = self
|
||||
.token(self.cursor - 1)
|
||||
@@ -1304,7 +1403,12 @@ impl Parser<'_> {
|
||||
.span
|
||||
.end;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.value_ends.insert(ast.offset(), end);
|
||||
let span = Span {
|
||||
start: ast.offset(),
|
||||
end,
|
||||
};
|
||||
mixed.value_end(span)?;
|
||||
mixed.projection(1, span)?;
|
||||
}
|
||||
}
|
||||
result
|
||||
@@ -1312,7 +1416,6 @@ impl Parser<'_> {
|
||||
fn value_inner(&mut self) -> Result<Ast, Error> {
|
||||
let token = self.current();
|
||||
let offset = token.span.start;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &self.mixed
|
||||
&& (token.class == TokenClass::Number || matches!(self.text(), "-" | "+"))
|
||||
{
|
||||
@@ -1328,6 +1431,9 @@ impl Parser<'_> {
|
||||
offset,
|
||||
message: "Expected a UUID after uuid.".into(),
|
||||
})?;
|
||||
let bytes = value.len();
|
||||
drop(value);
|
||||
self.release_text(bytes);
|
||||
Ok(Ast::HistoricalUuid(id, offset))
|
||||
}
|
||||
"@" => {
|
||||
@@ -1368,6 +1474,7 @@ impl Parser<'_> {
|
||||
Ok(Ast::Literal(value, offset))
|
||||
}
|
||||
TokenClass::Identifier | TokenClass::Keyword => {
|
||||
self.own_text(self.text().len(), self.current().span)?;
|
||||
let value = self.text().to_owned();
|
||||
self.cursor += 1;
|
||||
Ok(Ast::Word(value, offset))
|
||||
@@ -1416,19 +1523,16 @@ impl Parser<'_> {
|
||||
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
|
||||
let mut header = if entry {
|
||||
self.take("arcadia")?;
|
||||
#[cfg(test)]
|
||||
let mixed_mode = self.mixed.is_some();
|
||||
#[cfg(not(test))]
|
||||
let mixed_mode = false;
|
||||
if if mixed_mode {
|
||||
self.text() != "8"
|
||||
} else {
|
||||
!matches!(self.text(), "3" | "4" | "5" | "6" | "7")
|
||||
!matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8")
|
||||
} {
|
||||
return Err(self.error(if mixed_mode {
|
||||
"Private mixed syntax requires arcadia 8."
|
||||
"Mixed source requires arcadia 8."
|
||||
} else {
|
||||
"Unsupported schema version: expected arcadia 3, 4, 5, 6 or 7."
|
||||
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
|
||||
}));
|
||||
}
|
||||
let schema_version = self.text().parse().expect("checked source version");
|
||||
@@ -1436,7 +1540,6 @@ impl Parser<'_> {
|
||||
self.take(";")?;
|
||||
let start = self.current().span.start;
|
||||
self.take("project")?;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.record(false, Span { start, end: start })?;
|
||||
}
|
||||
@@ -1481,7 +1584,6 @@ impl Parser<'_> {
|
||||
let mut open: Vec<usize> = Vec::new();
|
||||
loop {
|
||||
let offset = self.current().span.start;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed
|
||||
&& mixed.typed_header(self.cursor, open.is_empty())
|
||||
{
|
||||
@@ -1514,6 +1616,7 @@ impl Parser<'_> {
|
||||
}
|
||||
} else {
|
||||
let (key, value, span) = self.field()?;
|
||||
self.own_text(key.len(), span)?;
|
||||
let declaration = &mut declarations[owner];
|
||||
if declaration.fields.insert(key.clone(), value).is_some() {
|
||||
return Err(Error {
|
||||
@@ -1532,7 +1635,6 @@ impl Parser<'_> {
|
||||
if !includes_allowed {
|
||||
return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
|
||||
}
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.include(Span {
|
||||
start: offset,
|
||||
@@ -1579,15 +1681,13 @@ impl Parser<'_> {
|
||||
header = Some(entry_header);
|
||||
continue;
|
||||
}
|
||||
#[cfg(test)]
|
||||
if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) {
|
||||
return Err(self.error("Use the short source8 analysis declaration word."));
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
let word = self.text().to_owned();
|
||||
let kind = declaration_kind(&word)
|
||||
let word = self.current().text(self.source);
|
||||
let kind = declaration_kind(word)
|
||||
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.record(
|
||||
matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)),
|
||||
@@ -1624,9 +1724,13 @@ impl Parser<'_> {
|
||||
let body_start = self.current().span.end;
|
||||
self.take("{")?;
|
||||
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.lexical_owners.push(open.last().copied());
|
||||
let span = Span {
|
||||
start: offset,
|
||||
end: body_start,
|
||||
};
|
||||
mixed.lexical_owner(open.last().copied(), span)?;
|
||||
mixed.projection(7, span)?;
|
||||
}
|
||||
declarations.push(Declaration {
|
||||
kind,
|
||||
@@ -2152,6 +2256,10 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
#[path = "language_nesting_tests.rs"]
|
||||
mod nesting_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "language_ordinary_source8_tests.rs"]
|
||||
mod ordinary_source8;
|
||||
|
||||
pub(crate) mod legacy {
|
||||
//! Arcadia text: a readable, lossless representation of a valid OA project.
|
||||
//!
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! Unresolved typed source8 syntax, staged for the complete source adapter.
|
||||
//! This module does not admit a Project or enable public source8.
|
||||
//! Private typed syntax and complete read-only mixed source8 resolution.
|
||||
mod budget;
|
||||
mod cursor;
|
||||
mod declarations;
|
||||
#[cfg(test)]
|
||||
mod index;
|
||||
mod lex;
|
||||
mod lower;
|
||||
pub(super) mod mixed;
|
||||
mod syntax;
|
||||
#[cfg(test)]
|
||||
mod update;
|
||||
#[cfg(test)]
|
||||
mod write;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -18,6 +20,7 @@ use super::Span;
|
||||
use crate::analysis::{BinaryOp, UnaryOp};
|
||||
use crate::analysis_numeric::Exact;
|
||||
use budget::Budget;
|
||||
pub use mixed::project::{CheckedMixed, check_mixed_bundle};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Error {
|
||||
@@ -104,6 +107,7 @@ pub enum Binding {
|
||||
Expression(Expression),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
|
||||
let tokens = lex::tokens(source, budget)?;
|
||||
let mut cursor = cursor::Cursor::new(source, &tokens, budget);
|
||||
|
||||
@@ -38,6 +38,17 @@ pub struct Budget {
|
||||
pub usage: Usage,
|
||||
numeric: WorkMeter,
|
||||
failed: Option<Error>,
|
||||
text: Option<ProducerText>,
|
||||
entries: Option<ProducerEntries>,
|
||||
}
|
||||
struct ProducerEntries {
|
||||
carried: usize,
|
||||
metadata: usize,
|
||||
}
|
||||
struct ProducerText {
|
||||
carried: usize,
|
||||
owned: usize,
|
||||
temporary: usize,
|
||||
}
|
||||
impl Budget {
|
||||
pub fn new(limits: Limits) -> Result<Self, Error> {
|
||||
@@ -62,8 +73,158 @@ impl Budget {
|
||||
.map_err(|e| error(Span::default(), e.to_string()))?,
|
||||
),
|
||||
failed: None,
|
||||
text: None,
|
||||
entries: None,
|
||||
})
|
||||
}
|
||||
pub(super) fn enable_entry_producers(
|
||||
&mut self,
|
||||
carried: usize,
|
||||
files: usize,
|
||||
) -> Result<(), Error> {
|
||||
assert!(self.entries.is_none());
|
||||
self.check_producer_entries(carried, files, 0, Span::default())?;
|
||||
self.entries = Some(ProducerEntries {
|
||||
carried,
|
||||
metadata: files,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
fn check_producer_entries(
|
||||
&mut self,
|
||||
carried: usize,
|
||||
metadata: usize,
|
||||
delta: usize,
|
||||
span: Span,
|
||||
) -> Result<(), Error> {
|
||||
self.active()?;
|
||||
let total = carried
|
||||
.checked_add(self.usage.entries)
|
||||
.and_then(|n| n.checked_add(metadata))
|
||||
.and_then(|n| n.checked_add(delta));
|
||||
let message = match total {
|
||||
None => Some("Source8 producer entry size overflow."),
|
||||
Some(n) if n > self.limits.entries => Some("Source8 lowering entry limit exceeded."),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(message) = message {
|
||||
let failure = error(span, message);
|
||||
self.failed = Some(failure.clone());
|
||||
return Err(failure);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn producer_entries(&self) -> Option<usize> {
|
||||
self.entries.as_ref().map(|e| e.metadata)
|
||||
}
|
||||
pub(super) fn reserve_metadata(&mut self, delta: usize, span: Span) -> Result<(), Error> {
|
||||
if let Some(e) = &self.entries {
|
||||
self.check_producer_entries(e.carried, e.metadata, delta, span)?;
|
||||
self.entries.as_mut().expect("enabled entries").metadata += delta;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn producer_entry_overflow(&mut self, span: Span) -> Error {
|
||||
if let Some(failure) = &self.failed {
|
||||
return failure.clone();
|
||||
}
|
||||
let failure = error(span, "Source8 producer entry size overflow.");
|
||||
self.failed = Some(failure.clone());
|
||||
failure
|
||||
}
|
||||
pub(super) fn enable_text_producers(&mut self, carried: usize) -> Result<(), Error> {
|
||||
self.index_bytes(carried, Span::default())?;
|
||||
assert!(self.text.is_none());
|
||||
self.text = Some(ProducerText {
|
||||
carried,
|
||||
owned: 0,
|
||||
temporary: 0,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn producer_text(&self) -> Option<(usize, usize)> {
|
||||
self.text.as_ref().map(|t| (t.owned, t.temporary))
|
||||
}
|
||||
fn reserve_text(&mut self, bytes: usize, temporary: bool, span: Span) -> Result<(), Error> {
|
||||
let Some(t) = &self.text else {
|
||||
return Ok(());
|
||||
};
|
||||
let total = t
|
||||
.carried
|
||||
.checked_add(t.owned)
|
||||
.and_then(|n| n.checked_add(t.temporary))
|
||||
.and_then(|n| n.checked_add(bytes))
|
||||
.ok_or_else(|| error(span, "Source8 producer text size overflow."))?;
|
||||
self.index_bytes(total, span)?;
|
||||
let t = self.text.as_mut().expect("enabled producer text");
|
||||
if temporary {
|
||||
t.temporary += bytes;
|
||||
} else {
|
||||
t.owned += bytes;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
|
||||
self.reserve_text(bytes, false, span)
|
||||
}
|
||||
pub(super) fn temporary_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
|
||||
self.reserve_text(bytes, true, span)
|
||||
}
|
||||
pub(super) fn release_text(&mut self, bytes: usize) {
|
||||
if let Some(t) = &mut self.text {
|
||||
t.owned = t.owned.checked_sub(bytes).expect("owned text reservation");
|
||||
}
|
||||
}
|
||||
pub(super) fn release_temporary_text(&mut self, bytes: usize) {
|
||||
if let Some(t) = &mut self.text {
|
||||
t.temporary = t
|
||||
.temporary
|
||||
.checked_sub(bytes)
|
||||
.expect("temporary text reservation");
|
||||
}
|
||||
}
|
||||
pub(super) fn quoted_text(&mut self, raw: &str, span: Span) -> Result<String, Error> {
|
||||
if self.text.is_none() {
|
||||
return serde_json::from_str(raw).map_err(|_| error(span, "Invalid quoted string."));
|
||||
}
|
||||
let payload = raw
|
||||
.len()
|
||||
.checked_sub(2)
|
||||
.ok_or_else(|| error(span, "Invalid quoted text extent."))?;
|
||||
self.temporary_text(payload, span)?;
|
||||
let result = serde_json::from_str::<String>(raw);
|
||||
let value = match result {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
self.release_temporary_text(payload);
|
||||
return Err(error(span, "Invalid quoted string."));
|
||||
}
|
||||
};
|
||||
if value.len() > payload {
|
||||
drop(value);
|
||||
self.release_temporary_text(payload);
|
||||
return Err(error(span, "Decoded text exceeds its payload reservation."));
|
||||
}
|
||||
// Keep the reservation continuous while moving decoded bytes into T.
|
||||
// D <= P and the original checked aggregate bound prove both sums fit.
|
||||
let t = self.text.as_mut().expect("enabled producer text");
|
||||
let temporary = t
|
||||
.temporary
|
||||
.checked_sub(payload)
|
||||
.expect("quoted text reservation");
|
||||
let owned = t
|
||||
.owned
|
||||
.checked_add(value.len())
|
||||
.expect("bounded decoded transfer");
|
||||
t.temporary = temporary;
|
||||
t.owned = owned;
|
||||
Ok(value)
|
||||
}
|
||||
/// Immutable exact policy snapshot for private deliberate-snapshot continuation.
|
||||
#[cfg(test)]
|
||||
pub(super) fn limits(&self) -> Limits {
|
||||
self.limits
|
||||
}
|
||||
pub fn active(&self) -> Result<(), Error> {
|
||||
self.failed.clone().map_or(Ok(()), Err)
|
||||
}
|
||||
@@ -101,6 +262,9 @@ impl Budget {
|
||||
span,
|
||||
"syntax entries",
|
||||
)?;
|
||||
if let Some(e) = &self.entries {
|
||||
self.check_producer_entries(e.carried, e.metadata, 1, span)?;
|
||||
}
|
||||
self.usage.entries += 1;
|
||||
Ok(())
|
||||
}
|
||||
@@ -131,6 +295,7 @@ impl Budget {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn numeric_units(&self) -> u64 {
|
||||
self.numeric.usage().numeric_units
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ impl Cursor<'_, '_> {
|
||||
self.cursor += 1;
|
||||
Ok(span)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn end(&self) -> Result<(), Error> {
|
||||
if self.cursor != self.tokens.len() {
|
||||
Err(error(
|
||||
@@ -74,6 +75,7 @@ impl Cursor<'_, '_> {
|
||||
{
|
||||
return Err(error(span, "Expected a valid reference or parameter name."));
|
||||
}
|
||||
self.budget.own_text(self.text().len(), span)?;
|
||||
let name = self.text().to_owned();
|
||||
self.cursor += 1;
|
||||
Ok(Reference { name, span })
|
||||
@@ -87,8 +89,8 @@ impl Cursor<'_, '_> {
|
||||
{
|
||||
return Err(error(span, "Expected a quoted string."));
|
||||
}
|
||||
let value =
|
||||
serde_json::from_str(self.text()).map_err(|_| error(span, "Invalid quoted string."))?;
|
||||
let raw = &self.source[span.start..span.end];
|
||||
let value = self.budget.quoted_text(raw, span)?;
|
||||
self.cursor += 1;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
@@ -426,6 +426,9 @@ impl Cursor<'_, '_> {
|
||||
));
|
||||
}
|
||||
let id = Id::parse_str(&spelling).map_err(|_| error(literal, "Invalid UUID identity."))?;
|
||||
let bytes = spelling.len();
|
||||
drop(spelling);
|
||||
self.budget.release_text(bytes);
|
||||
Ok(IdentitySyntax {
|
||||
value: Some(id),
|
||||
keyword: Some(keyword),
|
||||
|
||||
@@ -380,7 +380,7 @@ fn missing_id_metadata_is_stable_and_parameter_ids_are_owner_scoped() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherited_version_root_and_short_word_contracts_do_not_enable_public8() {
|
||||
fn inherited_version_root_and_short_word_contracts_do_not_enable_public_typed8() {
|
||||
let source = "oa_check c \"C\" { predicate unbound; }";
|
||||
for version in [1, 2, 3, 4, 5, 6, 7, 9] {
|
||||
let mut budget = budget();
|
||||
@@ -431,8 +431,12 @@ fn inherited_version_root_and_short_word_contracts_do_not_enable_public8() {
|
||||
let contextual =
|
||||
declaration("calculation unit \"C\" { input and : Number; result Number; body $and; }");
|
||||
assert_eq!(contextual.header.alias.name, "unit");
|
||||
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
|
||||
for version in 3..=7 {
|
||||
let ordinary = crate::language::parse_with_report("arcadia 8; project p \"P\" {}").unwrap();
|
||||
assert!(ordinary.migration.is_none());
|
||||
assert_eq!(ordinary.project.schema_version, 8);
|
||||
assert_eq!(ordinary.project.alias, "p");
|
||||
assert_eq!(ordinary.project.name, "P");
|
||||
for version in 3..=8 {
|
||||
assert!(
|
||||
crate::language::parse(&format!(
|
||||
"arcadia {version}; project p \"P\" {{ {source} }}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Source8 significant-token adapter over the shared raw scanner.
|
||||
use super::super::raw_tokens::{Class as RawClass, Scanner};
|
||||
use super::super::raw_tokens::{Class as RawClass, Scanner, StringEvent};
|
||||
use super::{Budget, Error, Span, error};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -33,7 +33,20 @@ pub fn tokens(source: &str, budget: &mut Budget) -> Result<Vec<Token>, Error> {
|
||||
start: offset,
|
||||
end: offset,
|
||||
})?;
|
||||
let token = scanner.next().expect("source remains");
|
||||
let token = if budget.producer_text().is_some() {
|
||||
scanner.next_with_strings(|event| {
|
||||
match event {
|
||||
StringEvent::Reserve { span, payload } => {
|
||||
budget.temporary_text(payload, span)?
|
||||
}
|
||||
StringEvent::Released { payload } => budget.release_temporary_text(payload),
|
||||
}
|
||||
Ok::<(), Error>(())
|
||||
})?
|
||||
} else {
|
||||
scanner.next()
|
||||
}
|
||||
.expect("source remains");
|
||||
offset = token.span.end;
|
||||
let class = match token.class {
|
||||
RawClass::Word => Class::Word,
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
//! single complete-Project semantic admission after merging these additions.
|
||||
use super::declarations::{self as d, Declaration, DeclarationKind};
|
||||
use super::*;
|
||||
#[cfg(test)]
|
||||
use crate::Project;
|
||||
use crate::analysis as a;
|
||||
use crate::analysis_numeric::Dimension;
|
||||
use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody};
|
||||
use crate::{Element, ElementBody, Id, Kind, Layer, SharedBody};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Caller-assigned file key; spans are original offsets within that file.
|
||||
#[cfg(test)]
|
||||
pub(super) struct Unit {
|
||||
pub file: usize,
|
||||
pub declarations: Vec<Declaration>,
|
||||
@@ -180,6 +183,7 @@ impl Tables {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> {
|
||||
let at = Span::default();
|
||||
self.insert(project.id, &project.alias, Category::Project, at, budget)?;
|
||||
@@ -270,6 +274,7 @@ impl Tables {
|
||||
/// Inputs must come from the same live Budget and stabilization pass. No scan,
|
||||
/// exact conversion or reference charge occurs here. The ordinary skeleton is
|
||||
/// not semantically admitted: its typed forward references may need additions.
|
||||
#[cfg(test)]
|
||||
pub(super) fn lower(
|
||||
ordinary: &Project,
|
||||
units: &[Unit],
|
||||
|
||||
@@ -96,7 +96,16 @@ fn staged_oa_and_sa_lowering_match_complete_independent_17_uuid_oracles() {
|
||||
assert_eq!(actual, expected);
|
||||
admit(&actual); // the complete model's one semantic admission
|
||||
}
|
||||
assert!(crate::language::parse(&ORDINARY_OA.replacen("arcadia 7", "arcadia 8", 1)).is_err());
|
||||
for (ordinary_source, typed_source) in [(ORDINARY_OA, OA), (ORDINARY_SA, SA)] {
|
||||
let current = ordinary_source.replacen("arcadia 7", "arcadia 8", 1);
|
||||
let decoded = crate::language::parse_with_report(¤t).unwrap();
|
||||
assert!(decoded.migration.is_none());
|
||||
assert_eq!(decoded.project, ordinary(ordinary_source));
|
||||
assert!(
|
||||
crate::language::parse(&format!("arcadia 8; project p \"P\" {{ {typed_source} }}"))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
//! value-end entries, projected value/field/header/declaration positions, lexical
|
||||
//! owner slots and the file table. Checks do not recharge syntax or latch errors;
|
||||
//! every failure must return immediately. Tokens themselves use the token bound.
|
||||
//! This deliberately bounded staging policy is not calibrated public source8.
|
||||
//! The public read checker selects both producers; editing remains separately staged.
|
||||
|
||||
use super::{
|
||||
budget::{Budget, Limits},
|
||||
@@ -27,6 +27,8 @@ use crate::language::{
|
||||
use crate::source_project::{ProjectLimits, SourceBundle, SourceDiagnostic, traversal::Traversal};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod carried;
|
||||
|
||||
struct ModelCounts {
|
||||
limits: ProjectLimits,
|
||||
graph: usize,
|
||||
@@ -44,16 +46,22 @@ pub(super) struct ParsedMixedSyntax<'s> {
|
||||
// Includes the conservative dropped value-end subtotal. The semantic
|
||||
// phase adds syntax entries to this once, not tokens/positions again.
|
||||
metadata: usize,
|
||||
// Kept through consumed resolution/indexing; fixes the live backing owner.
|
||||
carried: Option<carried::CarriedGuard<'s>>,
|
||||
}
|
||||
|
||||
struct FileSyntax<'s> {
|
||||
path: &'s str,
|
||||
source: &'s str,
|
||||
// Retained original indexing metadata; the read result preserves its accounted ownership.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
tokens: Vec<lex::Token>,
|
||||
ordinary: SourceUnit,
|
||||
typed: Vec<declarations::Declaration>,
|
||||
positions: Vec<(Id, EditDeclaration)>,
|
||||
// A lexical owner is an occurrence index, not only a possibly repeated UUID.
|
||||
// Retained original indexing metadata; the read result preserves its accounted ownership.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
lexical_owners: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
@@ -64,8 +72,11 @@ pub(in crate::language) struct State<'a> {
|
||||
budget: &'a mut Budget,
|
||||
model: &'a mut ModelCounts,
|
||||
typed: Vec<declarations::Declaration>,
|
||||
pub(in crate::language) value_ends: BTreeMap<usize, usize>,
|
||||
pub(in crate::language) lexical_owners: Vec<Option<usize>>,
|
||||
value_ends: BTreeMap<usize, usize>,
|
||||
lexical_owners: Vec<Option<usize>>,
|
||||
pending_projection: usize,
|
||||
reserved_values: usize,
|
||||
reserved_owners: usize,
|
||||
}
|
||||
|
||||
fn ordinary_error(error: super::Error) -> Error {
|
||||
@@ -89,6 +100,55 @@ fn lexeme(token: lex::Token) -> Lexeme {
|
||||
}
|
||||
|
||||
impl State<'_> {
|
||||
pub(in crate::language) fn projection(
|
||||
&mut self,
|
||||
slots: usize,
|
||||
span: Span,
|
||||
) -> Result<(), Error> {
|
||||
if self.budget.producer_entries().is_some() {
|
||||
self.pending_projection = self
|
||||
.pending_projection
|
||||
.checked_add(slots)
|
||||
.ok_or_else(|| ordinary_error(self.budget.producer_entry_overflow(span)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(in crate::language) fn value_end(&mut self, span: Span) -> Result<(), Error> {
|
||||
let new_key = !self.value_ends.contains_key(&span.start);
|
||||
self.budget
|
||||
.reserve_metadata(usize::from(new_key), span)
|
||||
.map_err(ordinary_error)?;
|
||||
if new_key {
|
||||
self.reserved_values += 1;
|
||||
}
|
||||
self.value_ends.insert(span.start, span.end);
|
||||
Ok(())
|
||||
}
|
||||
pub(in crate::language) fn lexical_owner(
|
||||
&mut self,
|
||||
owner: Option<usize>,
|
||||
span: Span,
|
||||
) -> Result<(), Error> {
|
||||
self.budget
|
||||
.reserve_metadata(1, span)
|
||||
.map_err(ordinary_error)?;
|
||||
self.lexical_owners.push(owner);
|
||||
self.reserved_owners += 1;
|
||||
Ok(())
|
||||
}
|
||||
pub(in crate::language) fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
|
||||
self.budget.own_text(bytes, span).map_err(ordinary_error)
|
||||
}
|
||||
pub(in crate::language) fn release_text(&mut self, bytes: usize) {
|
||||
self.budget.release_text(bytes);
|
||||
}
|
||||
pub(in crate::language) fn quoted_text(
|
||||
&mut self,
|
||||
raw: &str,
|
||||
span: Span,
|
||||
) -> Result<String, Error> {
|
||||
self.budget.quoted_text(raw, span).map_err(ordinary_error)
|
||||
}
|
||||
pub(in crate::language) fn token(&self, at: usize) -> Option<Lexeme> {
|
||||
self.tokens.get(at).copied().map(lexeme)
|
||||
}
|
||||
@@ -200,7 +260,7 @@ impl State<'_> {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset: declaration.span.start,
|
||||
message: "Private mixed syntax requires explicit IDs.".into(),
|
||||
message: "Mixed source8 requires explicit UUIDs.".into(),
|
||||
});
|
||||
}
|
||||
self.typed.push(declaration);
|
||||
@@ -220,17 +280,22 @@ pub(in crate::language) fn long_analysis_word(word: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn parse(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let profile = Limits::source8(ParseLimits {
|
||||
let profile = source_profile(limits);
|
||||
parse_with_profile(bundle, limits, profile)
|
||||
}
|
||||
|
||||
fn source_profile(limits: ProjectLimits) -> Limits {
|
||||
Limits::source8(ParseLimits {
|
||||
// Storage is not the caller's final model record/ordered-step accounting.
|
||||
max_source_bytes: limits.model.max_source_bytes,
|
||||
max_step_references: limits.model.max_step_references,
|
||||
..ParseLimits::UNLIMITED
|
||||
});
|
||||
parse_with_profile(bundle, limits, profile)
|
||||
})
|
||||
}
|
||||
|
||||
fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
|
||||
@@ -241,11 +306,23 @@ fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
|
||||
}]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_with_profile(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
profile: Limits,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
parse_impl(bundle, limits, profile, None, false, false)
|
||||
}
|
||||
|
||||
fn parse_impl<'s>(
|
||||
bundle: &'s SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
profile: Limits,
|
||||
carried: Option<carried::CarriedGuard<'s>>,
|
||||
text_producers: bool,
|
||||
entry_producers: bool,
|
||||
) -> Result<ParsedMixedSyntax<'s>, Vec<SourceDiagnostic>> {
|
||||
let entry_source = bundle.files.get(&bundle.entry).map_or("", String::as_str);
|
||||
let at_entry = |error| located(&bundle.entry, entry_source, ordinary_error(error));
|
||||
let maximum = Limits::source8(ParseLimits {
|
||||
@@ -266,6 +343,17 @@ fn parse_with_profile(
|
||||
)));
|
||||
}
|
||||
let mut budget = Budget::new(profile).map_err(at_entry)?;
|
||||
if text_producers {
|
||||
let input = carried
|
||||
.as_ref()
|
||||
.expect("text mode requires carried authority");
|
||||
budget
|
||||
.enable_text_producers(input.slots().text)
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
if let Some(input) = &carried {
|
||||
input.check(&budget, 0, 0).map_err(at_entry)?;
|
||||
}
|
||||
let bytes = bundle
|
||||
.files
|
||||
.values()
|
||||
@@ -275,9 +363,20 @@ fn parse_with_profile(
|
||||
.index_bytes(bytes, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
let mut metadata = bundle.files.len();
|
||||
budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
if entry_producers {
|
||||
let input = carried
|
||||
.as_ref()
|
||||
.expect("entry mode requires carried authority");
|
||||
budget
|
||||
.enable_entry_producers(input.slots().entries, metadata)
|
||||
.map_err(at_entry)?;
|
||||
} else if let Some(input) = &carried {
|
||||
input.check(&budget, 0, metadata).map_err(at_entry)?;
|
||||
} else {
|
||||
budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
// Tighter private byte/file-slot limits precede folded-path table and
|
||||
// traversal allocations. Public Traversal's own validation order is intact.
|
||||
let mut traversal = Traversal::new(bundle, limits)?;
|
||||
@@ -295,6 +394,7 @@ fn parse_with_profile(
|
||||
.expect("traversal checked source");
|
||||
let failure = |error| located(path, source, ordinary_error(error));
|
||||
let tokens = lex::tokens(source, &mut budget).map_err(failure)?;
|
||||
let metadata_before = budget.producer_entries();
|
||||
let mut parser = Parser {
|
||||
source,
|
||||
tokens: Vec::new(),
|
||||
@@ -309,6 +409,9 @@ fn parse_with_profile(
|
||||
typed: Vec::new(),
|
||||
value_ends: BTreeMap::new(),
|
||||
lexical_owners: Vec::new(),
|
||||
pending_projection: 0,
|
||||
reserved_values: 0,
|
||||
reserved_owners: 0,
|
||||
}),
|
||||
};
|
||||
let ordinary = parser
|
||||
@@ -318,23 +421,48 @@ fn parse_with_profile(
|
||||
drop(parser);
|
||||
// Projected ordinary AST nodes/fields are bounded before constructing a
|
||||
// second position representation. Count references as slots, not syntax.
|
||||
let projection_slots = ordinary_position_slots(&ordinary);
|
||||
let projection_slots = if entry_producers {
|
||||
let slots = tokens
|
||||
.len()
|
||||
.checked_add(state.pending_projection)
|
||||
.ok_or_else(|| failure(state.budget.producer_entry_overflow(Span::default())))?;
|
||||
state
|
||||
.budget
|
||||
.reserve_metadata(slots, Span::default())
|
||||
.map_err(failure)?;
|
||||
state.pending_projection
|
||||
} else {
|
||||
ordinary_position_slots(&ordinary)
|
||||
};
|
||||
metadata = metadata
|
||||
.checked_add(tokens.len())
|
||||
.and_then(|n| n.checked_add(state.value_ends.len()))
|
||||
.and_then(|n| n.checked_add(projection_slots))
|
||||
.and_then(|n| n.checked_add(state.lexical_owners.len()))
|
||||
.ok_or_else(|| failure(super::error(Span::default(), "Metadata size overflow.")))?;
|
||||
state
|
||||
.budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(failure)?;
|
||||
let positions = language::edit_positions_from_tokens(
|
||||
if let Some(input) = &carried {
|
||||
input
|
||||
.check(state.budget, state.budget.usage.entries, metadata)
|
||||
.map_err(failure)?;
|
||||
} else {
|
||||
state
|
||||
.budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(failure)?;
|
||||
}
|
||||
let positions = language::edit_positions_from_tokens_with_producers(
|
||||
source,
|
||||
&ordinary,
|
||||
|at| lexeme(tokens[at]),
|
||||
tokens.len(),
|
||||
Some(&state.value_ends),
|
||||
entry_producers.then_some(projection_slots),
|
||||
|bytes, span| {
|
||||
state
|
||||
.budget
|
||||
.own_text(bytes, span)
|
||||
.map_err(|e| Box::new(language::diagnostic(source, ordinary_error(e))))
|
||||
},
|
||||
)
|
||||
.map_err(|e| {
|
||||
vec![SourceDiagnostic {
|
||||
@@ -343,6 +471,23 @@ fn parse_with_profile(
|
||||
related: Vec::new(),
|
||||
}]
|
||||
})?;
|
||||
if let Some(before) = metadata_before {
|
||||
let expected = tokens
|
||||
.len()
|
||||
.checked_add(state.value_ends.len())
|
||||
.and_then(|n| n.checked_add(state.lexical_owners.len()))
|
||||
.and_then(|n| n.checked_add(projection_slots))
|
||||
.and_then(|n| n.checked_add(before));
|
||||
if state.reserved_values != state.value_ends.len()
|
||||
|| state.reserved_owners != state.lexical_owners.len()
|
||||
|| expected != state.budget.producer_entries()
|
||||
{
|
||||
return Err(failure(super::error(
|
||||
Span::default(),
|
||||
"Source8 producer entry inventory mismatch.",
|
||||
)));
|
||||
}
|
||||
}
|
||||
for (raw, span) in &ordinary.includes {
|
||||
traversal.include(path, raw, *span)?;
|
||||
}
|
||||
@@ -367,12 +512,27 @@ fn parse_with_profile(
|
||||
}
|
||||
traversal.finish()?;
|
||||
budget.active().map_err(at_entry)?;
|
||||
if budget.producer_entries().is_some_and(|m| m != metadata) {
|
||||
return Err(at_entry(super::error(
|
||||
Span::default(),
|
||||
"Source8 producer entry inventory mismatch.",
|
||||
)));
|
||||
}
|
||||
if let Some((_, temporary)) = budget.producer_text() {
|
||||
assert_eq!(temporary, 0, "completed syntax has no temporary text");
|
||||
}
|
||||
if let Some(input) = &carried {
|
||||
input
|
||||
.check(&budget, budget.usage.entries, metadata)
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
Ok(ParsedMixedSyntax {
|
||||
bundle,
|
||||
limits,
|
||||
files: parsed.into_values().collect(),
|
||||
budget,
|
||||
metadata,
|
||||
carried,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Complete pending-backed authority and fixture-only carried snapshots.
|
||||
//! Checks existing logical phase totals; does not meter every parser allocation.
|
||||
use super::*;
|
||||
use project::accounting::{Ledger, Slots};
|
||||
#[cfg(test)]
|
||||
use project::ordinary_index::rename::emission::PendingRename;
|
||||
|
||||
struct SnapshotPolicy {
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
}
|
||||
|
||||
/// The allocation-owning bundle guard. No production constructor exists yet.
|
||||
#[cfg(test)]
|
||||
struct RetainedSnapshot {
|
||||
bundle: SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
retained: Slots,
|
||||
}
|
||||
enum SnapshotBacking<'s> {
|
||||
#[cfg(test)]
|
||||
Pending(&'s PendingRename<'s>),
|
||||
Borrowed {
|
||||
bundle: &'s SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
},
|
||||
#[cfg(test)]
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
/// Neither Clone nor Copy: fixes the bundle, policy and retained allocation owner.
|
||||
pub(super) struct CarriedInput<'s> {
|
||||
backing: SnapshotBacking<'s>,
|
||||
}
|
||||
enum GuardBacking<'s> {
|
||||
#[cfg(test)]
|
||||
Pending(&'s PendingRename<'s>),
|
||||
Borrowed {
|
||||
_bundle: &'s SourceBundle,
|
||||
},
|
||||
#[cfg(test)]
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
/// Policy has transferred into Budget/ParsedMixedSyntax. Only backing ownership
|
||||
/// remains here, avoiding a duplicate inline policy in every later witness.
|
||||
pub(super) struct CarriedGuard<'s> {
|
||||
backing: GuardBacking<'s>,
|
||||
}
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn fresh(bundle: &'s SourceBundle, limits: ProjectLimits) -> Self {
|
||||
Self {
|
||||
backing: SnapshotBacking::Borrowed {
|
||||
bundle,
|
||||
policy: SnapshotPolicy {
|
||||
limits: source_profile(limits),
|
||||
project_limits: limits,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self {
|
||||
Self {
|
||||
backing: SnapshotBacking::Pending(pending),
|
||||
}
|
||||
}
|
||||
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
|
||||
match self.backing {
|
||||
#[cfg(test)]
|
||||
SnapshotBacking::Pending(pending) => {
|
||||
let (limits, project_limits) = pending.carried_policy();
|
||||
(
|
||||
pending.carried_bundle(),
|
||||
SnapshotPolicy {
|
||||
limits,
|
||||
project_limits,
|
||||
},
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Pending(pending),
|
||||
},
|
||||
)
|
||||
}
|
||||
SnapshotBacking::Borrowed { bundle, policy } => (
|
||||
bundle,
|
||||
policy,
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Borrowed { _bundle: bundle },
|
||||
},
|
||||
),
|
||||
#[cfg(test)]
|
||||
SnapshotBacking::Retained(owner) => (
|
||||
&owner.bundle,
|
||||
SnapshotPolicy {
|
||||
limits: owner.policy.limits,
|
||||
project_limits: owner.policy.project_limits,
|
||||
},
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Retained(owner),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CarriedGuard<'_> {
|
||||
pub(super) fn slots(&self) -> Slots {
|
||||
match &self.backing {
|
||||
#[cfg(test)]
|
||||
GuardBacking::Pending(pending) => pending.carried_slots(),
|
||||
GuardBacking::Borrowed { .. } => Slots::default(),
|
||||
#[cfg(test)]
|
||||
GuardBacking::Retained(owner) => owner.retained,
|
||||
}
|
||||
}
|
||||
pub(super) fn check(
|
||||
&self,
|
||||
budget: &Budget,
|
||||
syntax: usize,
|
||||
metadata: usize,
|
||||
) -> Result<(), super::super::Error> {
|
||||
let at = Span::default();
|
||||
let total = self
|
||||
.slots()
|
||||
.add(Slots::entries(syntax), at)?
|
||||
.add(Slots::entries(metadata), at)?;
|
||||
// A local check, not another reservation retained in the syntax owner.
|
||||
Ledger::default().reserve(budget, total, at)
|
||||
}
|
||||
}
|
||||
|
||||
/// Input already binds the actual bundle; callers cannot supply a replacement.
|
||||
#[cfg(test)]
|
||||
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let (bundle, policy, guard) = input.into_parts();
|
||||
parse_impl(
|
||||
bundle,
|
||||
policy.project_limits,
|
||||
policy.limits,
|
||||
Some(guard),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Explicit stronger fixture route; the actual owner supplies all authority.
|
||||
#[cfg(test)]
|
||||
fn parse_text_producers(
|
||||
input: CarriedInput<'_>,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let (bundle, policy, guard) = input.into_parts();
|
||||
parse_impl(
|
||||
bundle,
|
||||
policy.project_limits,
|
||||
policy.limits,
|
||||
Some(guard),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Combined producer route, selected only by sealed pending or fixture authority.
|
||||
pub(super) fn parse_entry_producers(
|
||||
input: CarriedInput<'_>,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let (bundle, policy, guard) = input.into_parts();
|
||||
parse_impl(
|
||||
bundle,
|
||||
policy.project_limits,
|
||||
policy.limits,
|
||||
Some(guard),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SnapshotPolicy {
|
||||
fn captured(syntax: &ParsedMixedSyntax<'_>) -> Self {
|
||||
Self {
|
||||
limits: syntax.budget.limits(),
|
||||
project_limits: syntax.limits,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl RetainedSnapshot {
|
||||
fn fixture(bundle: SourceBundle, policy: SnapshotPolicy) -> Result<Self, super::super::Error> {
|
||||
let at = Span::default();
|
||||
let mut text = bundle.entry.len();
|
||||
for (path, source) in &bundle.files {
|
||||
text = Slots::text(text)
|
||||
.add(Slots::text(path.len()), at)?
|
||||
.add(Slots::text(source.len()), at)?
|
||||
.text;
|
||||
}
|
||||
let retained = Slots {
|
||||
entries: bundle.files.len(),
|
||||
occurrences: 0,
|
||||
text,
|
||||
};
|
||||
Ok(Self {
|
||||
bundle,
|
||||
policy,
|
||||
retained,
|
||||
})
|
||||
}
|
||||
fn input(&self) -> CarriedInput<'_> {
|
||||
CarriedInput {
|
||||
backing: SnapshotBacking::Retained(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn borrowed(bundle: &'s SourceBundle, syntax: &ParsedMixedSyntax<'_>) -> Self {
|
||||
Self {
|
||||
backing: SnapshotBacking::Borrowed {
|
||||
bundle,
|
||||
policy: SnapshotPolicy::captured(syntax),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod text_producer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod entry_producer;
|
||||
|
||||
pub(super) fn parse_fresh(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
parse_entry_producers(CarriedInput::fresh(bundle, limits))
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod public_read;
|
||||
@@ -0,0 +1,445 @@
|
||||
//! Literal entry oracles, independently authored before the producer implementation.
|
||||
use super::*;
|
||||
const LIMIT: &str = "Source8 lowering entry limit exceeded.";
|
||||
fn doc(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
)
|
||||
}
|
||||
fn bundle(body: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(body))]),
|
||||
}
|
||||
}
|
||||
fn chain() -> SourceBundle {
|
||||
let mut b = bundle("include \"part.arcadia\";");
|
||||
b.files.insert(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
);
|
||||
b.files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
b
|
||||
}
|
||||
fn held(bundle: SourceBundle, entries: usize) -> RetainedSnapshot {
|
||||
RetainedSnapshot::fixture(
|
||||
bundle,
|
||||
SnapshotPolicy {
|
||||
limits: Limits {
|
||||
entries,
|
||||
bytes: 4096,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
},
|
||||
project_limits: ProjectLimits::default(),
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
fn failed(result: Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>) -> SourceDiagnostic {
|
||||
let errors = match result {
|
||||
Ok(_) => panic!("unexpected syntax success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(errors.len(), 1);
|
||||
errors.into_iter().next().unwrap()
|
||||
}
|
||||
fn diagnostic(d: &SourceDiagnostic, path: &str, start: usize, end: usize) {
|
||||
assert_eq!(d.path, path);
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.message, LIMIT);
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn real_owner_syntax_and_resolver_boundaries_are_separate() {
|
||||
for (b, c, u, m, syntax_cap, resolve_cap, refused_path) in [
|
||||
(bundle(""), 1, 1, 11, 13, 17, "main.arcadia"),
|
||||
(chain(), 3, 3, 19, 25, 29, "part.arcadia"),
|
||||
] {
|
||||
let owner = held(b.clone(), syntax_cap);
|
||||
let syntax = parse_entry_producers(owner.input()).unwrap();
|
||||
assert_eq!(owner.retained.entries, c);
|
||||
assert_eq!(syntax.budget.usage.entries, u);
|
||||
assert_eq!(syntax.budget.producer_entries(), Some(m));
|
||||
assert_eq!(syntax.metadata, m);
|
||||
assert_eq!(
|
||||
project::text_producer_inventory(&syntax).unwrap().entries,
|
||||
c + u + m
|
||||
);
|
||||
drop(syntax);
|
||||
let low = held(b.clone(), syntax_cap - 1);
|
||||
diagnostic(
|
||||
&failed(parse_entry_producers(low.input())),
|
||||
refused_path,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
for cap in [resolve_cap, resolve_cap - 1] {
|
||||
let owner = held(b.clone(), cap);
|
||||
let syntax = parse_entry_producers(owner.input()).unwrap();
|
||||
let usage = syntax.budget.usage;
|
||||
let numeric = syntax.budget.numeric_units();
|
||||
match project::resolve(syntax) {
|
||||
Ok(result) => {
|
||||
assert_eq!(cap, resolve_cap);
|
||||
assert_eq!(result.phase_state().1.entries, c + u + m);
|
||||
assert_eq!(result.phase_state().0.budget.usage, usage);
|
||||
assert_eq!(result.phase_state().0.budget.numeric_units(), numeric);
|
||||
let text =
|
||||
project::resolve(parse_text_producers(owner.input()).unwrap()).unwrap();
|
||||
assert_eq!(result.project(), text.project());
|
||||
assert_eq!(result.warnings(), text.warnings());
|
||||
assert_eq!(result.phase_state().1, text.phase_state().1);
|
||||
}
|
||||
Err(failure) => {
|
||||
assert_eq!(cap, resolve_cap - 1);
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
diagnostic(&failure.diagnostics[0], "main.arcadia", 11, 73);
|
||||
}
|
||||
}
|
||||
assert_eq!(owner.bundle, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn owner_and_value_refuse_at_the_original_producer() {
|
||||
for (body, cap, offset, complete) in [
|
||||
(
|
||||
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { }",
|
||||
4,
|
||||
75,
|
||||
29,
|
||||
),
|
||||
(
|
||||
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"\"; }",
|
||||
7,
|
||||
149,
|
||||
37,
|
||||
),
|
||||
] {
|
||||
let owner = held(bundle(body), cap);
|
||||
diagnostic(
|
||||
&failed(parse_entry_producers(owner.input())),
|
||||
"main.arcadia",
|
||||
offset,
|
||||
offset + 1,
|
||||
);
|
||||
let owner = held(bundle(body), complete);
|
||||
let syntax = parse_entry_producers(owner.input()).unwrap();
|
||||
assert_eq!(
|
||||
owner.retained.entries + syntax.budget.usage.entries + syntax.metadata,
|
||||
complete
|
||||
);
|
||||
// The independent inventory walker has its own existing scratch peak.
|
||||
let roomy = held(bundle(body), 4096);
|
||||
let syntax = parse_entry_producers(roomy.input()).unwrap();
|
||||
assert_eq!(
|
||||
project::text_producer_inventory(&syntax).unwrap().entries,
|
||||
complete
|
||||
);
|
||||
}
|
||||
}
|
||||
const NESTED: &str = "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\noa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { payload { items [@p, uuid \"00000000-0000-0000-0000-000000000001\", { flag true; }]; };\noa_activity b \"B\" id \"00000000-0000-0000-0000-000000000003\" { }\n}\nnode n id \"00000000-0000-0000-0000-000000000004\" { }\n}\n";
|
||||
fn nested() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), NESTED.into())]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn nested_inventory_and_prepaid_projection_quota() {
|
||||
assert_eq!(NESTED.len(), 344);
|
||||
let owner = held(nested(), 103);
|
||||
let syntax = parse_entry_producers(owner.input()).unwrap();
|
||||
assert_eq!(syntax.budget.usage.entries, 13);
|
||||
assert_eq!(syntax.budget.producer_entries(), Some(89));
|
||||
assert_eq!(syntax.metadata, 89);
|
||||
assert_eq!(
|
||||
owner.retained.entries + syntax.budget.usage.entries + syntax.metadata,
|
||||
103
|
||||
);
|
||||
let roomy = held(nested(), 4096);
|
||||
let inventory_syntax = parse_entry_producers(roomy.input()).unwrap();
|
||||
assert_eq!(
|
||||
project::text_producer_inventory(&inventory_syntax)
|
||||
.unwrap()
|
||||
.entries,
|
||||
103
|
||||
);
|
||||
let file = &syntax.files[0];
|
||||
assert_eq!(file.tokens.len(), 49);
|
||||
assert_eq!(file.lexical_owners, [None, Some(0), None]);
|
||||
assert_eq!(file.positions.len(), 3);
|
||||
assert_eq!(file.positions[0].1.header.len(), 5);
|
||||
assert_eq!(file.positions[2].1.header.len(), 4);
|
||||
let language::EditKind::Record(fields) = &file.positions[0].1.fields.kind else {
|
||||
panic!("body")
|
||||
};
|
||||
let payload = &fields["payload"].value;
|
||||
assert_eq!(
|
||||
payload.span,
|
||||
Span {
|
||||
start: 145,
|
||||
end: 221
|
||||
}
|
||||
);
|
||||
let language::EditKind::Record(fields) = &payload.kind else {
|
||||
panic!("payload")
|
||||
};
|
||||
let items = &fields["items"].value;
|
||||
assert_eq!(
|
||||
items.span,
|
||||
Span {
|
||||
start: 153,
|
||||
end: 218
|
||||
}
|
||||
);
|
||||
let language::EditKind::List(items) = &items.kind else {
|
||||
panic!("items")
|
||||
};
|
||||
assert_eq!(
|
||||
items.iter().map(|v| v.span).collect::<Vec<_>>(),
|
||||
[
|
||||
Span {
|
||||
start: 154,
|
||||
end: 156
|
||||
},
|
||||
Span {
|
||||
start: 158,
|
||||
end: 201
|
||||
},
|
||||
Span {
|
||||
start: 203,
|
||||
end: 217
|
||||
}
|
||||
]
|
||||
);
|
||||
let language::EditKind::Record(fields) = &items[2].kind else {
|
||||
panic!("member")
|
||||
};
|
||||
assert_eq!(
|
||||
fields["flag"].value.span,
|
||||
Span {
|
||||
start: 210,
|
||||
end: 214
|
||||
}
|
||||
);
|
||||
// Injected prepaid quota errors, not valid alternate source inventories.
|
||||
for (quota, message) in [
|
||||
(29, "Source8 projection quota underflow."),
|
||||
(31, "Source8 projection quota leftover."),
|
||||
] {
|
||||
let error = match language::edit_positions_from_tokens_with_producers(
|
||||
file.source,
|
||||
&file.ordinary,
|
||||
|at| lexeme(file.tokens[at]),
|
||||
file.tokens.len(),
|
||||
None,
|
||||
Some(quota),
|
||||
|_, _| Ok(()),
|
||||
) {
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("injected projection quota must refuse"),
|
||||
};
|
||||
assert_eq!(error.message, message);
|
||||
}
|
||||
assert_eq!(syntax.budget.producer_entries(), Some(89));
|
||||
let low = held(nested(), 102);
|
||||
diagnostic(
|
||||
&failed(parse_entry_producers(low.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
);
|
||||
}
|
||||
fn make_state<'a>(budget: &'a mut Budget, model: &'a mut ModelCounts) -> State<'a> {
|
||||
State {
|
||||
source: "",
|
||||
tokens: &[],
|
||||
budget,
|
||||
model,
|
||||
typed: Vec::new(),
|
||||
value_ends: BTreeMap::new(),
|
||||
lexical_owners: Vec::new(),
|
||||
pending_projection: 0,
|
||||
reserved_values: 0,
|
||||
reserved_owners: 0,
|
||||
}
|
||||
}
|
||||
fn internal_budget(cap: usize) -> Budget {
|
||||
Budget::new(Limits {
|
||||
entries: cap,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn internal_reserve_and_mutate_helpers_leave_collections_unchanged() {
|
||||
// Scalar fault fixtures exercise atomic mutation; these do not invent real owners.
|
||||
let mut model = ModelCounts {
|
||||
limits: ProjectLimits::default(),
|
||||
graph: 0,
|
||||
records: 0,
|
||||
includes: 0,
|
||||
};
|
||||
let mut budget = internal_budget(3);
|
||||
budget.enable_entry_producers(1, 1).unwrap();
|
||||
let mut state = make_state(&mut budget, &mut model);
|
||||
state.value_end(Span { start: 10, end: 11 }).unwrap();
|
||||
state.value_end(Span { start: 10, end: 12 }).unwrap();
|
||||
assert_eq!(state.value_ends, BTreeMap::from([(10, 12)]));
|
||||
assert_eq!(state.reserved_values, 1);
|
||||
assert_eq!(state.budget.producer_entries(), Some(2));
|
||||
let error = state.value_end(Span { start: 20, end: 21 }).unwrap_err();
|
||||
assert_eq!(error.message, LIMIT);
|
||||
assert_eq!(error.offset, 20);
|
||||
let repeated = state.value_end(Span { start: 10, end: 99 }).unwrap_err();
|
||||
assert_eq!(repeated.offset, 20);
|
||||
assert_eq!(repeated.message, LIMIT);
|
||||
assert_eq!(state.value_ends, BTreeMap::from([(10, 12)]));
|
||||
assert_eq!(state.budget.producer_entries(), Some(2));
|
||||
let error = state
|
||||
.lexical_owner(Some(0), Span { start: 30, end: 31 })
|
||||
.unwrap_err();
|
||||
assert_eq!(error.offset, 20); // Original latched refusal survives a later producer.
|
||||
assert!(state.lexical_owners.is_empty());
|
||||
assert_eq!(state.reserved_owners, 0);
|
||||
let mut budget = internal_budget(3);
|
||||
budget.enable_entry_producers(1, 1).unwrap();
|
||||
let mut state = make_state(&mut budget, &mut model);
|
||||
state.lexical_owner(None, Span::default()).unwrap();
|
||||
assert!(
|
||||
state
|
||||
.lexical_owner(None, Span { start: 40, end: 41 })
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(state.lexical_owners, [None]);
|
||||
assert_eq!(state.reserved_owners, 1);
|
||||
assert_eq!(state.budget.producer_entries(), Some(2));
|
||||
}
|
||||
#[test]
|
||||
fn internal_overflow_latch_and_syntax_work_precedence() {
|
||||
let at = Span { start: 9, end: 10 };
|
||||
let mut budget = internal_budget(2);
|
||||
budget.enable_entry_producers(1, 1).unwrap();
|
||||
let e = budget.entry(at).unwrap_err();
|
||||
assert_eq!(e.message, LIMIT);
|
||||
assert_eq!(budget.producer_entry_overflow(Span::default()).span, at);
|
||||
assert_eq!(budget.active().unwrap_err().message, LIMIT);
|
||||
assert_eq!(budget.usage.entries, 0);
|
||||
assert_eq!(budget.producer_entries(), Some(1));
|
||||
assert_eq!(
|
||||
budget
|
||||
.reserve_metadata(0, Span::default())
|
||||
.unwrap_err()
|
||||
.span,
|
||||
at
|
||||
);
|
||||
let mut budget = internal_budget(0);
|
||||
budget.enable_entry_producers(0, 0).unwrap();
|
||||
assert_eq!(
|
||||
budget.entry(at).unwrap_err().message,
|
||||
"Source8 syntax entries limit exceeded."
|
||||
);
|
||||
let mut budget = internal_budget(100);
|
||||
budget.enable_entry_producers(1, 1).unwrap();
|
||||
assert_eq!(
|
||||
budget.reserve_metadata(usize::MAX, at).unwrap_err().message,
|
||||
"Source8 producer entry size overflow."
|
||||
);
|
||||
assert_eq!(budget.producer_entries(), Some(1));
|
||||
assert_eq!(budget.active().unwrap_err().span, at);
|
||||
let mut budget = internal_budget(100);
|
||||
assert!(budget.enable_entry_producers(usize::MAX, 1).is_err());
|
||||
assert_eq!(budget.producer_entries(), None);
|
||||
assert!(budget.active().is_err());
|
||||
let mut budget = internal_budget(100);
|
||||
budget.enable_entry_producers(0, 0).unwrap();
|
||||
let mut model = ModelCounts {
|
||||
limits: ProjectLimits::default(),
|
||||
graph: 0,
|
||||
records: 0,
|
||||
includes: 0,
|
||||
};
|
||||
let mut state = make_state(&mut budget, &mut model);
|
||||
state.pending_projection = usize::MAX;
|
||||
assert_eq!(
|
||||
state.projection(1, at).unwrap_err().message,
|
||||
"Source8 producer entry size overflow."
|
||||
);
|
||||
assert_eq!(state.pending_projection, usize::MAX);
|
||||
assert!(state.budget.active().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn explicit_zero_and_actual_owner_preserve_complete_oa_sa_and_old_routes() {
|
||||
for (work, property) in [
|
||||
("oa_activity", "oa_property"),
|
||||
("sa_function", "sa_property"),
|
||||
] {
|
||||
let body = format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000003\" {{ input x id \"00000000-0000-0000-0000-000000000004\" : Number; result Number; body $x; }}\n{property} v \"V\" id \"00000000-0000-0000-0000-000000000002\" {{ subject @w; type Number; base @c(x: 1); }}\n{work} w \"W\" id \"00000000-0000-0000-0000-000000000005\" {{}}"
|
||||
);
|
||||
let original = bundle(&body);
|
||||
let owner = held(original.clone(), 4096);
|
||||
for carried in [false, true] {
|
||||
let seed = parse_text_producers(owner.input()).unwrap();
|
||||
let input = if carried {
|
||||
owner.input()
|
||||
} else {
|
||||
CarriedInput::borrowed(&owner.bundle, &seed)
|
||||
};
|
||||
let syntax = parse_entry_producers(input).unwrap();
|
||||
let input = if carried {
|
||||
owner.input()
|
||||
} else {
|
||||
CarriedInput::borrowed(&owner.bundle, &seed)
|
||||
};
|
||||
let prior = parse_text_producers(input).unwrap();
|
||||
assert_eq!(syntax.budget.usage, prior.budget.usage);
|
||||
assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units());
|
||||
assert_eq!(syntax.metadata, prior.metadata);
|
||||
assert_eq!(syntax.budget.producer_text(), prior.budget.producer_text());
|
||||
assert_eq!(syntax.budget.producer_entries(), Some(syntax.metadata));
|
||||
let result = project::resolve(syntax).unwrap();
|
||||
let prior = project::resolve(prior).unwrap();
|
||||
assert_eq!(result.project(), prior.project());
|
||||
assert_eq!(result.warnings(), prior.warnings());
|
||||
assert_eq!(result.phase_state().1, prior.phase_state().1);
|
||||
}
|
||||
assert_eq!(owner.bundle, original);
|
||||
}
|
||||
// Text-only and phase entry acceptance retain their old checkpoints.
|
||||
for cap in [12, 11] {
|
||||
let owner = held(bundle(""), cap);
|
||||
let seed = parse_with_profile(
|
||||
&owner.bundle,
|
||||
owner.policy.project_limits,
|
||||
owner.policy.limits,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(seed.budget.producer_entries().is_none());
|
||||
let result = parse_entry_producers(CarriedInput::borrowed(&owner.bundle, &seed));
|
||||
if cap == 12 {
|
||||
assert_eq!(result.unwrap().metadata, 11);
|
||||
} else {
|
||||
diagnostic(&failed(result), "main.arcadia", 0, 1);
|
||||
}
|
||||
}
|
||||
let low = held(
|
||||
bundle("oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { }"),
|
||||
4,
|
||||
);
|
||||
diagnostic(
|
||||
&failed(parse_entry_producers(low.input())),
|
||||
"main.arcadia",
|
||||
75,
|
||||
76,
|
||||
);
|
||||
diagnostic(
|
||||
&failed(parse_text_producers(low.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
);
|
||||
diagnostic(&failed(parse_carried(low.input())), "main.arcadia", 0, 1);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Real public-constructor wiring; arbitrary fixture profiles are not public policy.
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn public_read_fresh_constructor_keeps_zero_carry_both_modes_and_exact_policy() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: [("main.arcadia".into(), "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n".into())].into(),
|
||||
};
|
||||
let original = bundle.clone();
|
||||
for (source_bytes, references, expected_bytes, expected_tokens, expected_references) in [
|
||||
(4096, 17, 4096, 4096, 17),
|
||||
(usize::MAX, usize::MAX, 64 * 1024 * 1024, 1_048_576, 500_000),
|
||||
] {
|
||||
let limits = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 9,
|
||||
max_include_edges: 11,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: source_bytes,
|
||||
max_graph_objects: 3,
|
||||
max_records: 1,
|
||||
max_step_references: references,
|
||||
},
|
||||
};
|
||||
// This is the same production helper the exported checker calls.
|
||||
let syntax = parse_fresh(&bundle, limits).unwrap();
|
||||
let guard = syntax.carried.as_ref().expect("explicit carried mode");
|
||||
assert!(matches!(guard.backing, GuardBacking::Borrowed { .. }));
|
||||
assert_eq!(guard.slots(), Slots::default());
|
||||
assert_eq!(syntax.budget.producer_text(), Some((8, 0)));
|
||||
assert_eq!(syntax.budget.producer_entries(), Some(11));
|
||||
assert_eq!(syntax.metadata, 11);
|
||||
let profile = syntax.budget.limits();
|
||||
assert_eq!(profile.bytes, expected_bytes);
|
||||
assert_eq!(profile.tokens, expected_tokens);
|
||||
assert_eq!(profile.entries, 262_144);
|
||||
assert_eq!(profile.references, expected_references);
|
||||
assert_eq!(profile.frames, 256);
|
||||
assert_eq!(profile.numeric_units, 50_000_000);
|
||||
assert_eq!(syntax.limits.max_files, 7);
|
||||
assert_eq!(syntax.limits.max_include_depth, 9);
|
||||
assert_eq!(syntax.limits.max_include_edges, 11);
|
||||
assert_eq!(syntax.limits.model.max_source_bytes, source_bytes);
|
||||
assert_eq!(syntax.limits.model.max_graph_objects, 3);
|
||||
assert_eq!(syntax.limits.model.max_records, 1);
|
||||
assert_eq!(syntax.limits.model.max_step_references, references);
|
||||
let prior = parse(&bundle, limits).unwrap();
|
||||
assert_eq!(syntax.budget.usage, prior.budget.usage);
|
||||
assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units());
|
||||
let resolved = project::resolve(syntax).unwrap();
|
||||
let prior = project::resolve(prior).unwrap();
|
||||
let checked = project::check_mixed_bundle(&bundle, limits).unwrap();
|
||||
let mut expected = crate::Project::empty();
|
||||
expected.id = Id::from_u128(1);
|
||||
expected.alias = "p".into();
|
||||
expected.name = "Project".into();
|
||||
assert_eq!(resolved.project(), &expected);
|
||||
assert_eq!(prior.project(), &expected);
|
||||
assert_eq!(checked.project(), &expected);
|
||||
assert!(checked.warnings().is_empty());
|
||||
assert_eq!(resolved.warnings(), prior.warnings());
|
||||
}
|
||||
assert_eq!(bundle, original);
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Usage;
|
||||
fn doc(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
)
|
||||
}
|
||||
fn empty() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(""))]),
|
||||
}
|
||||
}
|
||||
fn chain() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), doc("include \"part.arcadia\";")),
|
||||
(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
),
|
||||
("last.arcadia".into(), "//é\r\n".into()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
// Fixture-only policies may deliberately be too tight to admit the target.
|
||||
// These are validated profiles, not a production old-owner transition.
|
||||
fn fixture_policy(limits: Limits, project_limits: ProjectLimits) -> SnapshotPolicy {
|
||||
SnapshotPolicy {
|
||||
limits: Budget::new(limits).unwrap().limits(),
|
||||
project_limits,
|
||||
}
|
||||
}
|
||||
fn owner(bundle: SourceBundle, limits: Limits) -> RetainedSnapshot {
|
||||
RetainedSnapshot::fixture(bundle, fixture_policy(limits, ProjectLimits::default())).unwrap()
|
||||
}
|
||||
fn parse_failure(result: Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>) -> SourceDiagnostic {
|
||||
let errors = match result {
|
||||
Ok(_) => panic!("unexpected syntax success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(errors.len(), 1);
|
||||
errors.into_iter().next().unwrap()
|
||||
}
|
||||
fn resolve_failure(syntax: ParsedMixedSyntax<'_>) -> SourceDiagnostic {
|
||||
let failure = match project::resolve(syntax) {
|
||||
Ok(_) => panic!("unexpected resolution success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
failure.diagnostics.into_iter().next().unwrap()
|
||||
}
|
||||
fn diagnostic(d: &SourceDiagnostic, path: &str, start: usize, end: usize, message: &str) {
|
||||
assert_eq!(d.path, path);
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert_eq!(d.diagnostic.message, message);
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
fn expected_project() -> crate::Project {
|
||||
crate::Project {
|
||||
schema_version: 8,
|
||||
id: Id::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
alias: "p".into(),
|
||||
name: "Project".into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn real_retained_bundles_keep_exact_guard_work_and_once_only_resolved_baseline() {
|
||||
for (bundle, c, u, m, live, entries, bytes) in [
|
||||
(
|
||||
empty(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 102,
|
||||
},
|
||||
Usage {
|
||||
bytes: 78,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0,
|
||||
},
|
||||
11,
|
||||
Slots {
|
||||
entries: 13,
|
||||
occurrences: 0,
|
||||
text: 110,
|
||||
},
|
||||
17,
|
||||
112,
|
||||
),
|
||||
(
|
||||
chain(),
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: 180,
|
||||
},
|
||||
Usage {
|
||||
bytes: 132,
|
||||
tokens: 31,
|
||||
entries: 3,
|
||||
references: 0,
|
||||
deepest_frame: 0,
|
||||
},
|
||||
19,
|
||||
Slots {
|
||||
entries: 25,
|
||||
occurrences: 0,
|
||||
text: 212,
|
||||
},
|
||||
29,
|
||||
214,
|
||||
),
|
||||
] {
|
||||
let owner = owner(bundle, profile(entries, bytes));
|
||||
assert_eq!(owner.retained, c);
|
||||
let original = owner.bundle.clone();
|
||||
let parsed = parse_carried(owner.input()).unwrap();
|
||||
assert_eq!(parsed.metadata, m);
|
||||
assert_eq!(parsed.budget.usage, u);
|
||||
assert_eq!(parsed.budget.numeric_units(), 0);
|
||||
assert!(std::ptr::eq(parsed.bundle, &owner.bundle));
|
||||
assert!(
|
||||
matches!(&parsed.carried.as_ref().unwrap().backing,GuardBacking::Retained(retained) if std::ptr::eq(*retained,&owner))
|
||||
);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
assert_eq!(resolved.project(), &expected_project());
|
||||
assert!(resolved.warnings().is_empty());
|
||||
let (syntax, stored) = resolved.phase_state();
|
||||
assert_eq!(stored, live);
|
||||
assert_eq!(syntax.budget.usage, u);
|
||||
assert_eq!(syntax.carried.as_ref().unwrap().slots(), c);
|
||||
assert_eq!(
|
||||
syntax.files.iter().map(|f| f.path).collect::<Vec<_>>(),
|
||||
original
|
||||
.files
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
drop(resolved);
|
||||
assert_eq!(owner.bundle, original);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_combined_syntax_caps_and_canonical_failure_files() {
|
||||
for (bundle, cap, path) in [(empty(), 13, "main.arcadia"), (chain(), 25, "part.arcadia")] {
|
||||
let pass = owner(bundle.clone(), profile(cap, 1024));
|
||||
drop(parse_carried(pass.input()).unwrap());
|
||||
let fail = owner(bundle, profile(cap - 1, 1024));
|
||||
let d = parse_failure(parse_carried(fail.input()));
|
||||
diagnostic(&d, path, 0, 1, "Source8 lowering entry limit exceeded.");
|
||||
assert_eq!((d.diagnostic.line, d.diagnostic.column), (1, 1));
|
||||
}
|
||||
let owner = owner(chain(), profile(20, 1024));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(owner.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn legacy_zero_timing_is_preserved_but_explicit_zero_carry_still_combines() {
|
||||
let bundle = empty();
|
||||
let original =
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), profile(11, 1024)).unwrap();
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(CarriedInput::borrowed(&bundle, &original))),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
diagnostic(
|
||||
&resolve_failure(original),
|
||||
"main.arcadia",
|
||||
0,
|
||||
0,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
let original =
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), profile(12, 1024)).unwrap();
|
||||
let carried = parse_carried(CarriedInput::borrowed(&bundle, &original)).unwrap();
|
||||
assert_eq!(carried.budget.usage, original.budget.usage);
|
||||
assert_eq!(carried.metadata, 11);
|
||||
}
|
||||
#[test]
|
||||
fn resolver_entry_and_catalog_text_caps_include_c_once_after_parser_success() {
|
||||
for (bundle, entries, bytes) in [(empty(), 17, 112), (chain(), 29, 214)] {
|
||||
for limits in [profile(entries - 1, bytes), profile(entries, bytes - 1)] {
|
||||
let held = owner(bundle.clone(), limits);
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
"main.arcadia",
|
||||
11,
|
||||
73,
|
||||
if limits.entries == entries - 1 {
|
||||
"Source8 lowering entry limit exceeded."
|
||||
} else {
|
||||
"Source8 index/output byte limit exceeded."
|
||||
},
|
||||
);
|
||||
drop(
|
||||
project::resolve(
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), limits).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn syntax_text_accounting_is_after_decoding_and_has_precise_original_location() {
|
||||
for (bundle, bytes, path, start, end) in [
|
||||
(empty(), 109, "main.arcadia", 11, 73),
|
||||
(chain(), 211, "part.arcadia", 0, 23),
|
||||
] {
|
||||
let held = owner(bundle, profile(100, bytes));
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
assert_eq!(
|
||||
parsed
|
||||
.files
|
||||
.iter()
|
||||
.find_map(|f| f.ordinary.header.as_ref())
|
||||
.unwrap()
|
||||
.project
|
||||
.name,
|
||||
"Project"
|
||||
);
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
path,
|
||||
start,
|
||||
end,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn initial_c_and_file_slots_are_checked_before_traversal() {
|
||||
for (bundle, cap) in [(empty(), 1), (chain(), 5)] {
|
||||
let held = owner(bundle, profile(cap, 1024));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
}
|
||||
for (cap, code) in [(1, "invalid_source"), (2, "project_limit")] {
|
||||
let held = RetainedSnapshot::fixture(
|
||||
empty(),
|
||||
fixture_policy(
|
||||
profile(cap, 1024),
|
||||
ProjectLimits {
|
||||
max_files: 0,
|
||||
..ProjectLimits::default()
|
||||
},
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parse_failure(parse_carried(held.input())).diagnostic.code,
|
||||
code
|
||||
);
|
||||
}
|
||||
let held = owner(empty(), profile(100, 101));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
let held = owner(empty(), profile(100, 102));
|
||||
drop(parse_carried(held.input()).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn explicit_fault_injections_cover_occurrence_caps_and_unreachable_overflows() {
|
||||
let mut held = owner(empty(), profile(100, 1024));
|
||||
let maximum = held.policy.limits.entries + held.policy.limits.references;
|
||||
// Not real bundle ownership: only a fault-injected dimension check.
|
||||
held.retained.occurrences = maximum;
|
||||
drop(parse_carried(held.input()).unwrap());
|
||||
held.retained.occurrences = maximum + 1;
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering occurrence limit exceeded.",
|
||||
);
|
||||
held.retained = Slots {
|
||||
entries: usize::MAX,
|
||||
occurrences: 0,
|
||||
text: 0,
|
||||
};
|
||||
let budget = Budget::new(profile(100, 1024)).unwrap();
|
||||
// Valid hard limits reject C first: direct injection reaches arithmetic.
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.into_parts()
|
||||
.2
|
||||
.check(&budget, 0, 1)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
held.retained = Slots::default();
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.into_parts()
|
||||
.2
|
||||
.check(&budget, usize::MAX, 1)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
assert_eq!(
|
||||
Slots::text(usize::MAX)
|
||||
.add(Slots::text(1), Span::default())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
let mut ledger = Ledger {
|
||||
live: Slots::entries(usize::MAX),
|
||||
};
|
||||
assert_eq!(
|
||||
ledger
|
||||
.reserve(&budget, Slots::entries(1), Span::default())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
}
|
||||
|
||||
fn same_policy(syntax: &ParsedMixedSyntax<'_>, policy: &SnapshotPolicy) {
|
||||
let a = syntax.budget.limits();
|
||||
let b = policy.limits;
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[b.bytes, b.tokens, b.entries, b.references, b.frames]
|
||||
);
|
||||
assert_eq!(a.numeric_units, b.numeric_units);
|
||||
let a = syntax.limits;
|
||||
let b = policy.project_limits;
|
||||
assert_eq!(
|
||||
[
|
||||
a.max_files,
|
||||
a.max_include_depth,
|
||||
a.max_include_edges,
|
||||
a.model.max_source_bytes,
|
||||
a.model.max_graph_objects,
|
||||
a.model.max_records,
|
||||
a.model.max_step_references
|
||||
],
|
||||
[
|
||||
b.max_files,
|
||||
b.max_include_depth,
|
||||
b.max_include_edges,
|
||||
b.model.max_source_bytes,
|
||||
b.model.max_graph_objects,
|
||||
b.model.max_records,
|
||||
b.model.max_step_references
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn exact_policy_capture_preserves_every_field_and_starts_fresh_snapshot_work() {
|
||||
let base = empty();
|
||||
let next = chain();
|
||||
let limits = Limits {
|
||||
bytes: 4096,
|
||||
tokens: 2048,
|
||||
entries: 1024,
|
||||
references: 128,
|
||||
frames: 64,
|
||||
numeric_units: 4096,
|
||||
};
|
||||
let caller = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 5,
|
||||
max_include_edges: 9,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 8192,
|
||||
max_graph_objects: 10,
|
||||
max_records: 30,
|
||||
max_step_references: 256,
|
||||
},
|
||||
};
|
||||
let seed = parse_with_profile(&base, caller, limits).unwrap();
|
||||
assert_eq!(seed.budget.usage.bytes, 78);
|
||||
let input = CarriedInput::borrowed(&next, &seed);
|
||||
let captured = SnapshotPolicy::captured(&seed);
|
||||
drop(seed);
|
||||
let parsed = parse_carried(input).unwrap();
|
||||
same_policy(&parsed, &captured);
|
||||
assert_eq!(
|
||||
parsed.budget.usage,
|
||||
Usage {
|
||||
bytes: 132,
|
||||
tokens: 31,
|
||||
entries: 3,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(parsed.budget.numeric_units(), 0);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
same_policy(resolved.phase_state().0, &captured);
|
||||
}
|
||||
#[test]
|
||||
fn inherited_byte_token_and_traversal_limits_reach_the_real_parser() {
|
||||
let seed_bundle = empty();
|
||||
let bundle = chain();
|
||||
for (bytes, tokens, success) in [(132, 31, true), (131, 31, false), (132, 30, false)] {
|
||||
let limits = Limits {
|
||||
tokens,
|
||||
..profile(100, bytes)
|
||||
};
|
||||
let seed = parse_with_profile(&seed_bundle, ProjectLimits::default(), limits).unwrap();
|
||||
let result = parse_carried(CarriedInput::borrowed(&bundle, &seed));
|
||||
if success {
|
||||
drop(project::resolve(result.unwrap()).unwrap());
|
||||
} else {
|
||||
let d = parse_failure(result);
|
||||
if bytes == 131 {
|
||||
diagnostic(
|
||||
&d,
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
} else {
|
||||
diagnostic(&d, "last.arcadia", 5, 6, "Source8 tokens limit exceeded.");
|
||||
assert_eq!((d.diagnostic.line, d.diagnostic.column), (1, 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (files, depth, edges, success) in [
|
||||
(3, 2, 2, true),
|
||||
(2, 2, 2, false),
|
||||
(3, 1, 2, false),
|
||||
(3, 2, 1, false),
|
||||
] {
|
||||
let caller = ProjectLimits {
|
||||
max_files: files,
|
||||
max_include_depth: depth,
|
||||
max_include_edges: edges,
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let seed = parse_with_profile(&seed_bundle, caller, profile(100, 1024)).unwrap();
|
||||
let result = parse_carried(CarriedInput::borrowed(&bundle, &seed));
|
||||
if success {
|
||||
drop(project::resolve(result.unwrap()).unwrap());
|
||||
} else {
|
||||
let d = parse_failure(result);
|
||||
assert_eq!(d.diagnostic.code, "project_limit");
|
||||
if files == 2 {
|
||||
assert_eq!(d.path, "main.arcadia");
|
||||
} else {
|
||||
assert_eq!(d.path, "part.arcadia");
|
||||
assert_eq!(
|
||||
d.diagnostic.span,
|
||||
Span {
|
||||
start: 0,
|
||||
end: if edges == 1 { 1 } else { 23 }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn typed(expression: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
doc(&format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000002\" {{ input x id \"00000000-0000-0000-0000-000000000003\" : Number; result Number; body {expression}; }}"
|
||||
)),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn captured_numeric_reference_frame_and_model_policies_are_behavioral_not_just_copied() {
|
||||
let base = empty();
|
||||
for (target, limits, expected) in [
|
||||
(
|
||||
typed("1"),
|
||||
Limits {
|
||||
numeric_units: 72,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"NumericUnits",
|
||||
),
|
||||
(
|
||||
typed("$x"),
|
||||
Limits {
|
||||
references: 0,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"Source8 references limit exceeded.",
|
||||
),
|
||||
(
|
||||
typed("abs(1)"),
|
||||
Limits {
|
||||
frames: 0,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"Source8 syntax frames limit exceeded.",
|
||||
),
|
||||
] {
|
||||
let seed = parse_with_profile(&base, ProjectLimits::default(), limits).unwrap();
|
||||
let d = parse_failure(parse_carried(CarriedInput::borrowed(&target, &seed)));
|
||||
assert!(d.diagnostic.message.contains(expected), "{:?}", d);
|
||||
}
|
||||
for expression in ["1", "$x", "abs(1)"] {
|
||||
let target = typed(expression);
|
||||
let seed =
|
||||
parse_with_profile(&base, ProjectLimits::default(), profile(1000, 4096)).unwrap();
|
||||
drop(
|
||||
project::resolve(parse_carried(CarriedInput::borrowed(&target, &seed)).unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
// Nonzero old numeric work must not leak into the second snapshot.
|
||||
let one = typed("1");
|
||||
let limits = Limits {
|
||||
numeric_units: 73,
|
||||
..profile(1000, 4096)
|
||||
};
|
||||
let seed = parse_with_profile(&one, ProjectLimits::default(), limits).unwrap();
|
||||
assert_eq!(seed.budget.numeric_units(), 73);
|
||||
let parsed = parse_carried(CarriedInput::borrowed(&one, &seed)).unwrap();
|
||||
assert_eq!(parsed.budget.numeric_units(), 73);
|
||||
assert_eq!(parsed.budget.usage, seed.budget.usage);
|
||||
for (graph, records, steps) in [(0, 10, 10), (10, 1, 10), (10, 10, 0)] {
|
||||
let caller = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_graph_objects: graph,
|
||||
max_records: records,
|
||||
max_step_references: steps,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let seed = parse_with_profile(
|
||||
&base,
|
||||
caller,
|
||||
Limits::source8(ParseLimits {
|
||||
max_step_references: steps,
|
||||
..ParseLimits::UNLIMITED
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let target = if steps == 0 { typed("$x") } else { typed("1") };
|
||||
let errors = parse_carried(CarriedInput::borrowed(&target, &seed));
|
||||
if steps == 0 {
|
||||
assert!(
|
||||
parse_failure(errors)
|
||||
.diagnostic
|
||||
.message
|
||||
.contains("references")
|
||||
);
|
||||
} else {
|
||||
assert!(parse_failure(errors).diagnostic.message.contains("limit"));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn zero_carried_mixed_oa_sa_unicode_results_and_index_ownership_match_legacy() {
|
||||
for (activity, property, layer) in [
|
||||
("oa_activity", "oa_property", "Oa"),
|
||||
("sa_function", "sa_property", "Sa"),
|
||||
] {
|
||||
let body = format!(
|
||||
"node n id \"00000000-0000-0000-0000-000000000004\" {{ diagram @d; element @v; }}\n{property} v \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @w; type Number; base 1; }}\n{activity} w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram d \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ layer {layer}; }}"
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
format!("// 雨\r\n{}", doc(&body).replace('\n', "\r\n")),
|
||||
)]),
|
||||
};
|
||||
let legacy = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
let carried = parse_carried(CarriedInput::borrowed(&bundle, &legacy)).unwrap();
|
||||
assert_eq!(legacy.budget.usage, carried.budget.usage);
|
||||
assert_eq!(
|
||||
legacy.budget.numeric_units(),
|
||||
carried.budget.numeric_units()
|
||||
);
|
||||
assert_eq!(legacy.metadata, carried.metadata);
|
||||
let legacy = project::resolve(legacy).unwrap();
|
||||
let carried = project::resolve(carried).unwrap();
|
||||
assert_eq!(legacy.project(), carried.project());
|
||||
assert_eq!(legacy.warnings(), carried.warnings());
|
||||
assert_eq!(legacy.phase_state().1, carried.phase_state().1);
|
||||
assert!(carried.phase_state().0.carried.is_some());
|
||||
let legacy = legacy.index().unwrap().index_ordinary().unwrap();
|
||||
let carried = carried.index().unwrap().index_ordinary().unwrap();
|
||||
assert_eq!(legacy.names(), carried.names());
|
||||
assert_eq!(legacy.typed_occurrences(), carried.typed_occurrences());
|
||||
assert_eq!(
|
||||
legacy.ordinary_occurrences(),
|
||||
carried.ordinary_occurrences()
|
||||
);
|
||||
assert!(std::ptr::eq(carried.bundle(), &bundle));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_position_key_copy_is_counted_at_existing_postparse_text_phase() {
|
||||
let source = doc(
|
||||
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"é\"; }",
|
||||
);
|
||||
let start = source.find("description \"é\";").unwrap();
|
||||
let end = start + "description \"é\";".len();
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), source)]),
|
||||
};
|
||||
let retained_text = bundle.files["main.arcadia"].len() + 24;
|
||||
// Existing text metric: project8 + alias/name2 + field-span key11 +
|
||||
// AST field key11 + decoded string2 + projected field key11 =45.
|
||||
// The final11-byte key is copied before resolve accounts this phase.
|
||||
let held = owner(bundle, profile(1000, retained_text + 44));
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
assert_eq!(parsed.files[0].positions.len(), 1);
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
"main.arcadia",
|
||||
start,
|
||||
end,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emission_refuses_changed_carried_even_zero_and_preserves_noop_guards() {
|
||||
for retained in [false, true] {
|
||||
let held = owner(empty(), profile(100, 4096));
|
||||
let seed =
|
||||
parse_with_profile(&held.bundle, ProjectLimits::default(), profile(100, 4096)).unwrap();
|
||||
for changed in [false, true] {
|
||||
let input = if retained {
|
||||
held.input()
|
||||
} else {
|
||||
CarriedInput::borrowed(&held.bundle, &seed)
|
||||
};
|
||||
let parsed = parse_carried(input).unwrap();
|
||||
let original_guard = parsed.carried.as_ref().unwrap().slots();
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
let source = resolved.index().unwrap().index_ordinary().unwrap();
|
||||
let mut desired = source.project().clone();
|
||||
if changed {
|
||||
desired.alias = "next".into();
|
||||
}
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
if changed {
|
||||
let error = match plan.emit_changed_unverified(&held.bundle) {
|
||||
Ok(_) => panic!("carried changed emission must refuse"),
|
||||
Err(e) => e,
|
||||
};
|
||||
diagnostic(
|
||||
&error.diagnostics[0],
|
||||
"main.arcadia",
|
||||
0,
|
||||
0,
|
||||
"Changed emission requires original legacy syntax.",
|
||||
);
|
||||
} else {
|
||||
let source = plan.into_unchanged(&held.bundle).unwrap();
|
||||
assert!(std::ptr::eq(source.bundle(), &held.bundle));
|
||||
// The intact owner can be consumed through another no-op plan;
|
||||
// no production getter exposes or replaces its guard.
|
||||
let source = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.into_unchanged(&held.bundle)
|
||||
.unwrap();
|
||||
assert!(std::ptr::eq(source.bundle(), &held.bundle));
|
||||
let mut changed_desired = desired.clone();
|
||||
changed_desired.alias = "next".into();
|
||||
assert!(
|
||||
source
|
||||
.plan_rename(&changed_desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&held.bundle)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
original_guard,
|
||||
if retained {
|
||||
held.retained
|
||||
} else {
|
||||
Slots::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(held.bundle.files["main.arcadia"], doc(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
//! Independent literal/storage oracles; expected text is not generated by parsing.
|
||||
use super::*;
|
||||
use crate::language::raw_tokens::{Class as RawClass, Scanner, StringEvent};
|
||||
fn doc(body: &str, name: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"{name}\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
)
|
||||
}
|
||||
fn bundle(body: &str, name: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(body, name))]),
|
||||
}
|
||||
}
|
||||
fn held(bundle: SourceBundle, entries: usize, bytes: usize) -> RetainedSnapshot {
|
||||
RetainedSnapshot::fixture(
|
||||
bundle,
|
||||
SnapshotPolicy {
|
||||
limits: Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
},
|
||||
project_limits: ProjectLimits::default(),
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
fn failed(result: Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>) -> SourceDiagnostic {
|
||||
let errors = match result {
|
||||
Ok(_) => panic!("unexpected syntax success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(errors.len(), 1);
|
||||
errors.into_iter().next().unwrap()
|
||||
}
|
||||
fn expected(name: &str, description: &str) -> crate::Project {
|
||||
crate::Project {
|
||||
schema_version: 8,
|
||||
id: Id::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
alias: "p".into(),
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn exact_real_owner_empty_and_escaped_peaks_reconcile_once() {
|
||||
let escaped = "\\u0061".repeat(20);
|
||||
for (b, entries, bytes, c, t, model) in [
|
||||
(
|
||||
bundle("", "Project"),
|
||||
17,
|
||||
146,
|
||||
102,
|
||||
8,
|
||||
expected("Project", ""),
|
||||
),
|
||||
(
|
||||
bundle(&format!("description \"{escaped}\";"), &escaped),
|
||||
21,
|
||||
491,
|
||||
350,
|
||||
41,
|
||||
expected(&"a".repeat(20), &"a".repeat(20)),
|
||||
),
|
||||
] {
|
||||
for cap in [bytes, bytes - 1] {
|
||||
let owner = held(b.clone(), entries, cap);
|
||||
assert_eq!(owner.retained.text, c);
|
||||
let result = parse_text_producers(owner.input());
|
||||
if cap == bytes {
|
||||
let parsed = result.unwrap();
|
||||
assert_eq!(parsed.budget.producer_text(), Some((t, 0)));
|
||||
let usage = parsed.budget.usage;
|
||||
let numeric = parsed.budget.numeric_units();
|
||||
assert_eq!(
|
||||
project::text_producer_inventory(&parsed).unwrap().text,
|
||||
c + t
|
||||
);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
assert_eq!(resolved.project(), &model);
|
||||
assert_eq!(resolved.phase_state().1.text, c + t);
|
||||
assert_eq!(resolved.phase_state().0.budget.usage, usage);
|
||||
assert_eq!(resolved.phase_state().0.budget.numeric_units(), numeric);
|
||||
let legacy =
|
||||
project::resolve(parse(&owner.bundle, ProjectLimits::default()).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(legacy.project(), &model);
|
||||
assert_eq!(legacy.phase_state().0.budget.usage, usage);
|
||||
} else {
|
||||
let e = failed(result);
|
||||
assert_eq!(e.path, "main.arcadia");
|
||||
assert!(e.diagnostic.message.contains("byte limit exceeded"));
|
||||
let start = if c == 102 {
|
||||
34
|
||||
} else {
|
||||
b.files["main.arcadia"].find("description \"").unwrap() + 12
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostic.span,
|
||||
Span {
|
||||
start,
|
||||
end: start + 1
|
||||
}
|
||||
);
|
||||
}
|
||||
assert_eq!(owner.bundle, b);
|
||||
}
|
||||
}
|
||||
let owner = held(bundle("", "Project"), 17, 112);
|
||||
let syntax = parse_carried(owner.input()).unwrap();
|
||||
assert!(syntax.budget.producer_text().is_none());
|
||||
assert_eq!(project::resolve(syntax).unwrap().phase_state().1.text, 110);
|
||||
}
|
||||
fn multi() -> SourceBundle {
|
||||
let mut b = bundle("include \"part.arcadia\";", &"a".repeat(20));
|
||||
b.files.insert("part.arcadia".into(),"include \"\\u006c\\u0061\\u0073\\u0074\\u002e\\u0061\\u0072\\u0063\\u0061\\u0064\\u0069\\u0061\";\r\n".into());
|
||||
b.files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
b
|
||||
}
|
||||
#[test]
|
||||
fn multifile_retained_text_overlaps_scanner_and_preserves_complete_result() {
|
||||
for cap in [358, 357] {
|
||||
let owner = held(multi(), 29, cap);
|
||||
assert_eq!(
|
||||
owner.retained,
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: 253
|
||||
}
|
||||
);
|
||||
let result = parse_text_producers(owner.input());
|
||||
if cap == 358 {
|
||||
let parsed = result.unwrap();
|
||||
assert_eq!(parsed.budget.producer_text(), Some((45, 0)));
|
||||
assert_eq!(
|
||||
(
|
||||
parsed.budget.usage.bytes,
|
||||
parsed.budget.usage.tokens,
|
||||
parsed.budget.usage.entries
|
||||
),
|
||||
(205, 31, 3)
|
||||
);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
assert_eq!(
|
||||
resolved.phase_state().1,
|
||||
Slots {
|
||||
entries: 25,
|
||||
occurrences: 0,
|
||||
text: 298
|
||||
}
|
||||
);
|
||||
assert_eq!(resolved.project(), &expected(&"a".repeat(20), ""));
|
||||
} else {
|
||||
let e = failed(result);
|
||||
assert_eq!(e.path, "part.arcadia");
|
||||
assert_eq!(e.diagnostic.span, Span { start: 8, end: 9 });
|
||||
assert!(e.diagnostic.message.contains("byte limit exceeded"));
|
||||
}
|
||||
}
|
||||
// Arithmetic seam replay of independent C253/T33, not owner provenance.
|
||||
// No grammar runs here, so this refusal specifically proves scanner timing.
|
||||
let b = multi();
|
||||
let mut budget = Budget::new(Limits {
|
||||
bytes: 357,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
})
|
||||
.unwrap();
|
||||
budget.enable_text_producers(253).unwrap();
|
||||
budget.own_text(33, Span::default()).unwrap();
|
||||
let e = lex::tokens(&b.files["part.arcadia"], &mut budget).unwrap_err();
|
||||
assert_eq!(e.span, Span { start: 8, end: 82 });
|
||||
assert_eq!(budget.producer_text(), Some((33, 0)));
|
||||
}
|
||||
#[test]
|
||||
fn projection_and_three_uuid_consumers_reconcile_nested_copy_inventory() {
|
||||
let activity = "oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"dddddddddddddddddddddddddddddddddddddddd\"; }";
|
||||
for cap in [304, 303] {
|
||||
let owner = held(bundle(activity, "Project"), 1000, cap);
|
||||
assert_eq!(owner.retained.text, 221);
|
||||
let result = parse_text_producers(owner.input());
|
||||
if cap == 304 {
|
||||
let syntax = result.unwrap();
|
||||
assert_eq!(syntax.budget.producer_text(), Some((83, 0)));
|
||||
assert_eq!(project::text_producer_inventory(&syntax).unwrap().text, 304);
|
||||
} else {
|
||||
assert_eq!(
|
||||
failed(result).diagnostic.span,
|
||||
Span {
|
||||
start: 137,
|
||||
end: 138
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
let body = "oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { payload { ref @p; old uuid \"00000000-0000-0000-0000-000000000001\"; }; }\ncalculation c \"C\" id \"00000000-0000-0000-0000-000000000003\" { input x id \"00000000-0000-0000-0000-000000000001\" : Number; result Number; body $x; }";
|
||||
for cap in [453, 452] {
|
||||
let owner = held(bundle(body, "Project"), 1000, cap);
|
||||
assert_eq!(owner.retained.text, 383);
|
||||
let result = parse_text_producers(owner.input());
|
||||
if cap == 453 {
|
||||
let syntax = result.unwrap();
|
||||
assert_eq!(syntax.budget.producer_text(), Some((49, 0)));
|
||||
assert_eq!(project::text_producer_inventory(&syntax).unwrap().text, 432);
|
||||
// Arbitrary payload is deliberately syntax-only, not model admission.
|
||||
} else {
|
||||
assert_eq!(failed(result).diagnostic.span.start, 282);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_bounds_hook_order_refund_and_invalid_legacy_precedence() {
|
||||
for (raw, decoded, payload) in [
|
||||
(r#""""#, "", 0),
|
||||
(r#""a""#, "a", 1),
|
||||
(r#""é""#, "é", 2),
|
||||
(r#""\\""#, "\\", 2),
|
||||
(r#""\n""#, "\n", 2),
|
||||
(r#""\u0061""#, "a", 6),
|
||||
(r#""\u96ea""#, "雪", 6),
|
||||
(r#""\uD83D\uDE00""#, "😀", 12),
|
||||
] {
|
||||
let mut events = Vec::new();
|
||||
let token = Scanner::new(raw)
|
||||
.next_with_strings(|event| {
|
||||
events.push(event);
|
||||
Ok::<(), ()>(())
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(token.class, RawClass::String);
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
StringEvent::Reserve {
|
||||
span: Span {
|
||||
start: 0,
|
||||
end: raw.len()
|
||||
},
|
||||
payload
|
||||
},
|
||||
StringEvent::Released { payload }
|
||||
]
|
||||
);
|
||||
// Arithmetic-only bounds; baseline100 is not actual-owner provenance.
|
||||
let mut budget = Budget::new(Limits {
|
||||
bytes: 100 + payload,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
})
|
||||
.unwrap();
|
||||
budget.enable_text_producers(100).unwrap();
|
||||
let value = budget.quoted_text(raw, token.span).unwrap();
|
||||
assert_eq!(value, decoded);
|
||||
assert_eq!(budget.producer_text(), Some((decoded.len(), 0)));
|
||||
let bytes = value.len();
|
||||
drop(value);
|
||||
budget.release_text(bytes);
|
||||
assert_eq!(budget.producer_text(), Some((0, 0)));
|
||||
if payload > 0 {
|
||||
let mut budget = Budget::new(Limits {
|
||||
bytes: 100 + payload - 1,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
})
|
||||
.unwrap();
|
||||
budget.enable_text_producers(100).unwrap();
|
||||
assert!(budget.quoted_text(raw, token.span).is_err());
|
||||
assert_eq!(budget.producer_text(), Some((0, 0)));
|
||||
}
|
||||
}
|
||||
for raw in [r#""\q""#, r#""\uD800""#, r#""\uDE00""#] {
|
||||
assert_eq!(Scanner::new(raw).next().unwrap().class, RawClass::Error);
|
||||
let mut events = Vec::new();
|
||||
let token = Scanner::new(raw)
|
||||
.next_with_strings(|e| {
|
||||
events.push(e);
|
||||
Ok::<(), ()>(())
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(token.class, RawClass::Error);
|
||||
assert_eq!(events.len(), 2);
|
||||
let mut calls = 0;
|
||||
let result = Scanner::new(raw).next_with_strings(|_| {
|
||||
calls += 1;
|
||||
Err::<(), _>("reserve refused")
|
||||
});
|
||||
assert_eq!(result.unwrap_err(), "reserve refused");
|
||||
assert_eq!(calls, 1);
|
||||
}
|
||||
let mut calls = 0;
|
||||
assert_eq!(
|
||||
Scanner::new("\"unclosed")
|
||||
.next_with_strings(|_| {
|
||||
calls += 1;
|
||||
Ok::<(), ()>(())
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.class,
|
||||
RawClass::Error
|
||||
);
|
||||
assert_eq!(calls, 0);
|
||||
let b = bundle(&format!("description \"{}\";", "\\q".repeat(60)), "Project");
|
||||
let bytes = b.files["main.arcadia"].len() + 24 + 119;
|
||||
let owner = held(b, 1000, bytes);
|
||||
assert!(
|
||||
failed(parse_text_producers(owner.input()))
|
||||
.diagnostic
|
||||
.message
|
||||
.contains("byte limit exceeded")
|
||||
);
|
||||
assert_eq!(
|
||||
failed(parse_carried(owner.input())).diagnostic.message,
|
||||
"Invalid source token."
|
||||
);
|
||||
assert_eq!(
|
||||
failed(parse(&owner.bundle, ProjectLimits::default()))
|
||||
.diagnostic
|
||||
.message,
|
||||
"Invalid source token."
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn explicit_zero_mode_complete_oa_sa_parity_and_checked_arithmetic() {
|
||||
for (work, property) in [
|
||||
("oa_activity", "oa_property"),
|
||||
("sa_function", "sa_property"),
|
||||
] {
|
||||
let body = format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000003\" {{ input x id \"00000000-0000-0000-0000-000000000004\" : Number; result Number; body $x; }}\n{property} v \"V\" id \"00000000-0000-0000-0000-000000000002\" {{ subject @w; type Number; base @c(x: 1); }}\n{work} w \"W\" id \"00000000-0000-0000-0000-000000000005\" {{}}"
|
||||
);
|
||||
let b = bundle(&body, "Project");
|
||||
let legacy = parse(&b, ProjectLimits::default()).unwrap();
|
||||
let usage = legacy.budget.usage;
|
||||
let numeric = legacy.budget.numeric_units();
|
||||
let syntax = parse_text_producers(CarriedInput::borrowed(&b, &legacy)).unwrap();
|
||||
let t = syntax.budget.producer_text().unwrap();
|
||||
assert_eq!(t.1, 0);
|
||||
assert_eq!(project::text_producer_inventory(&syntax).unwrap().text, t.0);
|
||||
let result = project::resolve(syntax).unwrap();
|
||||
let prior = project::resolve(legacy).unwrap();
|
||||
assert_eq!(result.project(), prior.project());
|
||||
assert_eq!(result.warnings(), prior.warnings());
|
||||
assert_eq!(result.phase_state().0.budget.usage, usage);
|
||||
assert_eq!(result.phase_state().0.budget.numeric_units(), numeric);
|
||||
assert_eq!(result.phase_state().1, prior.phase_state().1);
|
||||
}
|
||||
// Arithmetic faults, not fabricated large allocations or owner provenance.
|
||||
let mut budget = Budget::new(Limits::source8(ParseLimits::UNLIMITED)).unwrap();
|
||||
budget.enable_text_producers(1).unwrap();
|
||||
assert!(budget.own_text(usize::MAX, Span::default()).is_err());
|
||||
assert_eq!(budget.producer_text(), Some((0, 0)));
|
||||
assert!(
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| budget.release_text(1))).is_err()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn duplicate_descriptions_refuse_before_a_second_grammar_decode() {
|
||||
let text = "d".repeat(100);
|
||||
for (body, extra, message) in [
|
||||
(
|
||||
format!("description \"{text}\"; description \"{text}\";"),
|
||||
108,
|
||||
"Duplicate project description.",
|
||||
),
|
||||
(
|
||||
format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000002\" {{ description \"{text}\"; description \"{text}\"; }}"
|
||||
),
|
||||
110,
|
||||
"Duplicate field 'description'.",
|
||||
),
|
||||
] {
|
||||
let b = bundle(&body, "Project");
|
||||
// Independently counted C is source plus the two12-byte owned paths;
|
||||
// first description needs108/110, a second decoded copy would not fit.
|
||||
let bytes = b.files["main.arcadia"].len() + 24 + extra;
|
||||
let owner = held(b, 1000, bytes);
|
||||
let e = failed(parse_text_producers(owner.input()));
|
||||
assert_eq!(e.diagnostic.message, message);
|
||||
assert_eq!(
|
||||
failed(parse_carried(owner.input())).diagnostic.message,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Private complete-Project admission from the actual complete syntax witness.
|
||||
//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy.
|
||||
//! Complete-Project admission and opaque public read-only mixed source8 witness.
|
||||
//! The separate typed index/edit chain remains test-only; no replacement policy.
|
||||
use super::super::{
|
||||
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
|
||||
declarations as d, error, lower,
|
||||
@@ -11,23 +11,129 @@ use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
|
||||
use std::collections::BTreeMap;
|
||||
pub(in crate::language::analysis_source) mod accounting;
|
||||
use accounting::{Ledger, Slots, add};
|
||||
#[cfg(test)]
|
||||
pub(super) fn text_producer_inventory(syntax: &ParsedMixedSyntax<'_>) -> Result<Slots, Failure> {
|
||||
let mut ledger = Ledger {
|
||||
live: syntax
|
||||
.carried
|
||||
.as_ref()
|
||||
.map_or_else(Slots::default, |g| g.slots()),
|
||||
};
|
||||
accounting::syntax(syntax, &mut ledger)?;
|
||||
Ok(ledger.live)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(in crate::language::analysis_source) mod ordinary_index;
|
||||
|
||||
/// An admitted mixed source8 model borrowing its complete original source bundle.
|
||||
///
|
||||
/// This non-Clone result grants immutable validation queries only. It is not an
|
||||
/// editable source index, stabilized output, or permission to format/save a typed
|
||||
/// model through the ordinary source APIs. All input UUIDs must be explicit.
|
||||
///
|
||||
/// The source cannot be mutated while a later query uses the result:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
|
||||
/// let mut bundle = SourceBundle {
|
||||
/// entry: "main.arcadia".into(),
|
||||
/// files: [("main.arcadia".into(),
|
||||
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
|
||||
/// };
|
||||
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
/// bundle.files.clear();
|
||||
/// assert_eq!(checked.project().alias, "p");
|
||||
/// ```
|
||||
///
|
||||
/// Nor can the result outlive that source:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
|
||||
/// let checked = {
|
||||
/// let bundle = SourceBundle {
|
||||
/// entry: "main.arcadia".into(),
|
||||
/// files: [("main.arcadia".into(),
|
||||
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
|
||||
/// };
|
||||
/// check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap()
|
||||
/// };
|
||||
/// assert_eq!(checked.project().alias, "p");
|
||||
/// ```
|
||||
pub struct CheckedMixed<'s> {
|
||||
resolved: ResolvedMixedProject<'s>,
|
||||
}
|
||||
impl CheckedMixed<'_> {
|
||||
/// The complete admitted model, without mutation or ownership extraction.
|
||||
pub fn project(&self) -> &Project {
|
||||
self.resolved.project()
|
||||
}
|
||||
/// Actual located non-error validation diagnostics in validation order.
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
self.resolved.warnings()
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a complete mixed ordinary/typed source8 bundle without changing bytes.
|
||||
///
|
||||
/// Every project, ordinary/typed declaration and typed parameter requires an
|
||||
/// explicit UUID. Supplied files must be canonical and reachable from the entry.
|
||||
/// Errors return no partial admitted model. There is no migration, ID generation,
|
||||
/// filesystem discovery, formatting or editable-index construction.
|
||||
///
|
||||
/// Caller policy is applied across the entire bundle with the existing bounded
|
||||
/// source8 syntax/storage/numeric policy; this is not an all-heap memory bound.
|
||||
/// Ordinary parse/compile/edit/save APIs and CLI discovery remain separate.
|
||||
///
|
||||
/// ```
|
||||
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
|
||||
/// let bundle = SourceBundle {
|
||||
/// entry: "main.arcadia".into(),
|
||||
/// files: [("main.arcadia".into(),
|
||||
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
|
||||
/// };
|
||||
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
/// assert_eq!(checked.project().alias, "p");
|
||||
/// assert!(checked.warnings().is_empty());
|
||||
/// ```
|
||||
pub fn check_mixed_bundle(
|
||||
bundle: &crate::source_project::SourceBundle,
|
||||
limits: crate::source_project::ProjectLimits,
|
||||
) -> Result<CheckedMixed<'_>, Vec<SourceDiagnostic>> {
|
||||
let syntax = super::carried::parse_fresh(bundle, limits)?;
|
||||
let resolved = resolve(syntax).map_err(|failure| failure.diagnostics)?;
|
||||
Ok(CheckedMixed { resolved })
|
||||
}
|
||||
/// Read-only authority: every component originates in the consumed syntax.
|
||||
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
|
||||
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
syntax: ParsedMixedSyntax<'s>,
|
||||
project: Project,
|
||||
warnings: Vec<SourceDiagnostic>,
|
||||
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
static_limits: analysis::AdmissionLimits,
|
||||
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
static_usage: analysis::AdmissionUsage,
|
||||
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
expressions: Vec<lower::ExpressionSite>,
|
||||
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
occurrences: Vec<lower::Occurrence>,
|
||||
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
storage: Ledger,
|
||||
}
|
||||
impl ResolvedMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn phase_state(&self) -> (&ParsedMixedSyntax<'_>, Slots) {
|
||||
(&self.syntax, self.storage.live)
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
@@ -36,10 +142,12 @@ 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.
|
||||
#[cfg(test)]
|
||||
pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> {
|
||||
resolved: ResolvedMixedProject<'s>,
|
||||
data: super::super::index::IndexData,
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl<'s> ResolvedMixedProject<'s> {
|
||||
pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> {
|
||||
let input = IndexInput {
|
||||
@@ -57,6 +165,7 @@ impl<'s> ResolvedMixedProject<'s> {
|
||||
})
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl IndexedMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
self.resolved.project()
|
||||
@@ -93,12 +202,14 @@ impl IndexedMixedProject<'_> {
|
||||
|
||||
/// Constructor-only borrowed adapter. Its private file table prevents fabrication;
|
||||
/// no API returns it and no alternative source/token table can be installed.
|
||||
#[cfg(test)]
|
||||
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],
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl IndexInput<'_> {
|
||||
pub(in crate::language::analysis_source) fn declarations(
|
||||
&self,
|
||||
@@ -125,6 +236,7 @@ pub(in crate::language::analysis_source) struct Failure {
|
||||
}
|
||||
impl Failure {
|
||||
/// Canonical attribution when justified; otherwise a safe explicit start fallback.
|
||||
#[cfg(test)]
|
||||
fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self {
|
||||
failure.file = failure.file.filter(|&f| f < syntax.files.len());
|
||||
if failure.file.is_none_or(|file| {
|
||||
@@ -438,7 +550,12 @@ fn active_references(
|
||||
pub(in crate::language::analysis_source) fn resolve(
|
||||
mut syntax: ParsedMixedSyntax<'_>,
|
||||
) -> Result<ResolvedMixedProject<'_>, Failure> {
|
||||
let mut storage = Ledger::default();
|
||||
let mut storage = Ledger {
|
||||
live: syntax
|
||||
.carried
|
||||
.as_ref()
|
||||
.map_or_else(Slots::default, |input| input.slots()),
|
||||
};
|
||||
let lower_storage = accounting::syntax(&syntax, &mut storage)?;
|
||||
let catalog = catalog(&syntax, &mut storage)?;
|
||||
let entry = entry(&syntax);
|
||||
|
||||
@@ -104,9 +104,14 @@ struct Walk<'a, 'b> {
|
||||
budget: &'b Budget,
|
||||
work: Vec<Item<'a>>,
|
||||
lower: LowerStorage,
|
||||
text_total: Option<usize>,
|
||||
}
|
||||
impl<'a> Walk<'a, '_> {
|
||||
fn text(&mut self, text: &str, at: Span) -> Result<(), TypedError> {
|
||||
if let Some(total) = &mut self.text_total {
|
||||
*total = add(*total, text.len(), at)?;
|
||||
return Ok(());
|
||||
}
|
||||
self.ledger
|
||||
.reserve(self.budget, Slots::text(text.len()), at)
|
||||
}
|
||||
@@ -236,6 +241,19 @@ pub(super) fn syntax(
|
||||
ledger: &mut Ledger,
|
||||
) -> Result<LowerStorage, Failure> {
|
||||
let at = Span::default();
|
||||
let producer_text = syntax.budget.producer_text();
|
||||
if let Some((text, temporary)) = producer_text {
|
||||
if temporary != 0 {
|
||||
return Err(Failure::typed(
|
||||
syntax,
|
||||
None,
|
||||
error(at, "Unreleased syntax text temporary."),
|
||||
));
|
||||
}
|
||||
ledger
|
||||
.reserve(&syntax.budget, Slots::text(text), at)
|
||||
.map_err(|e| Failure::typed(syntax, None, e))?;
|
||||
}
|
||||
ledger
|
||||
.reserve(
|
||||
&syntax.budget,
|
||||
@@ -251,6 +269,7 @@ pub(super) fn syntax(
|
||||
budget: &syntax.budget,
|
||||
work: Vec::new(),
|
||||
lower: LowerStorage::default(),
|
||||
text_total: producer_text.map(|_| 0),
|
||||
};
|
||||
for (file, unit) in syntax.files.iter().enumerate() {
|
||||
let mut count = || -> Result<(), TypedError> {
|
||||
@@ -348,5 +367,12 @@ pub(super) fn syntax(
|
||||
};
|
||||
count().map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
}
|
||||
if walk.text_total != producer_text.map(|(text, _)| text) {
|
||||
return Err(Failure::typed(
|
||||
syntax,
|
||||
None,
|
||||
error(at, "Producer text differs from retained syntax inventory."),
|
||||
));
|
||||
}
|
||||
Ok(walk.lower)
|
||||
}
|
||||
|
||||
@@ -429,3 +429,5 @@ fn word(unit: &FileSyntax<'_>, at: Span, expected: &str) -> Result<(), TypedErro
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(in crate::language::analysis_source) mod rename;
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
//! Complete-target rename planning and private unverified emission; no admission.
|
||||
pub(in crate::language::analysis_source) mod emission;
|
||||
use super::*;
|
||||
use crate::source_project::SourceBundle;
|
||||
use lower::Target;
|
||||
|
||||
pub(in crate::language::analysis_source) struct RenamePlan<'s, 't> {
|
||||
source: AuthoredMixedProject<'s>,
|
||||
desired: &'t Project,
|
||||
names: BTreeMap<Target, &'t str>,
|
||||
edits: BTreeMap<OrdinaryKey, Target>,
|
||||
}
|
||||
impl<'s> AuthoredMixedProject<'s> {
|
||||
pub fn plan_rename<'t>(mut self, desired: &'t Project) -> Result<RenamePlan<'s, 't>, Failure> {
|
||||
let mut failure_file = None;
|
||||
let result = (|| {
|
||||
let resolved = &mut self.indexed.resolved;
|
||||
let e = resolved.expressions.len();
|
||||
resolved.storage.reserve(
|
||||
&resolved.syntax.budget,
|
||||
Slots::entries(e),
|
||||
Span::default(),
|
||||
)?;
|
||||
let equal = same_project(&resolved.project, desired);
|
||||
resolved.storage.release(Slots::entries(e));
|
||||
if !equal {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename target changes non-name project semantics.",
|
||||
));
|
||||
}
|
||||
let mut names = BTreeMap::new();
|
||||
let mut bytes = 0;
|
||||
visit_names(desired, &mut |target, name| {
|
||||
let old = self.indexed.data.names.get(&target).ok_or_else(|| {
|
||||
error(
|
||||
Span::default(),
|
||||
"Rename target is absent from the original catalog.",
|
||||
)
|
||||
})?;
|
||||
if old != name {
|
||||
bytes = add(bytes, name.len(), Span::default())?;
|
||||
self.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.budget
|
||||
.index_bytes(bytes, Span::default())?;
|
||||
if !crate::model::valid_alias(name) {
|
||||
return Err(error(Span::default(), "Invalid rename alias."));
|
||||
}
|
||||
self.indexed.resolved.storage.reserve(
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
Slots::entries(1),
|
||||
Span::default(),
|
||||
)?;
|
||||
if names.insert(target, name).is_some() {
|
||||
return Err(error(Span::default(), "Duplicate rename target."));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if !names.is_empty() {
|
||||
let mut namespace = BTreeSet::new();
|
||||
for (target, old) in &self.indexed.data.names {
|
||||
let scope = match target {
|
||||
Target::Global(_) => None,
|
||||
Target::Parameter { calculation, .. } => Some(*calculation),
|
||||
};
|
||||
let name = names.get(target).copied().unwrap_or(old);
|
||||
self.indexed.resolved.storage.reserve(
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
Slots::entries(1),
|
||||
Span::default(),
|
||||
)?;
|
||||
if !namespace.insert((scope, name)) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename aliases collide in their final namespace.",
|
||||
));
|
||||
}
|
||||
}
|
||||
let q = namespace.len();
|
||||
drop(namespace);
|
||||
self.indexed.resolved.storage.release(Slots::entries(q));
|
||||
}
|
||||
let mut edits = BTreeMap::new();
|
||||
for (key, location) in &self.indexed.data.occurrences {
|
||||
if names.contains_key(&key.target) {
|
||||
failure_file = Some(location.file);
|
||||
insert_edit(
|
||||
&mut self.indexed.resolved.storage,
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
&mut edits,
|
||||
(location.file, location.span.start, location.span.end),
|
||||
key.target,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for (&key, occurrence) in &self.ordinary {
|
||||
let target = Target::Global(occurrence.target);
|
||||
if names.contains_key(&target) {
|
||||
failure_file = Some(key.0);
|
||||
insert_edit(
|
||||
&mut self.indexed.resolved.storage,
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
&mut edits,
|
||||
key,
|
||||
target,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok((names, edits))
|
||||
})();
|
||||
let (names, edits) = result.map_err(|error| {
|
||||
Failure::index(
|
||||
&self.indexed.resolved.syntax,
|
||||
lower::Failure {
|
||||
file: failure_file,
|
||||
error,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let plan = RenamePlan {
|
||||
source: self,
|
||||
desired,
|
||||
names,
|
||||
edits,
|
||||
};
|
||||
plan.validate()?;
|
||||
Ok(plan)
|
||||
}
|
||||
}
|
||||
fn insert_edit(
|
||||
storage: &mut Ledger,
|
||||
budget: &Budget,
|
||||
edits: &mut BTreeMap<OrdinaryKey, Target>,
|
||||
key: OrdinaryKey,
|
||||
target: Target,
|
||||
) -> Result<(), TypedError> {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
if edits.contains_key(&key) {
|
||||
return Err(error(at, "Duplicate rename physical token."));
|
||||
}
|
||||
storage.reserve(budget, Slots::entries(1), at)?;
|
||||
edits.insert(key, target);
|
||||
Ok(())
|
||||
}
|
||||
impl<'s, 't> RenamePlan<'s, 't> {
|
||||
pub fn source(&self) -> &AuthoredMixedProject<'s> {
|
||||
&self.source
|
||||
}
|
||||
pub fn desired(&self) -> &'t Project {
|
||||
self.desired
|
||||
}
|
||||
pub fn changed_names(&self) -> &BTreeMap<Target, &'t str> {
|
||||
&self.names
|
||||
}
|
||||
pub fn edits(&self) -> &BTreeMap<OrdinaryKey, Target> {
|
||||
&self.edits
|
||||
}
|
||||
pub fn validate_current(&self, current: &SourceBundle) -> Result<(), Failure> {
|
||||
let old = self.source.bundle();
|
||||
if old.entry != current.entry || old.files != current.files {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Current complete bundle differs from the rename snapshot.",
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn failure(&self, file: Option<usize>, error: TypedError) -> Failure {
|
||||
Failure::index(
|
||||
&self.source.indexed.resolved.syntax,
|
||||
lower::Failure { file, error },
|
||||
)
|
||||
}
|
||||
fn validate(&self) -> Result<(), Failure> {
|
||||
let mut previous: Option<OrdinaryKey> = None;
|
||||
for (&key, target) in &self.edits {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
let checked = (|| {
|
||||
let unit = self
|
||||
.source
|
||||
.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.files
|
||||
.get(key.0)
|
||||
.ok_or_else(|| error(at, "Rename token has no original file."))?;
|
||||
let original = self
|
||||
.source
|
||||
.names()
|
||||
.get(target)
|
||||
.ok_or_else(|| error(at, "Rename token has no original target."))?;
|
||||
if !self.names.contains_key(target) {
|
||||
return Err(error(at, "Rename token targets an unchanged name."));
|
||||
}
|
||||
if at.start >= at.end {
|
||||
return Err(error(at, "Rename token must be nonempty."));
|
||||
}
|
||||
word(unit, at, original)?;
|
||||
if previous.is_some_and(|p| p.0 == key.0 && p.2 > key.1) {
|
||||
return Err(error(at, "Rename token ranges overlap."));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
checked.map_err(|e| self.failure(Some(key.0), e))?;
|
||||
previous = Some(key);
|
||||
}
|
||||
let mut required = 0;
|
||||
for (key, location) in self.source.typed_occurrences() {
|
||||
self.require(
|
||||
(location.file, location.span.start, location.span.end),
|
||||
key.target,
|
||||
&mut required,
|
||||
)?;
|
||||
}
|
||||
for (&key, occurrence) in self.source.ordinary_occurrences() {
|
||||
self.require(key, Target::Global(occurrence.target), &mut required)?;
|
||||
}
|
||||
if required != self.edits.len() {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(Span::default(), "Rename edit set contains extra tokens."),
|
||||
));
|
||||
}
|
||||
self.output_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
fn require(
|
||||
&self,
|
||||
key: OrdinaryKey,
|
||||
target: Target,
|
||||
required: &mut usize,
|
||||
) -> Result<(), Failure> {
|
||||
if self.names.contains_key(&target) {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
if self.edits.get(&key) != Some(&target) {
|
||||
return Err(self.failure(
|
||||
Some(key.0),
|
||||
error(at, "Rename edit set is missing a required target token."),
|
||||
));
|
||||
}
|
||||
*required = add(*required, 1, at).map_err(|e| self.failure(Some(key.0), e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn output_bytes(&self) -> Result<usize, Failure> {
|
||||
let result = (|| {
|
||||
let mut total = 0;
|
||||
let mut edits = self.edits.iter().peekable();
|
||||
for (file, _, original) in self.source.files() {
|
||||
let mut removed = 0;
|
||||
let mut replaced = 0;
|
||||
while let Some((&key, target)) = edits.peek().copied() {
|
||||
if key.0 != file {
|
||||
break;
|
||||
}
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
removed = add(
|
||||
removed,
|
||||
key.2
|
||||
.checked_sub(key.1)
|
||||
.ok_or_else(|| error(at, "Invalid rename token size."))?,
|
||||
at,
|
||||
)?;
|
||||
replaced = add(
|
||||
replaced,
|
||||
self.names
|
||||
.get(target)
|
||||
.ok_or_else(|| error(at, "Rename token has no replacement."))?
|
||||
.len(),
|
||||
at,
|
||||
)?;
|
||||
edits.next();
|
||||
}
|
||||
let size = original.len().checked_sub(removed).ok_or_else(|| {
|
||||
error(Span::default(), "Rename removals exceed original file.")
|
||||
})?;
|
||||
total = add(
|
||||
total,
|
||||
add(size, replaced, Span::default())?,
|
||||
Span::default(),
|
||||
)?;
|
||||
}
|
||||
if edits.next().is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename edit file is outside original bundle.",
|
||||
));
|
||||
}
|
||||
self.source
|
||||
.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.budget
|
||||
.index_bytes(total, Span::default())?;
|
||||
Ok(total)
|
||||
})();
|
||||
result.map_err(|e| self.failure(None, e))
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustive exclusion patterns make every new field an explicit gate decision.
|
||||
macro_rules! fixed {
|
||||
($old:expr, $new:expr, $ty:path, [$($field:ident),*], [$($excluded:ident),*]) => {{
|
||||
let $ty { $($field,)* $($excluded: _,)* } = $old;
|
||||
true $(&& $field == &$new.$field)*
|
||||
}};
|
||||
}
|
||||
fn map_same<T>(
|
||||
old: &BTreeMap<Id, T>,
|
||||
new: &BTreeMap<Id, T>,
|
||||
same: impl Fn(&T, &T) -> bool,
|
||||
) -> bool {
|
||||
old.len() == new.len()
|
||||
&& old
|
||||
.iter()
|
||||
.zip(new)
|
||||
.all(|((a, x), (b, y))| a == b && same(x, y))
|
||||
}
|
||||
fn same_project(a: &Project, b: &Project) -> bool {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
Project,
|
||||
[schema_version, id, name, description],
|
||||
[
|
||||
alias,
|
||||
definitions,
|
||||
elements,
|
||||
relations,
|
||||
diagrams,
|
||||
artifact_links,
|
||||
traces
|
||||
]
|
||||
) && map_same(&a.definitions, &b.definitions, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
analysis::Definition,
|
||||
[id, name, description],
|
||||
[alias, body]
|
||||
) && match (&a.body, &b.body) {
|
||||
(
|
||||
analysis::DefinitionBody::Calculation(a),
|
||||
analysis::DefinitionBody::Calculation(b),
|
||||
) => {
|
||||
a.parameters.len() == b.parameters.len()
|
||||
&& a.parameters
|
||||
.iter()
|
||||
.zip(&b.parameters)
|
||||
.all(|(a, b)| fixed!(a, b, analysis::Parameter, [id, value_type], [name]))
|
||||
&& fixed!(
|
||||
a,
|
||||
b,
|
||||
analysis::CalculationDefinition,
|
||||
[result, body],
|
||||
[parameters]
|
||||
)
|
||||
}
|
||||
(
|
||||
analysis::DefinitionBody::QuantityKind(a),
|
||||
analysis::DefinitionBody::QuantityKind(b),
|
||||
) => a == b,
|
||||
(analysis::DefinitionBody::Unit(a), analysis::DefinitionBody::Unit(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}) && map_same(&a.elements, &b.elements, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Element,
|
||||
[id, name, description, parent, body],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.relations, &b.relations, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Relation,
|
||||
[id, name, description, body],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.diagrams, &b.diagrams, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Diagram,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
layer,
|
||||
projection,
|
||||
view_options,
|
||||
routes
|
||||
],
|
||||
[alias, nodes]
|
||||
) && map_same(&a.nodes, &b.nodes, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::NodePlacement,
|
||||
[id, element, position, size, collapsed, port_anchors],
|
||||
[alias]
|
||||
)
|
||||
})
|
||||
}) && map_same(&a.artifact_links, &b.artifact_links, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::ArtifactLink,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
owner,
|
||||
relation,
|
||||
uri,
|
||||
revision,
|
||||
fragment
|
||||
],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.traces, &b.traces, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Trace,
|
||||
[id, name, description, oa, sa, kind, baseline],
|
||||
[alias]
|
||||
)
|
||||
})
|
||||
}
|
||||
fn visit_names<'t>(
|
||||
project: &'t Project,
|
||||
f: &mut impl FnMut(Target, &'t str) -> Result<(), TypedError>,
|
||||
) -> Result<(), TypedError> {
|
||||
f(Target::Global(project.id), &project.alias)?;
|
||||
for d in project.definitions.values() {
|
||||
f(Target::Global(d.id), &d.alias)?;
|
||||
if let analysis::DefinitionBody::Calculation(c) = &d.body {
|
||||
for p in &c.parameters {
|
||||
f(
|
||||
Target::Parameter {
|
||||
calculation: d.id,
|
||||
parameter: p.id,
|
||||
},
|
||||
&p.name,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in project.elements.values() {
|
||||
f(Target::Global(e.id), &e.alias)?;
|
||||
}
|
||||
for r in project.relations.values() {
|
||||
f(Target::Global(r.id), &r.alias)?;
|
||||
}
|
||||
for d in project.diagrams.values() {
|
||||
f(Target::Global(d.id), &d.alias)?;
|
||||
for n in d.nodes.values() {
|
||||
f(Target::Global(n.id), &n.alias)?;
|
||||
}
|
||||
}
|
||||
for a in project.artifact_links.values() {
|
||||
f(Target::Global(a.id), &a.alias)?;
|
||||
}
|
||||
for t in project.traces.values() {
|
||||
f(Target::Global(t.id), &t.alias)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
//! Owned unverified emission; consumed actual-output verification is a private child.
|
||||
pub(in crate::language::analysis_source) mod acceptance;
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::source_project::ProjectLimits;
|
||||
|
||||
pub(in crate::language::analysis_source) struct PendingRename<'t> {
|
||||
bundle: SourceBundle,
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
retained: Slots,
|
||||
expression_nodes: usize,
|
||||
desired: &'t Project,
|
||||
}
|
||||
|
||||
impl<'s, 't> RenamePlan<'s, 't> {
|
||||
pub fn into_unchanged(
|
||||
self,
|
||||
current: &SourceBundle,
|
||||
) -> Result<AuthoredMixedProject<'s>, Failure> {
|
||||
self.validate_current(current)?;
|
||||
self.validate()?;
|
||||
if !self.names.is_empty() {
|
||||
return Err(self.failure(None, error(Span::default(), "Rename plan changes names.")));
|
||||
}
|
||||
Ok(self.source)
|
||||
}
|
||||
|
||||
pub fn emit_changed_unverified(
|
||||
mut self,
|
||||
current: &SourceBundle,
|
||||
) -> Result<PendingRename<'t>, Failure> {
|
||||
self.validate_current(current)?;
|
||||
self.validate()?;
|
||||
if self.names.is_empty() {
|
||||
return Err(self.failure(None, error(Span::default(), "Rename plan is unchanged.")));
|
||||
}
|
||||
if self.source.indexed.resolved.syntax.carried.is_some() {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Changed emission requires original legacy syntax.",
|
||||
),
|
||||
));
|
||||
}
|
||||
let total = self.output_bytes()?;
|
||||
let retained =
|
||||
output_slots(self.source.bundle(), total).map_err(|e| self.failure(None, e))?;
|
||||
let resolved = &mut self.source.indexed.resolved;
|
||||
resolved
|
||||
.storage
|
||||
.reserve(&resolved.syntax.budget, retained, Span::default())
|
||||
.map_err(|e| self.failure(None, e))?;
|
||||
|
||||
// The complete output reservation precedes every owned byte and entry.
|
||||
let mut files = BTreeMap::new();
|
||||
let mut produced = 0;
|
||||
for (file, path, original) in self.source.files() {
|
||||
let result = (|| {
|
||||
let edits = || {
|
||||
self.edits
|
||||
.range((file, 0, 0)..=(file, usize::MAX, usize::MAX))
|
||||
};
|
||||
let mut removed = 0;
|
||||
let mut replacement = 0;
|
||||
for (&(_, start, end), target) in edits() {
|
||||
let at = Span { start, end };
|
||||
removed = add(
|
||||
removed,
|
||||
end.checked_sub(start)
|
||||
.ok_or_else(|| error(at, "Invalid rename token size."))?,
|
||||
at,
|
||||
)?;
|
||||
replacement = add(replacement, self.names[target].len(), at)?;
|
||||
}
|
||||
let size = add(
|
||||
original.len().checked_sub(removed).ok_or_else(|| {
|
||||
error(Span::default(), "Rename removals exceed original file.")
|
||||
})?,
|
||||
replacement,
|
||||
Span::default(),
|
||||
)?;
|
||||
let mut output = String::with_capacity(size);
|
||||
let mut cursor = 0;
|
||||
for (&(_, start, end), target) in edits() {
|
||||
let at = Span { start, end };
|
||||
output.push_str(
|
||||
original
|
||||
.get(cursor..start)
|
||||
.ok_or_else(|| error(at, "Invalid rename output range."))?,
|
||||
);
|
||||
output.push_str(self.names[target]);
|
||||
cursor = end;
|
||||
}
|
||||
output.push_str(
|
||||
original
|
||||
.get(cursor..)
|
||||
.ok_or_else(|| error(Span::default(), "Invalid rename output tail."))?,
|
||||
);
|
||||
if output.len() != size {
|
||||
return Err(error(Span::default(), "Rename output length mismatch."));
|
||||
}
|
||||
produced = add(produced, size, Span::default())?;
|
||||
Ok(output)
|
||||
})();
|
||||
let output = result.map_err(|e| self.failure(Some(file), e))?;
|
||||
files.insert(path.to_owned(), output);
|
||||
}
|
||||
if files.len() != retained.entries || produced != total {
|
||||
return Err(self.failure(None, error(Span::default(), "Incomplete rename output.")));
|
||||
}
|
||||
let bundle = SourceBundle {
|
||||
entry: self.source.bundle().entry.clone(),
|
||||
files,
|
||||
};
|
||||
let limits = self.source.indexed.resolved.syntax.budget.limits();
|
||||
let project_limits = self.source.indexed.resolved.syntax.limits;
|
||||
let expression_nodes = self.source.indexed.resolved.expressions.len();
|
||||
let Self {
|
||||
source,
|
||||
desired,
|
||||
names,
|
||||
edits,
|
||||
} = self;
|
||||
drop(names);
|
||||
drop(edits);
|
||||
drop(source);
|
||||
Ok(PendingRename {
|
||||
bundle,
|
||||
limits,
|
||||
project_limits,
|
||||
retained,
|
||||
expression_nodes,
|
||||
desired,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn output_slots(bundle: &SourceBundle, total: usize) -> Result<Slots, TypedError> {
|
||||
let mut text = add(total, bundle.entry.len(), Span::default())?;
|
||||
for path in bundle.files.keys() {
|
||||
text = add(text, path.len(), Span::default())?;
|
||||
}
|
||||
Ok(Slots {
|
||||
entries: bundle.files.len(),
|
||||
occurrences: 0,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
//! Actual-byte verification and owning result; no installation or mutable extraction.
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Usage;
|
||||
use crate::language::analysis_source::mixed::{carried, project};
|
||||
use crate::source_project::SourceDiagnostic;
|
||||
|
||||
pub(in crate::language::analysis_source) struct AcceptedRename {
|
||||
bundle: SourceBundle,
|
||||
project: Project,
|
||||
warnings: Vec<SourceDiagnostic>,
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
retained: Slots,
|
||||
verification_usage: Usage,
|
||||
verification_numeric_units: u64,
|
||||
}
|
||||
impl AcceptedRename {
|
||||
pub fn bundle(&self) -> &SourceBundle {
|
||||
&self.bundle
|
||||
}
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
pub fn policy(&self) -> (Limits, ProjectLimits) {
|
||||
(self.limits, self.project_limits)
|
||||
}
|
||||
pub fn retained(&self) -> Slots {
|
||||
self.retained
|
||||
}
|
||||
pub fn verification_work(&self) -> (Usage, u64) {
|
||||
(self.verification_usage, self.verification_numeric_units)
|
||||
}
|
||||
}
|
||||
impl PendingRename<'_> {
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_bundle(&self) -> &SourceBundle {
|
||||
&self.bundle
|
||||
}
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_policy(
|
||||
&self,
|
||||
) -> (Limits, ProjectLimits) {
|
||||
(self.limits, self.project_limits)
|
||||
}
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_slots(&self) -> Slots {
|
||||
self.retained
|
||||
}
|
||||
pub fn verify(self) -> Result<AcceptedRename, Failure> {
|
||||
let (project, warnings, retained, verification_usage, verification_numeric_units) = {
|
||||
let syntax = carried::parse_entry_producers(carried::CarriedInput::from_pending(&self))
|
||||
.map_err(|diagnostics| Failure { diagnostics })?;
|
||||
let mut resolved = project::resolve(syntax)?;
|
||||
let equal = equal_target(&mut resolved, self.desired)?;
|
||||
if !equal {
|
||||
return Err(Failure::typed(
|
||||
&resolved.syntax,
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Verified output differs from the complete rename target.",
|
||||
),
|
||||
));
|
||||
}
|
||||
let retained = warning_inventory(&resolved.warnings)
|
||||
.and_then(|warnings| self.retained.add(warnings, Span::default()))
|
||||
.and_then(|retained| {
|
||||
check_destination(&resolved.syntax.budget, resolved.storage.live, retained)?;
|
||||
Ok(retained)
|
||||
})
|
||||
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
|
||||
let usage = resolved.syntax.budget.usage;
|
||||
let numeric = resolved.syntax.budget.numeric_units();
|
||||
let project::ResolvedMixedProject {
|
||||
syntax,
|
||||
project,
|
||||
warnings,
|
||||
expressions,
|
||||
occurrences,
|
||||
static_limits: _,
|
||||
static_usage: _,
|
||||
storage: _,
|
||||
} = resolved;
|
||||
// The full live ledger remains intact through the checks above.
|
||||
// Only scalar destination accounting escapes these actual drops.
|
||||
drop(expressions);
|
||||
drop(occurrences);
|
||||
drop(syntax);
|
||||
(project, warnings, retained, usage, numeric)
|
||||
};
|
||||
Ok(AcceptedRename {
|
||||
bundle: self.bundle,
|
||||
project,
|
||||
warnings,
|
||||
limits: self.limits,
|
||||
project_limits: self.project_limits,
|
||||
retained,
|
||||
verification_usage,
|
||||
verification_numeric_units,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Every actual Expr root is lowered into this arena. The old pending expression
|
||||
// count cannot authenticate parsed topology. Keep this boundary independently
|
||||
// testable without changing the inherited policy or instrumenting comparison.
|
||||
fn equal_target(
|
||||
resolved: &mut project::ResolvedMixedProject<'_>,
|
||||
desired: &Project,
|
||||
) -> Result<bool, Failure> {
|
||||
let frontier = Slots::entries(resolved.expressions.len());
|
||||
resolved
|
||||
.storage
|
||||
.reserve(&resolved.syntax.budget, frontier, Span::default())
|
||||
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
|
||||
let equal = resolved.project.eq(desired);
|
||||
resolved.storage.release(frontier);
|
||||
Ok(equal)
|
||||
}
|
||||
fn check_destination(budget: &Budget, live: Slots, retained: Slots) -> Result<(), TypedError> {
|
||||
let at = Span::default();
|
||||
if retained.entries > live.entries
|
||||
|| retained.occurrences > live.occurrences
|
||||
|| retained.text > live.text
|
||||
{
|
||||
return Err(error(
|
||||
at,
|
||||
"Accepted inventory exceeds live verification storage.",
|
||||
));
|
||||
}
|
||||
budget.lowering_entries(retained.entries, at)?;
|
||||
budget.lowering_occurrences(retained.occurrences, at)?;
|
||||
budget.index_bytes(retained.text, at)
|
||||
}
|
||||
fn warning_inventory(warnings: &[SourceDiagnostic]) -> Result<Slots, TypedError> {
|
||||
let mut inventory = Slots::default();
|
||||
for SourceDiagnostic {
|
||||
path,
|
||||
diagnostic,
|
||||
related,
|
||||
} in warnings
|
||||
{
|
||||
let language::Diagnostic {
|
||||
severity,
|
||||
code,
|
||||
object_id: _,
|
||||
field,
|
||||
line: _,
|
||||
column: _,
|
||||
message,
|
||||
span: _,
|
||||
} = diagnostic;
|
||||
if !related.is_empty() || *severity == validation::Severity::Error {
|
||||
return Err(error(Span::default(), "Unexpected accepted warning shape."));
|
||||
}
|
||||
let text = [
|
||||
path.len(),
|
||||
code.len(),
|
||||
field.as_ref().map_or(0, String::len),
|
||||
message.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.try_fold(0, |n, k| add(n, k, Span::default()))?;
|
||||
inventory = inventory.add(
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text,
|
||||
},
|
||||
Span::default(),
|
||||
)?;
|
||||
}
|
||||
Ok(inventory)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+845
@@ -0,0 +1,845 @@
|
||||
use super::*;
|
||||
use crate::language::{ParseLimits, analysis_source::mixed::parse_with_profile};
|
||||
use crate::{Layer, analysis as a};
|
||||
use project::tests::{id, rich_bundle, rich_model, single};
|
||||
const MISMATCH: &str = "Verified output differs from the complete rename target.";
|
||||
const NEXT: &str =
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
const UNNAMED: &str =
|
||||
"arcadia 8;\nproject next \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
fn limited(b: &SourceBundle, limits: Limits, policy: ProjectLimits) -> AuthoredMixedProject<'_> {
|
||||
project::resolve(parse_with_profile(b, policy, limits).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
}
|
||||
fn authored(b: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
limited(b, profile(100_000, 1_000_000), ProjectLimits::default())
|
||||
}
|
||||
fn expected(name: &str) -> Project {
|
||||
Project {
|
||||
schema_version: 8,
|
||||
id: id(1),
|
||||
alias: "next".into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
fn failure<T>(result: Result<T, Failure>) -> Failure {
|
||||
match result {
|
||||
Ok(_) => panic!("unexpected verification success"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
fn diagnostic(failure: &Failure, message: &str, start: usize, end: usize) {
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
let d = &failure.diagnostics[0];
|
||||
assert_eq!(d.path, "main.arcadia");
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.message, message);
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
fn same_policy(accepted: &AcceptedRename, limits: Limits, policy: ProjectLimits) {
|
||||
let (a, b) = accepted.policy();
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[
|
||||
limits.bytes,
|
||||
limits.tokens,
|
||||
limits.entries,
|
||||
limits.references,
|
||||
limits.frames
|
||||
]
|
||||
);
|
||||
assert_eq!(a.numeric_units, limits.numeric_units);
|
||||
assert_eq!(
|
||||
[
|
||||
b.max_files,
|
||||
b.max_include_depth,
|
||||
b.max_include_edges,
|
||||
b.model.max_source_bytes,
|
||||
b.model.max_graph_objects,
|
||||
b.model.max_records,
|
||||
b.model.max_step_references
|
||||
],
|
||||
[
|
||||
policy.max_files,
|
||||
policy.max_include_depth,
|
||||
policy.max_include_edges,
|
||||
policy.model.max_source_bytes,
|
||||
policy.model.max_graph_objects,
|
||||
policy.model.max_records,
|
||||
policy.model.max_step_references
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn actual_minimal_bytes_full_model_and_late_decode_boundary() {
|
||||
for bytes in [152, 151] {
|
||||
let b = single("");
|
||||
let before = b.clone();
|
||||
let limits = profile(17, bytes);
|
||||
let source = limited(&b, limits, ProjectLimits::default());
|
||||
assert_eq!(source.indexed.resolved.syntax.budget.usage.bytes, 78);
|
||||
let desired = expected("Project");
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
let result = pending.verify();
|
||||
if bytes == 152 {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(
|
||||
accepted.bundle(),
|
||||
&SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), NEXT.into())])
|
||||
}
|
||||
);
|
||||
assert_eq!(accepted.project(), &expected("Project"));
|
||||
assert!(accepted.warnings().is_empty());
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 105
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.verification_work(),
|
||||
(
|
||||
Usage {
|
||||
bytes: 81,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
same_policy(&accepted, limits, ProjectLimits::default());
|
||||
} else {
|
||||
let e = failure(result);
|
||||
diagnostic(&e, "Source8 index/output byte limit exceeded.", 37, 38);
|
||||
assert_eq!(
|
||||
(
|
||||
e.diagnostics[0].diagnostic.line,
|
||||
e.diagnostics[0].diagnostic.column
|
||||
),
|
||||
(2, 27)
|
||||
);
|
||||
}
|
||||
assert_eq!(b, before);
|
||||
assert_eq!(desired, expected("Project"));
|
||||
}
|
||||
}
|
||||
fn warning() -> SourceDiagnostic {
|
||||
SourceDiagnostic {
|
||||
path: "main.arcadia".into(),
|
||||
related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning,
|
||||
code: "unnamed".into(),
|
||||
object_id: Some(id(1)),
|
||||
field: Some("name".into()),
|
||||
message: "Give the project a descriptive name.".into(),
|
||||
span: Span { start: 11, end: 69 },
|
||||
line: 2,
|
||||
column: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn actual_warning_mapping_boundary_and_final_transfer() {
|
||||
for bytes in [161, 160] {
|
||||
let mut b = single("");
|
||||
b.files.insert(
|
||||
"main.arcadia".into(),
|
||||
"arcadia 8;\nproject p \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n"
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(b.files["main.arcadia"].len(), 71);
|
||||
let limits = profile(18, bytes);
|
||||
let source = limited(&b, limits, ProjectLimits::default());
|
||||
assert_eq!(
|
||||
source.warnings()[0].diagnostic.span,
|
||||
Span { start: 11, end: 66 }
|
||||
);
|
||||
let desired = expected("");
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
let result = pending.verify();
|
||||
if bytes == 161 {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(accepted.bundle().files["main.arcadia"], UNNAMED);
|
||||
assert_eq!(accepted.project(), &expected(""));
|
||||
assert_eq!(accepted.warnings(), [warning()]);
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 2,
|
||||
occurrences: 0,
|
||||
text: 157
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.verification_work(),
|
||||
(
|
||||
Usage {
|
||||
bytes: 74,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
same_policy(&accepted, limits, ProjectLimits::default());
|
||||
} else {
|
||||
let e = failure(result);
|
||||
diagnostic(&e, "Source8 index/output byte limit exceeded.", 11, 69);
|
||||
assert_eq!(
|
||||
(
|
||||
e.diagnostics[0].diagnostic.line,
|
||||
e.diagnostics[0].diagnostic.column
|
||||
),
|
||||
(2, 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn owning_result_survives_all_inputs_and_preserves_every_policy_field() {
|
||||
let limits = Limits {
|
||||
bytes: 2048,
|
||||
tokens: 101,
|
||||
entries: 99,
|
||||
references: 31,
|
||||
frames: 17,
|
||||
numeric_units: 8192,
|
||||
};
|
||||
let policy = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 3,
|
||||
max_include_edges: 5,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 3000,
|
||||
max_graph_objects: 67,
|
||||
max_records: 123,
|
||||
max_step_references: 37,
|
||||
},
|
||||
};
|
||||
let accepted = {
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
let pending = limited(&b, limits, policy)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
// A short pending borrow carries a longer independently owned target borrow.
|
||||
{
|
||||
let syntax =
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
|
||||
.unwrap();
|
||||
assert_eq!(syntax.bundle, &pending.bundle);
|
||||
drop(syntax);
|
||||
}
|
||||
pending.verify().unwrap()
|
||||
};
|
||||
assert_eq!(accepted.bundle().files["main.arcadia"], NEXT);
|
||||
assert_eq!(accepted.project(), &expected("Project"));
|
||||
assert!(accepted.warnings().is_empty());
|
||||
same_policy(&accepted, limits, policy);
|
||||
}
|
||||
#[test]
|
||||
fn equal_byte_wrong_models_malformed_and_admission_failure_are_distinct() {
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
for (start, end, replacement) in [(19, 23, "nope"), (25, 32, "Changed"), (8, 9, "?")] {
|
||||
let mut pending = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
pending
|
||||
.bundle
|
||||
.files
|
||||
.get_mut("main.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(start..end, replacement);
|
||||
assert_eq!(pending.bundle.files["main.arcadia"].len(), 81);
|
||||
if start != 8 {
|
||||
let actual = project::resolve(
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(actual.project(), &desired); // Wrong output really is admissible.
|
||||
drop(actual);
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
} else {
|
||||
let e = failure(pending.verify());
|
||||
assert_eq!(e.diagnostics[0].diagnostic.message, "Invalid source token.");
|
||||
}
|
||||
}
|
||||
let b = single(&format!(
|
||||
"oa_activity a \"A\" id \"{}\" {{ parent @b; }}\noa_activity b \"B\" id \"{}\" {{}}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
let text = pending.bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let at = text.find("parent @b").unwrap() + 8;
|
||||
text.replace_range(at..at + 1, "a");
|
||||
let syntax =
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap();
|
||||
drop(syntax); // This corruption is grammar-valid but model-invalid.
|
||||
let e = failure(pending.verify());
|
||||
assert!(
|
||||
e.diagnostics
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.code == "containment_cycle")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn actual_expression_arena_controls_equality_reservation() {
|
||||
let b = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body 10000; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = limited(&b, profile(4096, 4096), ProjectLimits::default());
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, 1);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 195
|
||||
}
|
||||
);
|
||||
pending
|
||||
.bundle
|
||||
.files
|
||||
.get_mut("main.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(160..165, "1 + 2");
|
||||
let mut actual = project::resolve(
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual.expressions.len(), 3);
|
||||
let before = actual.storage.live;
|
||||
assert!(!equal_target(&mut actual, &desired).unwrap());
|
||||
assert_eq!(actual.storage.live, before);
|
||||
// Labeled ledger fault: two free slots would fit old E1, but not actual E3.
|
||||
// This is not an end-to-end late cap or a policy mutation.
|
||||
actual.storage.live.entries = 4094;
|
||||
let e = equal_target(&mut actual, &desired).unwrap_err();
|
||||
diagnostic(&e, "Source8 lowering entry limit exceeded.", 0, 0);
|
||||
assert_eq!(actual.storage.live.entries, 4094);
|
||||
drop(actual);
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
}
|
||||
#[test]
|
||||
fn internal_warning_shape_and_destination_inventory_faults() {
|
||||
assert_eq!(
|
||||
warning_inventory(&[warning()]).unwrap(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 59
|
||||
}
|
||||
);
|
||||
let mut bad = warning();
|
||||
bad.related.push(crate::source_project::SourceLocation {
|
||||
path: "extra".into(),
|
||||
span: Span::default(),
|
||||
});
|
||||
assert_eq!(
|
||||
warning_inventory(&[bad]).unwrap_err().message,
|
||||
"Unexpected accepted warning shape."
|
||||
);
|
||||
let mut bad = warning();
|
||||
bad.diagnostic.severity = validation::Severity::Error;
|
||||
assert!(warning_inventory(&[bad]).is_err());
|
||||
let budget = Budget::new(profile(100, 4096)).unwrap();
|
||||
let live = Slots {
|
||||
entries: 3,
|
||||
occurrences: 2,
|
||||
text: 200,
|
||||
};
|
||||
for retained in [
|
||||
Slots { entries: 4, ..live },
|
||||
Slots {
|
||||
occurrences: 3,
|
||||
..live
|
||||
},
|
||||
Slots { text: 201, ..live },
|
||||
] {
|
||||
assert_eq!(
|
||||
check_destination(&budget, live, retained)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Accepted inventory exceeds live verification storage."
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
Slots::text(usize::MAX)
|
||||
.add(Slots::text(1), Span::default())
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
Slots::entries(usize::MAX)
|
||||
.add(Slots::entries(1), Span::default())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
fn rich_desired(layer: Layer) -> Project {
|
||||
let mut p = rich_model(layer);
|
||||
p.alias = "w".into();
|
||||
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
|
||||
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
|
||||
let d = p.diagrams.get_mut(&id(5)).unwrap();
|
||||
d.alias = "z".into();
|
||||
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
|
||||
for (n, name) in [(10, "g"), (11, "f")] {
|
||||
let def = p.definitions.get_mut(&id(n)).unwrap();
|
||||
def.alias = name.into();
|
||||
let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c.parameters[0].name = "y".into();
|
||||
c.parameters[1].name = "x".into();
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
|
||||
}
|
||||
p
|
||||
}
|
||||
// Independently authored entire expected files: no edit iterator or output text
|
||||
// contributes to these strings. The LF/CRLF transform applies to whole literals.
|
||||
const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n";
|
||||
const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n";
|
||||
fn rich_expected(layer: Layer) -> SourceBundle {
|
||||
let (property, work, diagram, extra) = if layer == Layer::Oa {
|
||||
("oa_property", "oa_activity", "", "")
|
||||
} else {
|
||||
(
|
||||
"sa_property",
|
||||
"sa_function",
|
||||
"layer Sa;",
|
||||
"sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}",
|
||||
)
|
||||
};
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("a.arcadia".into(), RICH_A.into()),
|
||||
("b.arcadia".into(), RICH_B.into()),
|
||||
(
|
||||
"main.arcadia".into(),
|
||||
format!(
|
||||
"arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n"
|
||||
),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn rich_oa_sa_swaps_unicode_crlf_complete_actual_bytes_and_models() {
|
||||
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
|
||||
for unicode in [false, true] {
|
||||
let mut b = rich_bundle(layer);
|
||||
let mut expected = rich_expected(layer);
|
||||
if unicode {
|
||||
for text in b.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
for text in expected.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let before = b.clone();
|
||||
let desired = rich_desired(layer);
|
||||
let accepted = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.bundle(), &expected);
|
||||
assert_eq!(accepted.project(), &rich_desired(layer));
|
||||
let (span, line) = if unicode {
|
||||
(
|
||||
Span {
|
||||
start: 351,
|
||||
end: 416,
|
||||
},
|
||||
7,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Span {
|
||||
start: 338,
|
||||
end: 403,
|
||||
},
|
||||
6,
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
accepted.warnings(),
|
||||
leaf_work_warnings("main.arcadia", span, line)
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 5,
|
||||
occurrences: 0,
|
||||
text: if unicode {
|
||||
unicode_bytes + 42 + 227
|
||||
} else {
|
||||
bytes + 42 + 227
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(b, before);
|
||||
assert_eq!(desired, rich_desired(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn leaf_work_warnings(path: &str, span: Span, line: usize) -> [SourceDiagnostic; 2] {
|
||||
// Allocation validation precedes advisory coverage. Both locate the complete
|
||||
// ordinary Work declaration; neither fixture supplies allocation or context.
|
||||
[
|
||||
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
|
||||
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
|
||||
].map(|(code, field, message)| SourceDiagnostic {
|
||||
path: path.into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning, code: code.into(),
|
||||
object_id: Some(id(2)), field: field.map(str::to_owned),
|
||||
message: message.into(), span, line, column: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
fn calculation(p: &mut Project) -> &mut a::CalculationDefinition {
|
||||
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(2)).unwrap().body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c
|
||||
}
|
||||
#[test]
|
||||
fn scoped_small_stack_actual_deep_wide_verification_and_drop() {
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let body = format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
|
||||
id(2)
|
||||
);
|
||||
let b = single(&body);
|
||||
let before = b.clone();
|
||||
let expected_bytes = format!(
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
);
|
||||
for mismatch in [false, true] {
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut target = desired.clone();
|
||||
if result == "List(Number)" {
|
||||
let a::Binding::Expression(a::Expr::List(items)) =
|
||||
&mut calculation(&mut target).body
|
||||
else {
|
||||
panic!("list")
|
||||
};
|
||||
items.pop(); // Actual left remains wide; target-only shape differs.
|
||||
} else {
|
||||
target.name = "Different".into();
|
||||
}
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, nodes);
|
||||
if mismatch {
|
||||
pending.desired = ⌖
|
||||
} // Labeled target-only fault after valid emission.
|
||||
std::thread::scope(|scope| {
|
||||
let expected = &expected_bytes;
|
||||
let desired = &desired;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
let result = pending.verify();
|
||||
if mismatch {
|
||||
diagnostic(&failure(result), MISMATCH, 0, 0);
|
||||
} else {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(&accepted.bundle().files["main.arcadia"], expected);
|
||||
assert_eq!(accepted.project(), desired);
|
||||
drop(accepted);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(b, before);
|
||||
}
|
||||
}
|
||||
// Genuine inherited151 profile reaches a late parser refusal inside worker.
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
let pending = limited(&b, profile(17, 151), ProjectLimits::default())
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
std::thread::scope(|scope| {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
diagnostic(
|
||||
&failure(pending.verify()),
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
37,
|
||||
38,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn zero_actual_expression_rejects_large_target_only_variants() {
|
||||
let b = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
|
||||
id(2)
|
||||
));
|
||||
let mut deep = a::Expr::Boolean(true);
|
||||
for _ in 0..2048 {
|
||||
deep = a::Expr::Unary {
|
||||
op: a::UnaryOp::Not,
|
||||
operand: Box::new(deep),
|
||||
};
|
||||
}
|
||||
for expression in [deep, a::Expr::List(vec![a::Expr::Boolean(true); 4096])] {
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut damaged = desired.clone();
|
||||
calculation(&mut damaged).body = a::Binding::Expression(expression);
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, 0);
|
||||
pending.desired = &damaged;
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn signed_zero_geometry_scalar_and_historical_equality_moves_actual_model() {
|
||||
let b = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "next".into();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.0;
|
||||
let accepted = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.project(), &desired);
|
||||
assert_eq!(
|
||||
accepted.project().diagrams[&id(5)].nodes[&id(4)]
|
||||
.position
|
||||
.x
|
||||
.to_bits(),
|
||||
0.0f32.to_bits()
|
||||
);
|
||||
let b = single(&format!(
|
||||
r#"
|
||||
oa_data_type a "A" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
sa_data_type b "B" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
trace t "T" id "{}" {{ oa @a; sa @b; kind Item; baseline {{
|
||||
oa {{ name "A"; description ""; }}; sa {{ name "B"; description ""; }};
|
||||
semantic {{ facets [{{ Data {{ RealMinimum {{ oa {{ Value 0.0; }}; sa {{ Value 0.0; }}; }}; }}; }}]; }};
|
||||
}}; }}
|
||||
"#,
|
||||
id(2),
|
||||
id(3),
|
||||
id(4)
|
||||
));
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
for e in desired.elements.values_mut() {
|
||||
let body = match &mut e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("data")
|
||||
};
|
||||
let crate::DataTypeDefinition::Real { minimum, .. } = definition else {
|
||||
panic!("real")
|
||||
};
|
||||
*minimum = Some(-0.0);
|
||||
*default = Some(crate::DataValue::Real(-0.0));
|
||||
}
|
||||
let facet = &mut desired
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets[0];
|
||||
let crate::semantic::FacetReview::Data(crate::semantic::DataReview::RealMinimum(review)) =
|
||||
facet
|
||||
else {
|
||||
panic!("historical")
|
||||
};
|
||||
review.oa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
review.sa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
let accepted = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.project(), &desired);
|
||||
for e in accepted.project().elements.values() {
|
||||
let body = match &e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition:
|
||||
crate::DataTypeDefinition::Real {
|
||||
minimum: Some(minimum),
|
||||
..
|
||||
},
|
||||
default: Some(crate::DataValue::Real(value)),
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("real")
|
||||
};
|
||||
assert_eq!(minimum.to_bits(), 0.0f64.to_bits());
|
||||
assert_eq!(value.to_bits(), 0.0f64.to_bits());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() {
|
||||
let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into();
|
||||
desired.elements.get_mut(&id(2)).unwrap().alias = "f".into();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
let pending = pending.verify().unwrap();
|
||||
assert_eq!(pending.project(), &desired);
|
||||
assert_eq!(
|
||||
pending.warnings(),
|
||||
leaf_work_warnings("b.arcadia", Span { start: 25, end: 90 }, 2)
|
||||
);
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 6,
|
||||
occurrences: 0,
|
||||
text: 526 + 221
|
||||
}
|
||||
);
|
||||
}
|
||||
+646
@@ -0,0 +1,646 @@
|
||||
use super::*;
|
||||
use crate::Layer;
|
||||
use crate::language::ParseLimits;
|
||||
use crate::language::analysis_source::mixed::{parse_with_profile, project};
|
||||
use project::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
|
||||
fn authored(bundle: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap().index_ordinary().unwrap()
|
||||
}
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
fn limited(
|
||||
bundle: &SourceBundle,
|
||||
limits: Limits,
|
||||
policy: ProjectLimits,
|
||||
) -> AuthoredMixedProject<'_> {
|
||||
project::resolve(parse_with_profile(bundle, policy, limits).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
}
|
||||
fn chain() -> SourceBundle {
|
||||
let mut b = single("include \"part.arcadia\";");
|
||||
b.files.insert(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
);
|
||||
b.files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
b
|
||||
}
|
||||
fn failure<T>(result: Result<T, Failure>, text: &str) -> Failure {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("unexpected success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics.len(), 1);
|
||||
assert!(e.diagnostics[0].diagnostic.message.contains(text), "{e:?}");
|
||||
e
|
||||
}
|
||||
fn same_policy(output: &PendingRename<'_>, limits: Limits, policy: ProjectLimits) {
|
||||
let a = output.limits;
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[
|
||||
limits.bytes,
|
||||
limits.tokens,
|
||||
limits.entries,
|
||||
limits.references,
|
||||
limits.frames
|
||||
]
|
||||
);
|
||||
assert_eq!(a.numeric_units, limits.numeric_units);
|
||||
let a = output.project_limits;
|
||||
assert_eq!(
|
||||
[
|
||||
a.max_files,
|
||||
a.max_include_depth,
|
||||
a.max_include_edges,
|
||||
a.model.max_source_bytes,
|
||||
a.model.max_graph_objects,
|
||||
a.model.max_records,
|
||||
a.model.max_step_references
|
||||
],
|
||||
[
|
||||
policy.max_files,
|
||||
policy.max_include_depth,
|
||||
policy.max_include_edges,
|
||||
policy.model.max_source_bytes,
|
||||
policy.model.max_graph_objects,
|
||||
policy.model.max_records,
|
||||
policy.model.max_step_references
|
||||
]
|
||||
);
|
||||
}
|
||||
const NEXT: &str =
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
const NEXT_CHAIN: &str = "arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"part.arcadia\";\n}\n";
|
||||
#[test]
|
||||
fn exact_real_single_and_chain_overlap_caps_and_complete_owned_bytes() {
|
||||
for (bundle, entries, bytes, before, output_entries, output_text, expected) in [
|
||||
(
|
||||
single(""),
|
||||
17,
|
||||
114,
|
||||
Slots {
|
||||
entries: 16,
|
||||
occurrences: 0,
|
||||
text: 9,
|
||||
},
|
||||
1,
|
||||
105,
|
||||
NEXT,
|
||||
),
|
||||
(
|
||||
chain(),
|
||||
29,
|
||||
216,
|
||||
Slots {
|
||||
entries: 26,
|
||||
occurrences: 0,
|
||||
text: 33,
|
||||
},
|
||||
3,
|
||||
183,
|
||||
NEXT_CHAIN,
|
||||
),
|
||||
] {
|
||||
for (entry_cap, byte_cap, success) in [
|
||||
(entries, bytes, true),
|
||||
(entries - 1, bytes, false),
|
||||
(entries, bytes - 1, false),
|
||||
] {
|
||||
let limits = profile(entry_cap, byte_cap);
|
||||
let policy = ProjectLimits::default();
|
||||
let source = limited(&bundle, limits, policy);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.source.indexed.resolved.storage.live, before);
|
||||
let result = plan.emit_changed_unverified(&bundle);
|
||||
if success {
|
||||
let output = result.unwrap();
|
||||
assert_eq!(
|
||||
output.retained,
|
||||
Slots {
|
||||
entries: output_entries,
|
||||
occurrences: 0,
|
||||
text: output_text
|
||||
}
|
||||
);
|
||||
assert_eq!(output.bundle.entry, "main.arcadia");
|
||||
let mut expected_files = BTreeMap::from([("main.arcadia".into(), expected.into())]);
|
||||
if output_entries == 3 {
|
||||
expected_files.insert(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
);
|
||||
expected_files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
}
|
||||
assert_eq!(output.bundle.files, expected_files);
|
||||
assert_eq!(output.expression_nodes, 0);
|
||||
assert!(std::ptr::eq(output.desired, &desired));
|
||||
same_policy(&output, limits, policy);
|
||||
} else {
|
||||
let e = failure(result, "limit exceeded");
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(e.diagnostics[0].diagnostic.span, Span::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn noop_returns_identical_owner_below_output_overlap_and_releases_desired_borrow() {
|
||||
for (bundle, entries, bytes) in [(single(""), 16, 78), (chain(), 26, 132)] {
|
||||
let source = limited(&bundle, profile(entries, bytes), ProjectLimits::default());
|
||||
let storage = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let numeric = source.indexed.resolved.syntax.budget.numeric_units();
|
||||
let names = source.indexed.data.names.values().next().unwrap().as_ptr();
|
||||
let warnings = source.warnings().to_vec();
|
||||
let result = {
|
||||
let desired = source.project().clone();
|
||||
source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.into_unchanged(&bundle)
|
||||
.unwrap()
|
||||
};
|
||||
assert!(std::ptr::eq(result.bundle(), &bundle));
|
||||
assert_eq!(
|
||||
result.indexed.data.names.values().next().unwrap().as_ptr(),
|
||||
names
|
||||
);
|
||||
assert_eq!(result.indexed.resolved.storage.live, storage);
|
||||
assert_eq!(result.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
result.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(result.warnings(), warnings);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn wrong_branches_and_complete_stale_current_refuse_before_output() {
|
||||
let bundle = chain();
|
||||
let original = authored(&bundle).project().clone();
|
||||
let mut desired = original.clone();
|
||||
desired.alias = "next".into();
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&original)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle),
|
||||
"unchanged",
|
||||
);
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.into_unchanged(&bundle),
|
||||
"changes names",
|
||||
);
|
||||
for case in 0..6 {
|
||||
let mut current = bundle.clone();
|
||||
match case {
|
||||
0 => current.entry = "last.arcadia".into(),
|
||||
1 => current.files.get_mut("last.arcadia").unwrap().push(' '),
|
||||
2 => current.files.get_mut("main.arcadia").unwrap().push(' '),
|
||||
3 => {
|
||||
current.files.remove("last.arcadia");
|
||||
}
|
||||
4 => {
|
||||
current.files.insert("extra.arcadia".into(), String::new());
|
||||
}
|
||||
_ => {
|
||||
let text = current.files.remove("last.arcadia").unwrap();
|
||||
current.files.insert("renamed.arcadia".into(), text);
|
||||
}
|
||||
}
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&original)
|
||||
.unwrap()
|
||||
.into_unchanged(¤t),
|
||||
"Current complete bundle",
|
||||
);
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(¤t),
|
||||
"Current complete bundle",
|
||||
);
|
||||
}
|
||||
let current = bundle.clone();
|
||||
assert_eq!(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(¤t)
|
||||
.unwrap()
|
||||
.bundle
|
||||
.files["main.arcadia"],
|
||||
NEXT_CHAIN
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn existing_edit_validation_runs_before_both_transitions() {
|
||||
let bundle = single("");
|
||||
let original = authored(&bundle).project().clone();
|
||||
let mut desired = original.clone();
|
||||
desired.alias = "next".into();
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.clear();
|
||||
failure(
|
||||
plan.emit_changed_unverified(&bundle),
|
||||
"missing a required target token",
|
||||
);
|
||||
let mut plan = authored(&bundle).plan_rename(&original).unwrap();
|
||||
plan.edits.insert((0, 19, 20), Target::Global(id(1)));
|
||||
failure(plan.into_unchanged(&bundle), "unchanged name");
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.clear();
|
||||
plan.edits
|
||||
.insert((0, usize::MAX, usize::MAX), Target::Global(id(1)));
|
||||
failure(plan.emit_changed_unverified(&bundle), "nonempty");
|
||||
// Arithmetic-only fault injection, not a real bundle large enough to overflow.
|
||||
assert!(output_slots(&bundle, usize::MAX).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn exact_all_policy_fields_and_pending_outlives_original_bundle() {
|
||||
let desired = {
|
||||
let b = single("");
|
||||
let mut p = authored(&b).project().clone();
|
||||
p.alias = "next".into();
|
||||
p
|
||||
};
|
||||
let limits = Limits {
|
||||
bytes: 2048,
|
||||
tokens: 101,
|
||||
entries: 99,
|
||||
references: 31,
|
||||
frames: 17,
|
||||
numeric_units: 8192,
|
||||
};
|
||||
let policy = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 3,
|
||||
max_include_edges: 5,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 3000,
|
||||
max_graph_objects: 67,
|
||||
max_records: 123,
|
||||
max_step_references: 37,
|
||||
},
|
||||
};
|
||||
let pending = {
|
||||
let b = single("");
|
||||
limited(&b, limits, policy)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
same_policy(&pending, limits, policy);
|
||||
}
|
||||
#[test]
|
||||
fn scoped_small_stack_changed_unchanged_and_failure_drop() {
|
||||
let bundle = single("");
|
||||
let original = authored(&bundle).project().clone();
|
||||
let mut changed = original.clone();
|
||||
changed.alias = "next".into();
|
||||
std::thread::scope(|scope| {
|
||||
for mode in 0..3 {
|
||||
let plan = limited(
|
||||
&bundle,
|
||||
profile(17, if mode == 2 { 113 } else { 114 }),
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.plan_rename(if mode == 1 { &original } else { &changed })
|
||||
.unwrap();
|
||||
let b = &bundle;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
if mode == 1 {
|
||||
let owner = plan.into_unchanged(b).unwrap();
|
||||
assert!(std::ptr::eq(owner.bundle(), b));
|
||||
drop(owner);
|
||||
} else if mode == 0 {
|
||||
let pending = plan.emit_changed_unverified(b).unwrap();
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
drop(pending);
|
||||
} else {
|
||||
drop(failure(plan.emit_changed_unverified(b), "limit exceeded"));
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let body = format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000002\" {{ result {result}; body {expression}; }}"
|
||||
);
|
||||
let bundle = single(&body);
|
||||
let before = bundle.clone();
|
||||
let expected = format!(
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
);
|
||||
for changed in [false, true] {
|
||||
// Complete prerequisites stay outside the bounded emitter worker.
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
|
||||
let mut desired = source.project().clone();
|
||||
if changed {
|
||||
desired.alias = "next".into();
|
||||
}
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
std::thread::scope(|scope| {
|
||||
let bundle = &bundle;
|
||||
let expected = &expected;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
if changed {
|
||||
let pending = plan.emit_changed_unverified(bundle).unwrap();
|
||||
assert_eq!(pending.expression_nodes, nodes);
|
||||
assert_eq!(&pending.bundle.files["main.arcadia"], expected);
|
||||
drop(pending);
|
||||
} else {
|
||||
let owner = plan.into_unchanged(bundle).unwrap();
|
||||
assert!(std::ptr::eq(owner.bundle(), bundle));
|
||||
assert_eq!(owner.indexed.resolved.expressions.len(), nodes);
|
||||
drop(owner);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(bundle, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn rich_desired(layer: Layer) -> Project {
|
||||
let mut p = rich_model(layer);
|
||||
p.alias = "w".into();
|
||||
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
|
||||
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
|
||||
let d = p.diagrams.get_mut(&id(5)).unwrap();
|
||||
d.alias = "z".into();
|
||||
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
|
||||
for (n, name) in [(10, "g"), (11, "f")] {
|
||||
let def = p.definitions.get_mut(&id(n)).unwrap();
|
||||
def.alias = name.into();
|
||||
let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c.parameters[0].name = "y".into();
|
||||
c.parameters[1].name = "x".into();
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
|
||||
}
|
||||
p
|
||||
}
|
||||
// Independently authored entire expected files: no edit iterator or output text
|
||||
// contributes to these strings. The LF/CRLF transform applies to whole literals.
|
||||
const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n";
|
||||
const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n";
|
||||
fn rich_expected(layer: Layer) -> SourceBundle {
|
||||
let (property, work, diagram, extra) = if layer == Layer::Oa {
|
||||
("oa_property", "oa_activity", "", "")
|
||||
} else {
|
||||
(
|
||||
"sa_property",
|
||||
"sa_function",
|
||||
"layer Sa;",
|
||||
"sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}",
|
||||
)
|
||||
};
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("a.arcadia".into(), RICH_A.into()),
|
||||
("b.arcadia".into(), RICH_B.into()),
|
||||
(
|
||||
"main.arcadia".into(),
|
||||
format!(
|
||||
"arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n"
|
||||
),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn whole_rich_oa_sa_scoped_global_swaps_and_unicode_crlf_bytes() {
|
||||
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
|
||||
for unicode in [false, true] {
|
||||
let mut bundle = rich_bundle(layer);
|
||||
let mut expected = rich_expected(layer);
|
||||
if unicode {
|
||||
for text in bundle.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
for text in expected.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let desired = rich_desired(layer);
|
||||
let source = authored(&bundle);
|
||||
let original_project = source.project().clone();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(pending.expression_nodes, 15);
|
||||
let count = if unicode { unicode_bytes } else { bytes };
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: count + 42
|
||||
}
|
||||
);
|
||||
assert_eq!(authored(&bundle).project(), &original_project);
|
||||
assert_eq!(desired, rich_desired(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() {
|
||||
let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into();
|
||||
desired.elements.get_mut(&id(2)).unwrap().alias = "f".into();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 4,
|
||||
occurrences: 0,
|
||||
text: 526
|
||||
}
|
||||
);
|
||||
assert_eq!(pending.expression_nodes, 2);
|
||||
}
|
||||
const CATEGORY_EXPECTED: &str = r#"arcadia 8;
|
||||
project a "Authored @p child" id "00000000-0000-0000-0000-000000000001" {
|
||||
description "Do not index @child, @link or uuid in prose 雨";
|
||||
oa_activity p "Parent child" id "00000000-0000-0000-0000-000000000002" {
|
||||
oa_activity implicit "Explicit child" id "00000000-0000-0000-0000-000000000003" { parent @ // @noise child
|
||||
p; }
|
||||
oa_activity child "Implicit child" id "00000000-0000-0000-0000-000000000004" { description "child @a"; }
|
||||
}
|
||||
sa_function link "Selected" id "00000000-0000-0000-0000-000000000005" {}
|
||||
interaction s "Link" id "00000000-0000-0000-0000-000000000006" { source @implicit; target @child; }
|
||||
diagram note "View" id "00000000-0000-0000-0000-000000000007" {
|
||||
routes [{ edge @s; }];
|
||||
view_options { hidden_edges [@s]; };
|
||||
}
|
||||
artifact d "Evidence" id "00000000-0000-0000-0000-000000000008" { owner @s; uri "docs/@child.md"; }
|
||||
trace q "Review" id "00000000-0000-0000-0000-000000000009" {
|
||||
oa @implicit; sa @link; kind Manual;
|
||||
baseline {
|
||||
oa { name "child"; description "@child"; };
|
||||
sa { name "s"; description "@s"; };
|
||||
semantic { facets [{ Field { Type { oa { Value { Named uuid "00000000-0000-0000-0000-000000000003"; }; }; sa { Value Boolean; }; }; }; }]; };
|
||||
};
|
||||
}
|
||||
oa_process t "Caller" id "00000000-0000-0000-0000-000000000010" { steps [@sub, @left]; }
|
||||
oa_process use "Callee" id "00000000-0000-0000-0000-000000000011" { steps [@right]; }
|
||||
oa_occurrence sub "Call" id "00000000-0000-0000-0000-000000000012" { parent @t; body { SequenceReference { sequence @use; }; }; }
|
||||
oa_occurrence right "Left" id "00000000-0000-0000-0000-000000000013" { parent @use; body { Action { action @implicit; lifeline none; }; }; }
|
||||
oa_occurrence left "Right" id "00000000-0000-0000-0000-000000000014" { parent @t; body { Action { action @child; lifeline none; }; }; }
|
||||
contextual_link ctx "Context" id "00000000-0000-0000-0000-000000000015" { owner @t; source { references [@sub]; occurrence @right; }; target { references []; occurrence @left; }; purpose { Precedence { guard "@child"; }; }; }
|
||||
}
|
||||
"#;
|
||||
#[test]
|
||||
fn complete_categories_preserve_trivia_implicit_parent_and_historical_uuid_bytes() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
include_str!("../../tests/fixtures/categories.arcadia").into(),
|
||||
)]),
|
||||
};
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
let replacements = [
|
||||
"a", "p", "implicit", "child", "link", "s", "note", "d", "q", "t", "use", "sub", "right",
|
||||
"left", "ctx",
|
||||
];
|
||||
desired.alias = "a".into();
|
||||
for e in desired.elements.values_mut() {
|
||||
e.alias = replacements[(1..=15).find(|n| id(*n) == e.id).unwrap() - 1].into();
|
||||
}
|
||||
for r in desired.relations.values_mut() {
|
||||
r.alias = replacements[(1..=15).find(|n| id(*n) == r.id).unwrap() - 1].into();
|
||||
}
|
||||
desired.diagrams.get_mut(&id(7)).unwrap().alias = "note".into();
|
||||
desired.artifact_links.get_mut(&id(8)).unwrap().alias = "d".into();
|
||||
desired.traces.get_mut(&id(9)).unwrap().alias = "q".into();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
let expected = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
format!(
|
||||
"{}\r\n",
|
||||
CATEGORY_EXPECTED
|
||||
.trim_end_matches('\n')
|
||||
.replace("@noise child\n", "@noise child\r\n")
|
||||
),
|
||||
)]),
|
||||
};
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 2169
|
||||
}
|
||||
);
|
||||
assert_eq!(pending.expression_nodes, 0);
|
||||
}
|
||||
#[test]
|
||||
fn combined_growth_shrinkage_emits_original_coordinates_and_complete_untouched_file() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("main.arcadia").unwrap() =
|
||||
bundle.files["main.arcadia"].replace("node n ", "node nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn ");
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.alias = "n".into();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = "renamed_f".into();
|
||||
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation renamed_f \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput x id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput y id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $x + $y;\n}\n".into()),
|
||||
("b.arcadia".into(),"calculation g \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput x id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput y id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $x - $y;\n}\n".into()),
|
||||
("main.arcadia".into(),"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode n id \"00000000-0000-0000-0000-000000000004\" { diagram @d; element @v; }\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @renamed_f(y: 2, x: @g(x: 3, y: 4)) + @renamed_f(x: 5, y: 6); }\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\ndiagram d \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" { }\n\n}\n".into()),
|
||||
])};
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.output_bytes().unwrap(), 924);
|
||||
let pending = plan.emit_changed_unverified(&bundle).unwrap();
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: 966
|
||||
}
|
||||
);
|
||||
}
|
||||
+982
@@ -0,0 +1,982 @@
|
||||
use super::super::super::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::language::analysis_source::mixed::parse_with_profile;
|
||||
use crate::source_project::ProjectLimits;
|
||||
use crate::{Layer, analysis as a};
|
||||
|
||||
fn authored(bundle: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap().index_ordinary().unwrap()
|
||||
}
|
||||
fn calculation(p: &mut Project, n: usize) -> &mut a::CalculationDefinition {
|
||||
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(n)).unwrap().body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c
|
||||
}
|
||||
fn renamed(layer: Layer) -> Project {
|
||||
let mut p = rich_model(layer);
|
||||
p.alias = "w".into();
|
||||
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
|
||||
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
|
||||
let d = p.diagrams.get_mut(&id(5)).unwrap();
|
||||
d.alias = "z".into();
|
||||
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
|
||||
for (n, name) in [(10, "g"), (11, "f")] {
|
||||
p.definitions.get_mut(&id(n)).unwrap().alias = name.into();
|
||||
let c = calculation(&mut p, n);
|
||||
c.parameters[0].name = "y".into();
|
||||
c.parameters[1].name = "x".into();
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
|
||||
}
|
||||
p
|
||||
}
|
||||
fn rejected(bundle: &SourceBundle, desired: &Project, contains: &str) {
|
||||
let e = match authored(bundle).plan_rename(desired) {
|
||||
Ok(_) => panic!("unexpected accepted target"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics.len(), 1);
|
||||
assert!(
|
||||
e.diagnostics[0].diagnostic.message.contains(contains),
|
||||
"{:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn noop_borrows_exact_target_and_preserves_source_usage_and_storage() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let source = authored(&bundle);
|
||||
let desired = rich_model(Layer::Oa);
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let numeric = source.indexed.resolved.syntax.budget.numeric_units();
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert!(std::ptr::eq(plan.desired(), &desired));
|
||||
assert!(std::ptr::eq(plan.source().bundle(), &bundle));
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(plan.changed_names().is_empty());
|
||||
assert_eq!(plan.source.indexed.resolved.storage.live, before);
|
||||
assert_eq!(plan.source.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(desired, rich_model(Layer::Oa));
|
||||
assert_eq!(plan.source.project(), &desired);
|
||||
assert_eq!(plan.output_bytes().unwrap(), 900);
|
||||
}
|
||||
#[test]
|
||||
fn final_names_reject_unchanged_collisions_invalid_names_and_scoped_collisions() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
for alias in ["w", "", "雨", "project", "bad-name"] {
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = alias.into();
|
||||
rejected(
|
||||
&bundle,
|
||||
&desired,
|
||||
if alias == "w" { "collide" } else { "Invalid" },
|
||||
);
|
||||
}
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
calculation(&mut desired, 10).parameters[0].name = "y".into();
|
||||
rejected(&bundle, &desired, "collide");
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
calculation(&mut desired, 10).parameters[0].name = "p".into();
|
||||
calculation(&mut desired, 11).parameters[0].name = "p".into();
|
||||
let plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.changed_names().len(), 2);
|
||||
}
|
||||
#[test]
|
||||
fn complete_model_gate_rejects_identity_collection_parameter_and_payload_changes() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
type Mutation = Box<dyn Fn(&mut Project)>;
|
||||
let mutations: Vec<Mutation> = vec![
|
||||
Box::new(|p| p.schema_version = 7),
|
||||
Box::new(|p| p.id = id(99)),
|
||||
Box::new(|p| p.name.push('!')),
|
||||
Box::new(|p| p.description.push('!')),
|
||||
Box::new(|p| {
|
||||
p.elements.remove(&id(2));
|
||||
}),
|
||||
Box::new(|p| {
|
||||
let e = p.elements.remove(&id(2)).unwrap();
|
||||
p.elements.insert(id(99), e);
|
||||
}),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().parent = Some(id(3))),
|
||||
Box::new(|p| {
|
||||
p.elements.get_mut(&id(2)).unwrap().body =
|
||||
crate::ElementBody::Sa(crate::SaBody::Function)
|
||||
}),
|
||||
Box::new(|p| p.definitions.get_mut(&id(10)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.definitions.get_mut(&id(10)).unwrap().name.push('!')),
|
||||
Box::new(|p| {
|
||||
p.definitions
|
||||
.get_mut(&id(10))
|
||||
.unwrap()
|
||||
.description
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).parameters.swap(0, 1)),
|
||||
Box::new(|p| {
|
||||
calculation(p, 10).parameters.pop();
|
||||
}),
|
||||
Box::new(|p| {
|
||||
let q = calculation(p, 10).parameters[0].clone();
|
||||
calculation(p, 10).parameters.push(q);
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).parameters[0].id = a::ParameterId(id(99))),
|
||||
Box::new(|p| {
|
||||
calculation(p, 10).parameters[0].value_type =
|
||||
a::AnalysisType::Scalar(a::ScalarType::Boolean)
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).result = a::AnalysisType::Scalar(a::ScalarType::Boolean)),
|
||||
Box::new(|p| calculation(p, 10).body = a::Binding::Unbound),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().layer = Layer::Sa),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(id(99))
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams.get_mut(&id(5)).unwrap().nodes.clear();
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.id = id(99)
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.element = id(2)
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = 1.
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.size
|
||||
.width = 1.
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.collapsed = true
|
||||
}),
|
||||
];
|
||||
for mutate in mutations {
|
||||
let mut p = rich_model(Layer::Oa);
|
||||
mutate(&mut p);
|
||||
rejected(&bundle, &p, "non-name");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn complete_current_bundle_rejects_entry_paths_missing_extra_and_untouched_bytes() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "renamed".into();
|
||||
let plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.validate_current(&bundle).unwrap();
|
||||
for case in 0..6 {
|
||||
let mut current = bundle.clone();
|
||||
match case {
|
||||
0 => current.entry = "a.arcadia".into(),
|
||||
1 => {
|
||||
current.files.remove("a.arcadia");
|
||||
}
|
||||
2 => {
|
||||
current.files.insert("extra.arcadia".into(), String::new());
|
||||
}
|
||||
3 => current.files.get_mut("a.arcadia").unwrap().push(' '),
|
||||
4 => current.files.get_mut("main.arcadia").unwrap().push(' '),
|
||||
_ => {
|
||||
let s = current.files.remove("a.arcadia").unwrap();
|
||||
current.files.insert("renamed.arcadia".into(), s);
|
||||
}
|
||||
}
|
||||
assert!(plan.validate_current(¤t).is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn exact_merged_map_cap_passes_all_predecessors_and_retains_r_plus_m() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let desired = renamed(Layer::Oa);
|
||||
let baseline = authored(&bundle).indexed.resolved.storage.live;
|
||||
for (extra, fits) in [(37, false), (38, true)] {
|
||||
let profile = Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
assert_eq!(source.indexed.resolved.storage.live, baseline);
|
||||
let result = source.plan_rename(&desired);
|
||||
if fits {
|
||||
let plan = result.unwrap();
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
baseline.add(Slots::entries(38), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("cap accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 412,
|
||||
end: 413
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn damaged_complete_edits_fail_missing_extra_wrong_target_and_utf8_authority() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
for s in bundle.files.values_mut() {
|
||||
*s = format!("// 雨\r\n{s}");
|
||||
}
|
||||
let desired = renamed(Layer::Oa);
|
||||
for case in 0..7 {
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
let key = *plan.edits.keys().next().unwrap();
|
||||
match case {
|
||||
0 => {
|
||||
plan.edits.remove(&key);
|
||||
}
|
||||
1 => {
|
||||
plan.edits.insert((0, 0, 1), Target::Global(id(10)));
|
||||
}
|
||||
2 => {
|
||||
plan.edits.insert(key, Target::Global(id(11)));
|
||||
}
|
||||
3 => {
|
||||
plan.edits.insert((0, 4, 5), Target::Global(id(10)));
|
||||
}
|
||||
4 => {
|
||||
plan.edits
|
||||
.insert((99, key.1, key.2), Target::Global(id(10)));
|
||||
}
|
||||
5 => {
|
||||
plan.edits
|
||||
.insert((0, key.1, key.2 + 1), Target::Global(id(10)));
|
||||
}
|
||||
_ => {
|
||||
plan.edits.insert((0, key.1, key.1), Target::Global(id(10)));
|
||||
}
|
||||
}
|
||||
let failure = plan.validate().unwrap_err();
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
if case == 3 || case == 4 {
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].path,
|
||||
if case == 3 {
|
||||
"a.arcadia"
|
||||
} else {
|
||||
"main.arcadia"
|
||||
}
|
||||
);
|
||||
}
|
||||
if case == 0 {
|
||||
assert_eq!(failure.diagnostics[0].path, "a.arcadia");
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span,
|
||||
Span { start: 20, end: 21 }
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Rename edit set is missing a required target token."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn categories() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
include_str!("../tests/fixtures/categories.arcadia").into(),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn every_ordinary_record_field_group_is_fixed_including_historical_payloads() {
|
||||
let bundle = categories();
|
||||
let original = authored(&bundle).project().clone();
|
||||
type Mutation = Box<dyn Fn(&mut Project)>;
|
||||
let changes: Vec<Mutation> = vec![
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().description.push('!')),
|
||||
Box::new(|p| {
|
||||
let body = p.relations[&id(15)].body.clone();
|
||||
p.relations.get_mut(&id(6)).unwrap().body = body;
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.relations.remove(&id(6));
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams.get_mut(&id(7)).unwrap().projection = crate::ProjectionKind::Allocation
|
||||
}),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(7)).unwrap().routes.clear()),
|
||||
Box::new(|p| {
|
||||
p.diagrams.remove(&id(7));
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().name.push('!')),
|
||||
Box::new(|p| {
|
||||
p.artifact_links
|
||||
.get_mut(&id(8))
|
||||
.unwrap()
|
||||
.description
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().owner = id(3)),
|
||||
Box::new(|p| {
|
||||
p.artifact_links.get_mut(&id(8)).unwrap().relation = crate::ArtifactRelation::Verifies
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().uri.push('!')),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().revision = Some("changed".into())),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().fragment = Some("changed".into())),
|
||||
Box::new(|p| {
|
||||
p.artifact_links.remove(&id(8));
|
||||
}),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().oa = id(4)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().sa = id(4)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().kind = crate::TraceKind::Refinement),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().baseline = None),
|
||||
Box::new(|p| {
|
||||
p.traces
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = None
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.traces
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.oa
|
||||
.name
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.traces.remove(&id(9));
|
||||
}),
|
||||
];
|
||||
for change in changes {
|
||||
let mut p = original.clone();
|
||||
change(&mut p);
|
||||
rejected(&bundle, &p, "non-name");
|
||||
}
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
drop(source.plan_rename(&original).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn combined_shrink_offsets_typed_growth_and_counts_untouched_files() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main.replace("node n ", &format!("node {} ", "n".repeat(31)));
|
||||
assert_eq!(bundle.files.values().map(String::len).sum::<usize>(), 930);
|
||||
for (name, expected) in [("renamed_f", Some(924)), ("renamed_func", None)] {
|
||||
let profile = Limits {
|
||||
bytes: 930,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
let mut desired = source.project().clone();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.alias = "n".into();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = name.into();
|
||||
let result = source.plan_rename(&desired);
|
||||
if let Some(bytes) = expected {
|
||||
let plan = result.unwrap();
|
||||
assert_eq!(plan.output_bytes().unwrap(), bytes);
|
||||
assert_eq!(plan.edits().len(), 4);
|
||||
} else {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("oversize accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn deep_and_wide_equality_and_late_refusal_drop_on_small_stack() {
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
|
||||
id(2)
|
||||
));
|
||||
for mode in 0..3 {
|
||||
// Admission/intent construction are prerequisites, outside the
|
||||
// bounded planner worker. No limits or live owner state are reset.
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
|
||||
let mut desired = source.project().clone();
|
||||
match mode {
|
||||
0 => {}
|
||||
1 => desired.definitions.get_mut(&id(2)).unwrap().alias = "p".into(),
|
||||
_ => calculation(&mut desired, 2).body = a::Binding::Unbound,
|
||||
}
|
||||
std::thread::scope(|scope| {
|
||||
let desired = &desired;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
// The complete owner moves into this worker; successful
|
||||
// plans and consumed failure owners die here. Nothing
|
||||
// carrying source/edit authority escapes the worker.
|
||||
match source.plan_rename(desired) {
|
||||
Ok(plan) => {
|
||||
assert_eq!(mode, 0, "mutation unexpectedly accepted");
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(std::ptr::eq(plan.desired(), desired));
|
||||
drop(plan);
|
||||
}
|
||||
Err(failure) => {
|
||||
assert_ne!(mode, 0, "unchanged target unexpectedly refused");
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
if mode == 1 {
|
||||
"Rename aliases collide in their final namespace."
|
||||
} else {
|
||||
"Rename target changes non-name project semantics."
|
||||
}
|
||||
);
|
||||
drop(failure);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
drop(desired);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn zero_expression_original_rejects_target_only_deep_and_wide_variants() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
let original = source.project().clone();
|
||||
let mut deep = a::Expr::Boolean(true);
|
||||
for _ in 0..2048 {
|
||||
deep = a::Expr::Unary {
|
||||
op: a::UnaryOp::Not,
|
||||
operand: Box::new(deep),
|
||||
};
|
||||
}
|
||||
for expression in [deep, a::Expr::List(vec![a::Expr::Boolean(true); 4096])] {
|
||||
let mut desired = original.clone();
|
||||
calculation(&mut desired, 2).body = a::Binding::Expression(expression);
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn signed_zero_geometry_uses_existing_semantic_equality() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.;
|
||||
assert!(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.edits()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
fn parameter(c: usize, p: usize) -> Target {
|
||||
Target::Parameter {
|
||||
calculation: id(c),
|
||||
parameter: a::ParameterId(id(p)),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_entire_rich_maps_oa_sa_unicode_crlf_and_borrowed_names() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for crlf in [false, true] {
|
||||
let mut bundle = rich_bundle(layer);
|
||||
if crlf {
|
||||
for source in bundle.files.values_mut() {
|
||||
*source = format!("// 雨\r\n{}", source.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let desired = renamed(layer);
|
||||
let source = authored(&bundle);
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
let mut names = BTreeMap::from([
|
||||
(Target::Global(id(1)), "w"),
|
||||
(Target::Global(id(2)), "p"),
|
||||
(Target::Global(id(3)), "n"),
|
||||
(Target::Global(id(4)), "v"),
|
||||
(Target::Global(id(5)), "z"),
|
||||
(Target::Global(id(10)), "g"),
|
||||
(Target::Global(id(11)), "f"),
|
||||
(parameter(10, 1), "y"),
|
||||
(parameter(10, 12), "x"),
|
||||
(parameter(11, 1), "y"),
|
||||
(parameter(11, 12), "x"),
|
||||
]);
|
||||
if layer == Layer::Sa {
|
||||
names.insert(Target::Global(id(6)), "u");
|
||||
}
|
||||
let mut edits = BTreeMap::new();
|
||||
let offsets = if crlf {
|
||||
[20, 77, 138, 214, 219]
|
||||
} else {
|
||||
[12, 68, 128, 203, 208]
|
||||
};
|
||||
for (file, calculation) in [(0, 10), (1, 11)] {
|
||||
for (start, target) in offsets.into_iter().zip([
|
||||
Target::Global(id(calculation)),
|
||||
parameter(calculation, 1),
|
||||
parameter(calculation, 12),
|
||||
parameter(calculation, 1),
|
||||
parameter(calculation, 12),
|
||||
]) {
|
||||
edits.insert((file, start, start + 1), target);
|
||||
}
|
||||
}
|
||||
let offsets = if crlf {
|
||||
[
|
||||
28, 133, 188, 200, 218, 281, 303, 305, 311, 315, 317, 323, 333, 335, 341, 363,
|
||||
426,
|
||||
]
|
||||
} else {
|
||||
[
|
||||
19, 122, 177, 189, 206, 269, 291, 293, 299, 303, 305, 311, 321, 323, 329, 350,
|
||||
412,
|
||||
]
|
||||
};
|
||||
let g = |n| Target::Global(id(n));
|
||||
for (start, target) in offsets.into_iter().zip([
|
||||
g(1),
|
||||
g(4),
|
||||
g(5),
|
||||
g(3),
|
||||
g(3),
|
||||
g(2),
|
||||
g(10),
|
||||
parameter(10, 12),
|
||||
parameter(10, 1),
|
||||
g(11),
|
||||
parameter(11, 1),
|
||||
parameter(11, 12),
|
||||
g(10),
|
||||
parameter(10, 1),
|
||||
parameter(10, 12),
|
||||
g(2),
|
||||
g(5),
|
||||
]) {
|
||||
edits.insert((2, start, start + 1), target);
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
let start = if crlf { 505 } else { 490 };
|
||||
edits.insert((2, start, start + 1), g(6));
|
||||
}
|
||||
assert_eq!(plan.changed_names(), &names);
|
||||
assert_eq!(plan.edits(), &edits);
|
||||
let delta = if layer == Layer::Oa { 38 } else { 40 };
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(delta), Span::default()).unwrap()
|
||||
);
|
||||
assert_eq!(plan.source.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert!(std::ptr::eq(plan.desired(), &desired));
|
||||
assert_eq!(desired, renamed(layer));
|
||||
assert_eq!(plan.source.project(), &rich_model(layer));
|
||||
assert!(std::ptr::eq(
|
||||
plan.names[&g(1)].as_ptr(),
|
||||
desired.alias.as_ptr()
|
||||
));
|
||||
assert_eq!(
|
||||
plan.output_bytes().unwrap(),
|
||||
match (layer, crlf) {
|
||||
(Layer::Oa, false) => 900,
|
||||
(Layer::Oa, true) => 943,
|
||||
(Layer::Sa, false) => 974,
|
||||
(Layer::Sa, true) => 1017,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_entire_category_map_includes_comment_separated_refs_and_excludes_history() {
|
||||
let bundle = categories();
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
let replacements = [
|
||||
"a", "p", "implicit", "child", "link", "s", "note", "d", "q", "t", "use", "sub", "right",
|
||||
"left", "ctx",
|
||||
];
|
||||
desired.alias = replacements[0].into();
|
||||
for e in desired.elements.values_mut() {
|
||||
e.alias = replacements[(1..=15).find(|n| id(*n) == e.id).unwrap() - 1].into();
|
||||
}
|
||||
for r in desired.relations.values_mut() {
|
||||
r.alias = replacements[(1..=15).find(|n| id(*n) == r.id).unwrap() - 1].into();
|
||||
}
|
||||
desired.diagrams.get_mut(&id(7)).unwrap().alias = "note".into();
|
||||
desired.artifact_links.get_mut(&id(8)).unwrap().alias = "d".into();
|
||||
desired.traces.get_mut(&id(9)).unwrap().alias = "q".into();
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
let spans: &[(usize, &[(usize, usize)])] = &[
|
||||
(1, &[(19, 20)]),
|
||||
(2, &[(168, 169), (344, 345)]),
|
||||
(3, &[(249, 254), (625, 630), (992, 997), (1741, 1746)]),
|
||||
(4, &[(369, 377), (640, 648), (1882, 1890)]),
|
||||
(5, &[(487, 488), (1003, 1004)]),
|
||||
(6, &[(561, 565), (741, 745), (789, 793), (885, 889)]),
|
||||
(7, &[(664, 665)]),
|
||||
(8, &[(818, 822)]),
|
||||
(9, &[(925, 926)]),
|
||||
(
|
||||
10,
|
||||
&[(1328, 1329), (1577, 1578), (1855, 1856), (2004, 2005)],
|
||||
),
|
||||
(11, &[(1422, 1425), (1617, 1620), (1712, 1715)]),
|
||||
(12, &[(1391, 1394), (1514, 1517), (2029, 2032)]),
|
||||
(13, &[(1487, 1491), (1648, 1652), (2047, 2051)]),
|
||||
(14, &[(1397, 1402), (1789, 1794), (2092, 2097)]),
|
||||
(15, &[(1935, 1942)]),
|
||||
];
|
||||
let expected: BTreeMap<_, _> = spans
|
||||
.iter()
|
||||
.flat_map(|(n, spans)| {
|
||||
spans
|
||||
.iter()
|
||||
.map(move |&(start, end)| ((0, start, end), Target::Global(id(*n))))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(plan.edits(), &expected);
|
||||
let names: BTreeMap<_, _> = replacements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &name)| (Target::Global(id(i + 1)), name))
|
||||
.collect();
|
||||
assert_eq!(plan.changed_names(), &names);
|
||||
assert_eq!(plan.output_bytes().unwrap(), 2145);
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(51), Span::default()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_zero_scalar_bounds_defaults_and_historical_facets_preserve_original_bytes() {
|
||||
let bundle = single(&format!(
|
||||
r#"
|
||||
oa_data_type a "A" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
sa_data_type b "B" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
trace t "T" id "{}" {{ oa @a; sa @b; kind Item; baseline {{
|
||||
oa {{ name "A"; description ""; }}; sa {{ name "B"; description ""; }};
|
||||
semantic {{ facets [{{ Data {{ RealMinimum {{ oa {{ Value 0.0; }}; sa {{ Value 0.0; }}; }}; }}; }}]; }};
|
||||
}}; }}
|
||||
"#,
|
||||
id(2),
|
||||
id(3),
|
||||
id(4)
|
||||
));
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
for e in desired.elements.values_mut() {
|
||||
let body = match &mut e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("data")
|
||||
};
|
||||
let crate::DataTypeDefinition::Real { minimum, .. } = definition else {
|
||||
panic!("real")
|
||||
};
|
||||
*minimum = Some(-0.0);
|
||||
*default = Some(crate::DataValue::Real(-0.0));
|
||||
}
|
||||
let facet = &mut desired
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets[0];
|
||||
let crate::semantic::FacetReview::Data(crate::semantic::DataReview::RealMinimum(review)) =
|
||||
facet
|
||||
else {
|
||||
panic!("historical real")
|
||||
};
|
||||
review.oa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
review.sa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(std::ptr::eq(plan.source.bundle(), &bundle));
|
||||
}
|
||||
#[test]
|
||||
fn nonzero_expression_lists_calls_and_definition_payloads_reject_changes() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result List(Number); body [1,2]; }}",
|
||||
id(2)
|
||||
));
|
||||
let original = authored(&bundle).project().clone();
|
||||
for bigger in [false, true] {
|
||||
let mut desired = original.clone();
|
||||
let a::Binding::Expression(a::Expr::List(items)) = &mut calculation(&mut desired, 2).body
|
||||
else {
|
||||
panic!("list")
|
||||
};
|
||||
if bigger {
|
||||
items.push(a::Expr::Boolean(true));
|
||||
} else {
|
||||
items.pop();
|
||||
}
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
let crate::ElementBody::Oa(crate::OaBody::Shared(crate::SharedBody::EngineeringProperty {
|
||||
base: a::Binding::Expression(a::Expr::Binary { left, .. }),
|
||||
..
|
||||
})) = &mut desired.elements.get_mut(&id(3)).unwrap().body
|
||||
else {
|
||||
panic!("property")
|
||||
};
|
||||
let a::Expr::Call { arguments, .. } = left.as_mut() else {
|
||||
panic!("call")
|
||||
};
|
||||
arguments.remove(&a::ParameterId(id(1)));
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.port_anchors
|
||||
.push(crate::PortAnchor {
|
||||
port: id(99),
|
||||
side: crate::PortAnchorSide::Left,
|
||||
offset: 0.,
|
||||
});
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
// Opaque quantity/unit payload comparison is tested directly, independently
|
||||
// of admission's semantic dimension rules; these are gate-level controls.
|
||||
let mut old = rich_model(Layer::Oa);
|
||||
old.definitions.get_mut(&id(10)).unwrap().body =
|
||||
a::DefinitionBody::QuantityKind(a::QuantityKindDefinition {
|
||||
dimension: crate::analysis_numeric::Dimension::ZERO,
|
||||
algebra: a::QuantityAlgebra::Linear,
|
||||
});
|
||||
let mut changed = old.clone();
|
||||
let a::DefinitionBody::QuantityKind(q) =
|
||||
&mut changed.definitions.get_mut(&id(10)).unwrap().body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
q.algebra = a::QuantityAlgebra::Affine {
|
||||
difference: a::DefinitionRef::Local(id(11)),
|
||||
};
|
||||
assert!(!same_project(&old, &changed));
|
||||
old.definitions.get_mut(&id(10)).unwrap().body = a::DefinitionBody::Unit(a::UnitDefinition {
|
||||
kind: a::DefinitionRef::Local(id(11)),
|
||||
scale: crate::analysis_numeric::Exact::from_integer(1),
|
||||
offset: crate::analysis_numeric::Exact::from_integer(0),
|
||||
symbol: "u".into(),
|
||||
});
|
||||
let mut changed = old.clone();
|
||||
let a::DefinitionBody::Unit(u) = &mut changed.definitions.get_mut(&id(10)).unwrap().body else {
|
||||
unreachable!()
|
||||
};
|
||||
u.symbol.push('x');
|
||||
assert!(!same_project(&old, &changed));
|
||||
assert!(!same_project(&old, &rich_model(Layer::Oa)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_spelling_different_callee_and_unchanged_target_corruptions_are_rejected() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let desired = renamed(Layer::Oa);
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.insert((2, 299, 300), parameter(11, 1));
|
||||
let e = plan.validate().unwrap_err();
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 299,
|
||||
end: 300
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Rename edit set is missing a required target token."
|
||||
);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "new_project".into();
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.insert((2, 122, 123), Target::Global(id(4)));
|
||||
let e = plan.validate().unwrap_err();
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 122,
|
||||
end: 123
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Rename token targets an unchanged name."
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn included_file_last_reservation_preserves_its_canonical_diagnostic() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let start = main.find("diagram d ").unwrap();
|
||||
let end = start + main[start..].find('\n').unwrap() + 1;
|
||||
let diagram = main[start..end].to_owned();
|
||||
main.replace_range(start..end, "include \"z.arcadia\";\n");
|
||||
bundle.files.insert("z.arcadia".into(), diagram);
|
||||
let baseline = authored(&bundle).indexed.resolved.storage.live;
|
||||
let desired = renamed(Layer::Oa);
|
||||
// The unchanged-profile pipeline must clear every predecessor. The final
|
||||
// ordinary header is now in the canonical last include, at d[8..9).
|
||||
let profile = Limits {
|
||||
entries: baseline.entries + 37,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source = resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
let e = match source.plan_rename(&desired) {
|
||||
Ok(_) => panic!("cap accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics[0].path, "z.arcadia");
|
||||
assert_eq!(e.diagnostics[0].diagnostic.span, Span { start: 8, end: 9 });
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
}
|
||||
@@ -40,266 +40,282 @@ pub(super) fn binding(p: &mut Cursor<'_, '_>) -> Result<Binding, Error> {
|
||||
while let Some(task) = tasks.pop() {
|
||||
p.budget.active()?;
|
||||
match task {
|
||||
Task::Expression(minimum) => {
|
||||
let start = p.span().start;
|
||||
if p.text() != "(" {
|
||||
p.budget.entry(p.span())?;
|
||||
}
|
||||
tasks.push(Task::Continue(minimum));
|
||||
match p.text() {
|
||||
"true" | "false" => {
|
||||
let value = p.text() == "true";
|
||||
let span = p.span();
|
||||
p.cursor += 1;
|
||||
values.push(p.node(ExpressionKind::Boolean(value), span)?);
|
||||
}
|
||||
"-" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Unary(start, UnaryOp::Negate));
|
||||
tasks.push(Task::Expression(7));
|
||||
}
|
||||
"(" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Group(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"[" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(SequenceKind::List, start)));
|
||||
}
|
||||
"@" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
if p.text() == "(" {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(
|
||||
SequenceKind::Call(reference),
|
||||
start,
|
||||
)));
|
||||
} else {
|
||||
let end = reference.span.end;
|
||||
values.push(
|
||||
p.node(ExpressionKind::Property(reference), Span { start, end })?,
|
||||
);
|
||||
}
|
||||
}
|
||||
"$" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
let end = reference.span.end;
|
||||
values.push(
|
||||
p.node(ExpressionKind::Parameter(reference), Span { start, end })?,
|
||||
);
|
||||
}
|
||||
"ratio" => {
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
"abs" | "not" | "if" | "sum" => {
|
||||
let kind = match p.text() {
|
||||
"abs" => SequenceKind::Unary(UnaryOp::Absolute),
|
||||
"not" => SequenceKind::Unary(UnaryOp::Not),
|
||||
"if" => SequenceKind::If,
|
||||
_ => SequenceKind::Sum,
|
||||
};
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Next(Sequence::new(kind, start)));
|
||||
}
|
||||
"power" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Power(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"as_kind" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
p.take("@")?;
|
||||
let kind = p.reference()?;
|
||||
p.take(",")?;
|
||||
tasks.push(Task::AsKind(start, kind));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
_ if p
|
||||
.tokens
|
||||
.get(p.cursor)
|
||||
.is_some_and(|token| token.class == Class::Number) =>
|
||||
{
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
_ => return Err(error(p.span(), "Expected an analysis expression.")),
|
||||
}
|
||||
}
|
||||
Task::Continue(minimum) => {
|
||||
if let Some((op, precedence)) =
|
||||
operator(p.text()).filter(|(_, prec)| *prec >= minimum)
|
||||
{
|
||||
let left = values.pop().expect("completed Pratt operand");
|
||||
if comparison(op) && ungrouped_comparison(&left) {
|
||||
return Err(error(p.span(), "Comparison children require parentheses."));
|
||||
}
|
||||
p.budget.entry(p.span())?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Continue(minimum));
|
||||
tasks.push(Task::Binary(left, op));
|
||||
tasks.push(Task::Expression(precedence + 1));
|
||||
}
|
||||
}
|
||||
Task::Binary(left, op) => {
|
||||
let right = values.pop().expect("completed right operand");
|
||||
if comparison(op) && ungrouped_comparison(&right) {
|
||||
return Err(error(
|
||||
right.span,
|
||||
"Comparison children require parentheses.",
|
||||
));
|
||||
}
|
||||
let span = Span {
|
||||
start: left.span.start,
|
||||
end: right.span.end,
|
||||
};
|
||||
values.push(p.node(
|
||||
ExpressionKind::Binary {
|
||||
op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
},
|
||||
span,
|
||||
)?);
|
||||
}
|
||||
Task::Unary(start, op) => {
|
||||
let operand = values.pop().expect("completed unary operand");
|
||||
let end = operand.span.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Unary {
|
||||
op,
|
||||
operand: Box::new(operand),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::Group(start) => {
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
let value = values.last_mut().expect("completed grouped expression");
|
||||
value.grouped = true;
|
||||
value.span = Span { start, end };
|
||||
}
|
||||
Task::Next(mut sequence) => {
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() == closing {
|
||||
let end = p.take(closing)?.end;
|
||||
p.close();
|
||||
values.push(p.sequence(sequence, end)?);
|
||||
} else {
|
||||
let count = sequence.values.len();
|
||||
let maximum = match sequence.kind {
|
||||
SequenceKind::List => crate::analysis::MAX_LIST_LENGTH,
|
||||
SequenceKind::Unary(_) => 1,
|
||||
SequenceKind::If => 3,
|
||||
SequenceKind::Sum => 2,
|
||||
SequenceKind::Call(_) => usize::MAX,
|
||||
};
|
||||
if count >= maximum {
|
||||
return Err(error(
|
||||
p.span(),
|
||||
"Too many list members or constructor arguments.",
|
||||
));
|
||||
}
|
||||
let key = if matches!(sequence.kind, SequenceKind::Call(_)) {
|
||||
p.budget.entry(p.span())?;
|
||||
let key = p.reference()?;
|
||||
if !sequence.keys.insert(key.name.clone()) {
|
||||
return Err(error(key.span, "Duplicate call argument name."));
|
||||
}
|
||||
p.take(":")?;
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tasks.push(Task::Collected(sequence, key));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
}
|
||||
Task::Collected(mut sequence, key) => {
|
||||
sequence
|
||||
.values
|
||||
.push((key, values.pop().expect("completed sequence value")));
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() != closing {
|
||||
p.take(",")?;
|
||||
if p.text() == closing {
|
||||
return Err(error(p.span(), "Trailing comma is not an expression."));
|
||||
}
|
||||
}
|
||||
tasks.push(Task::Next(sequence));
|
||||
}
|
||||
Task::Power(start) => {
|
||||
let base = values.pop().expect("completed power base");
|
||||
p.take(",")?;
|
||||
let negative = if p.text() == "-" {
|
||||
p.cursor += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let span = p.integer()?;
|
||||
let magnitude = p.source[span.start..span.end]
|
||||
.parse::<u8>()
|
||||
.map_err(|_| error(span, "Power exponent is outside -16..16."))?;
|
||||
if magnitude > 16 {
|
||||
return Err(error(span, "Power exponent is outside -16..16."));
|
||||
}
|
||||
let exponent = if negative {
|
||||
-(magnitude as i8)
|
||||
} else {
|
||||
magnitude as i8
|
||||
};
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Power {
|
||||
base: Box::new(base),
|
||||
exponent,
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::AsKind(start, kind) => {
|
||||
let value = values.pop().expect("completed kind assertion value");
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::AsKind {
|
||||
kind,
|
||||
value: Box::new(value),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::Expression(minimum) => prefix(p, minimum, &mut tasks, &mut values)?,
|
||||
task => continue_task(p, task, &mut tasks, &mut values)?,
|
||||
}
|
||||
}
|
||||
Ok(Binding::Expression(
|
||||
values.pop().expect("one complete expression"),
|
||||
))
|
||||
}
|
||||
// Keep mutually exclusive construction temporaries out of the numeric prefix
|
||||
// call chain. Both steps borrow the driver's original iterative stacks.
|
||||
fn prefix(
|
||||
p: &mut Cursor<'_, '_>,
|
||||
minimum: u8,
|
||||
tasks: &mut Vec<Task>,
|
||||
values: &mut Vec<Expression>,
|
||||
) -> Result<(), Error> {
|
||||
let start = p.span().start;
|
||||
if p.text() != "(" {
|
||||
p.budget.entry(p.span())?;
|
||||
}
|
||||
tasks.push(Task::Continue(minimum));
|
||||
match p.text() {
|
||||
"true" | "false" => {
|
||||
let value = p.text() == "true";
|
||||
let span = p.span();
|
||||
p.cursor += 1;
|
||||
values.push(p.node(ExpressionKind::Boolean(value), span)?);
|
||||
}
|
||||
"-" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Unary(start, UnaryOp::Negate));
|
||||
tasks.push(Task::Expression(7));
|
||||
}
|
||||
"(" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Group(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"[" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(SequenceKind::List, start)));
|
||||
}
|
||||
"@" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
if p.text() == "(" {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(
|
||||
SequenceKind::Call(reference),
|
||||
start,
|
||||
)));
|
||||
} else {
|
||||
let end = reference.span.end;
|
||||
values.push(p.node(ExpressionKind::Property(reference), Span { start, end })?);
|
||||
}
|
||||
}
|
||||
"$" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
let end = reference.span.end;
|
||||
values.push(p.node(ExpressionKind::Parameter(reference), Span { start, end })?);
|
||||
}
|
||||
"ratio" => {
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
"abs" | "not" | "if" | "sum" => {
|
||||
let kind = match p.text() {
|
||||
"abs" => SequenceKind::Unary(UnaryOp::Absolute),
|
||||
"not" => SequenceKind::Unary(UnaryOp::Not),
|
||||
"if" => SequenceKind::If,
|
||||
_ => SequenceKind::Sum,
|
||||
};
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Next(Sequence::new(kind, start)));
|
||||
}
|
||||
"power" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Power(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"as_kind" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
p.take("@")?;
|
||||
let kind = p.reference()?;
|
||||
p.take(",")?;
|
||||
tasks.push(Task::AsKind(start, kind));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
_ if p
|
||||
.tokens
|
||||
.get(p.cursor)
|
||||
.is_some_and(|token| token.class == Class::Number) =>
|
||||
{
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
_ => return Err(error(p.span(), "Expected an analysis expression.")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn continue_task(
|
||||
p: &mut Cursor<'_, '_>,
|
||||
task: Task,
|
||||
tasks: &mut Vec<Task>,
|
||||
values: &mut Vec<Expression>,
|
||||
) -> Result<(), Error> {
|
||||
match task {
|
||||
Task::Expression(_) => unreachable!("prefix tasks are dispatched by the driver"),
|
||||
Task::Continue(minimum) => {
|
||||
if let Some((op, precedence)) = operator(p.text()).filter(|(_, prec)| *prec >= minimum)
|
||||
{
|
||||
let left = values.pop().expect("completed Pratt operand");
|
||||
if comparison(op) && ungrouped_comparison(&left) {
|
||||
return Err(error(p.span(), "Comparison children require parentheses."));
|
||||
}
|
||||
p.budget.entry(p.span())?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Continue(minimum));
|
||||
tasks.push(Task::Binary(left, op));
|
||||
tasks.push(Task::Expression(precedence + 1));
|
||||
}
|
||||
}
|
||||
Task::Binary(left, op) => {
|
||||
let right = values.pop().expect("completed right operand");
|
||||
if comparison(op) && ungrouped_comparison(&right) {
|
||||
return Err(error(
|
||||
right.span,
|
||||
"Comparison children require parentheses.",
|
||||
));
|
||||
}
|
||||
let span = Span {
|
||||
start: left.span.start,
|
||||
end: right.span.end,
|
||||
};
|
||||
values.push(p.node(
|
||||
ExpressionKind::Binary {
|
||||
op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
},
|
||||
span,
|
||||
)?);
|
||||
}
|
||||
Task::Unary(start, op) => {
|
||||
let operand = values.pop().expect("completed unary operand");
|
||||
let end = operand.span.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Unary {
|
||||
op,
|
||||
operand: Box::new(operand),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::Group(start) => {
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
let value = values.last_mut().expect("completed grouped expression");
|
||||
value.grouped = true;
|
||||
value.span = Span { start, end };
|
||||
}
|
||||
Task::Next(mut sequence) => {
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() == closing {
|
||||
let end = p.take(closing)?.end;
|
||||
p.close();
|
||||
values.push(p.sequence(sequence, end)?);
|
||||
} else {
|
||||
let count = sequence.values.len();
|
||||
let maximum = match sequence.kind {
|
||||
SequenceKind::List => crate::analysis::MAX_LIST_LENGTH,
|
||||
SequenceKind::Unary(_) => 1,
|
||||
SequenceKind::If => 3,
|
||||
SequenceKind::Sum => 2,
|
||||
SequenceKind::Call(_) => usize::MAX,
|
||||
};
|
||||
if count >= maximum {
|
||||
return Err(error(
|
||||
p.span(),
|
||||
"Too many list members or constructor arguments.",
|
||||
));
|
||||
}
|
||||
let key = if matches!(sequence.kind, SequenceKind::Call(_)) {
|
||||
p.budget.entry(p.span())?;
|
||||
let key = p.reference()?;
|
||||
if !sequence.keys.insert(key.name.clone()) {
|
||||
return Err(error(key.span, "Duplicate call argument name."));
|
||||
}
|
||||
p.take(":")?;
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tasks.push(Task::Collected(sequence, key));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
}
|
||||
Task::Collected(mut sequence, key) => {
|
||||
sequence
|
||||
.values
|
||||
.push((key, values.pop().expect("completed sequence value")));
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() != closing {
|
||||
p.take(",")?;
|
||||
if p.text() == closing {
|
||||
return Err(error(p.span(), "Trailing comma is not an expression."));
|
||||
}
|
||||
}
|
||||
tasks.push(Task::Next(sequence));
|
||||
}
|
||||
Task::Power(start) => {
|
||||
let base = values.pop().expect("completed power base");
|
||||
p.take(",")?;
|
||||
let negative = if p.text() == "-" {
|
||||
p.cursor += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let span = p.integer()?;
|
||||
let magnitude = p.source[span.start..span.end]
|
||||
.parse::<u8>()
|
||||
.map_err(|_| error(span, "Power exponent is outside -16..16."))?;
|
||||
if magnitude > 16 {
|
||||
return Err(error(span, "Power exponent is outside -16..16."));
|
||||
}
|
||||
let exponent = if negative {
|
||||
-(magnitude as i8)
|
||||
} else {
|
||||
magnitude as i8
|
||||
};
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Power {
|
||||
base: Box::new(base),
|
||||
exponent,
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::AsKind(start, kind) => {
|
||||
let value = values.pop().expect("completed kind assertion value");
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::AsKind {
|
||||
kind,
|
||||
value: Box::new(value),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
impl Sequence {
|
||||
fn new(kind: SequenceKind, start: usize) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -522,8 +522,18 @@ fn partial_wide_and_deep_input_cleanup_does_not_poison_a_fresh_parse() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_source_headers_and_existing_numeric_token_controls_are_unchanged() {
|
||||
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
|
||||
fn public_ordinary_source8_preserves_typed_refusal_and_numeric_controls() {
|
||||
let ordinary = crate::language::parse_with_report("arcadia 8; project p \"P\" {}").unwrap();
|
||||
assert!(ordinary.migration.is_none());
|
||||
assert_eq!(ordinary.project.schema_version, 8);
|
||||
assert_eq!(ordinary.project.alias, "p");
|
||||
assert_eq!(ordinary.project.name, "P");
|
||||
assert!(
|
||||
crate::language::parse(
|
||||
"arcadia 8; project p \"P\" { calculation c \"C\" { result Number; body 1; } }"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
for accepted in ["-1.5", "1e-2", "0", "-0"] {
|
||||
let tokens = crate::language::tokenize(accepted);
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
@@ -133,6 +133,16 @@ fn typed_oa_sa_canonical_roundtrips_match_independent_projects() {
|
||||
);
|
||||
}
|
||||
let text = canonical(&expected);
|
||||
let ordinary8 = ordinary_source.replacen("arcadia 7", "arcadia 8", 1);
|
||||
let ordinary_decoded = crate::language::parse_with_report(&ordinary8).unwrap();
|
||||
assert!(ordinary_decoded.migration.is_none());
|
||||
assert_eq!(
|
||||
ordinary_decoded.project,
|
||||
crate::language::parse(ordinary_source).unwrap()
|
||||
);
|
||||
assert!(
|
||||
crate::language::parse(&format!("arcadia 8; project p \"P\" {{ {text} }}")).is_err()
|
||||
);
|
||||
let actual = parse_typed(crate::language::parse(ordinary_source).unwrap(), &text);
|
||||
assert_eq!(actual, expected);
|
||||
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
|
||||
@@ -147,7 +157,6 @@ fn typed_oa_sa_canonical_roundtrips_match_independent_projects() {
|
||||
assert!(text.contains(&format!("{prefix}_{word} ")));
|
||||
}
|
||||
}
|
||||
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -32,14 +32,31 @@ impl<'a> Scanner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum StringEvent {
|
||||
Reserve { span: Span, payload: usize },
|
||||
Released { payload: usize },
|
||||
}
|
||||
impl Iterator for Scanner<'_> {
|
||||
type Item = Token;
|
||||
|
||||
fn next(&mut self) -> Option<Token> {
|
||||
match self.next_with_strings(|_| Ok::<(), std::convert::Infallible>(())) {
|
||||
Ok(token) => token,
|
||||
Err(never) => match never {},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Scanner<'_> {
|
||||
pub(super) fn next_with_strings<E>(
|
||||
&mut self,
|
||||
mut hook: impl FnMut(StringEvent) -> Result<(), E>,
|
||||
) -> Result<Option<Token>, E> {
|
||||
let source = self.source;
|
||||
let bytes = source.as_bytes();
|
||||
let start = self.cursor;
|
||||
let first = *bytes.get(start)?;
|
||||
let Some(&first) = bytes.get(start) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut legacy_number_part = false;
|
||||
let class = if matches!(first, b' ' | b'\t' | b'\r' | b'\n') {
|
||||
self.cursor += 1;
|
||||
@@ -123,11 +140,24 @@ impl Iterator for Scanner<'_> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if closed && serde_json::from_str::<String>(&source[start..self.cursor]).is_ok() {
|
||||
Class::String
|
||||
let valid = if closed {
|
||||
let payload = self.cursor - start - 2;
|
||||
hook(StringEvent::Reserve {
|
||||
span: Span {
|
||||
start,
|
||||
end: self.cursor,
|
||||
},
|
||||
payload,
|
||||
})?;
|
||||
// The temporary Result/String drops at this statement's end,
|
||||
// before the refund hook runs.
|
||||
let valid = serde_json::from_str::<String>(&source[start..self.cursor]).is_ok();
|
||||
hook(StringEvent::Released { payload })?;
|
||||
valid
|
||||
} else {
|
||||
Class::Error
|
||||
}
|
||||
false
|
||||
};
|
||||
if valid { Class::String } else { Class::Error }
|
||||
} else if b"{}();,[]:=@$+-*/<>!".contains(&first) {
|
||||
self.cursor += 1;
|
||||
if matches!(first, b'<' | b'>' | b'=' | b'!') && bytes.get(self.cursor) == Some(&b'=') {
|
||||
@@ -144,14 +174,14 @@ impl Iterator for Scanner<'_> {
|
||||
legacy_number_part = first == b'.';
|
||||
Class::Error
|
||||
};
|
||||
Some(Token {
|
||||
Ok(Some(Token {
|
||||
class,
|
||||
span: Span {
|
||||
start,
|
||||
end: self.cursor,
|
||||
},
|
||||
legacy_number_part,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,6 @@ pub(super) fn row_key(source: &str, origins: &BTreeMap<Id, Id>) -> Option<String
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits: ParseLimits::UNLIMITED,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
};
|
||||
let ast = parser.value().ok()?;
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
//! Ordinary current-schema compatibility; typed analysis authoring remains private.
|
||||
use super::*;
|
||||
use crate::source_project::{self, ProjectLimits, SourceBundle, compile_bundle};
|
||||
|
||||
fn example_bundle() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: [
|
||||
(
|
||||
"project.arcadia",
|
||||
include_str!("../../../examples/source-project/project.arcadia"),
|
||||
),
|
||||
(
|
||||
"oa.arcadia",
|
||||
include_str!("../../../examples/source-project/oa.arcadia"),
|
||||
),
|
||||
(
|
||||
"sa/participants.arcadia",
|
||||
include_str!("../../../examples/source-project/sa/participants.arcadia"),
|
||||
),
|
||||
(
|
||||
"sa/reporting.arcadia",
|
||||
include_str!("../../../examples/source-project/sa/reporting.arcadia"),
|
||||
),
|
||||
(
|
||||
"sa/assurance.arcadia",
|
||||
include_str!("../../../examples/source-project/sa/assurance.arcadia"),
|
||||
),
|
||||
(
|
||||
"views.arcadia",
|
||||
include_str!("../../../examples/source-project/views.arcadia"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(path, text)| (path.into(), text.into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
fn document(version: &str, body: &str) -> String {
|
||||
format!(
|
||||
"arcadia {version};\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
)
|
||||
}
|
||||
fn identity(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
#[test]
|
||||
fn current_formatter_roundtrips_complete_ordinary_examples_and_history() {
|
||||
let input = example_bundle();
|
||||
let before = input.clone();
|
||||
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
|
||||
let mut example = compiled.decoded.project;
|
||||
assert!(example.elements.values().any(|e| e.layer() == Layer::Oa));
|
||||
assert!(example.elements.values().any(|e| e.layer() == Layer::Sa));
|
||||
assert!(!example.diagrams.is_empty());
|
||||
// The published example has no traces: author an explicit participant link
|
||||
// rather than assuming that the example already exercises this record class.
|
||||
let trace_id = Id::from_u128(0x188b_0000_0000_0000_0000_0000_0000_0001);
|
||||
let trace = Trace {
|
||||
id: trace_id,
|
||||
alias: "rescue_participant_trace".into(),
|
||||
name: "Coordination participant realization".into(),
|
||||
description: "Relates the operational coordination team to its selected system.".into(),
|
||||
oa: Id::parse_str("00000000-0000-4000-8000-000000000102").unwrap(),
|
||||
sa: Id::parse_str("00000000-0000-4000-8000-000000000301").unwrap(),
|
||||
kind: TraceKind::Participant,
|
||||
baseline: None,
|
||||
};
|
||||
assert_eq!(example.elements[&trace.oa].kind(), Kind::Entity);
|
||||
assert_eq!(example.elements[&trace.sa].kind(), Kind::System);
|
||||
assert!(example.traces.insert(trace_id, trace.clone()).is_none());
|
||||
assert_eq!(example.traces[&trace_id], trace);
|
||||
let historical =
|
||||
crate::persistence::decode(include_bytes!("../tests/fixtures/v4-reviewed-prose.json"))
|
||||
.unwrap();
|
||||
assert!(historical.traces.values().any(|t| t.baseline.is_some()));
|
||||
for project in [example, historical] {
|
||||
assert!(project.definitions.is_empty());
|
||||
let source = format(&project);
|
||||
assert!(source.starts_with("arcadia 8;"));
|
||||
let decoded = parse_with_report(&source).unwrap();
|
||||
assert_eq!(decoded.project, project);
|
||||
assert!(decoded.migration.is_none());
|
||||
let bundle = source_project::format_bundle(&project);
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle, bundle);
|
||||
assert_eq!(compiled.decoded.project, project);
|
||||
assert!(compiled.decoded.migration.is_none());
|
||||
let again = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(again.bundle, compiled.bundle);
|
||||
assert_eq!(again.index, compiled.index);
|
||||
assert_eq!(again.decoded.project, project);
|
||||
assert_eq!(again.diagnostics, compiled.diagnostics);
|
||||
assert!(again.decoded.migration.is_none());
|
||||
}
|
||||
assert_eq!(input, before);
|
||||
}
|
||||
#[test]
|
||||
fn versions_three_through_seven_migrate_to_idempotent_ordinary_eight() {
|
||||
for version in ["3", "4", "5", "6", "7"] {
|
||||
let source = document(version, "");
|
||||
let decoded = parse_with_report(&source).unwrap();
|
||||
assert_eq!(decoded.project.schema_version, 8);
|
||||
assert!(decoded.migration.is_some());
|
||||
let input = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source.clone())]),
|
||||
};
|
||||
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, decoded.project);
|
||||
assert!(compiled.decoded.migration.is_some());
|
||||
assert!(compiled.bundle.files["project.arcadia"].starts_with("arcadia 8;"));
|
||||
let again = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert!(again.decoded.migration.is_none());
|
||||
assert_eq!(again.decoded.project, decoded.project);
|
||||
assert_eq!(again.bundle, compiled.bundle);
|
||||
assert_eq!(again.index, compiled.index);
|
||||
assert_eq!(input.files["project.arcadia"], source);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn included_nested_omitted_ids_keep_actual_index_and_unicode_crlf() {
|
||||
let entry =
|
||||
"// 海岸\r\narcadia 8;\r\nproject coast \"Coast\" { include \"team.arcadia\"; }\r\n";
|
||||
let fragment = "// Keep nested ownership é\r\noa_entity team \"Team\" {\r\n oa_actor sailor \"Sailor\" { description \"海\"; }\r\n}\r\n";
|
||||
let input = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), entry.into()),
|
||||
("team.arcadia".into(), fragment.into()),
|
||||
]),
|
||||
};
|
||||
let before = input.clone();
|
||||
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
|
||||
assert!(compiled.decoded.migration.is_none());
|
||||
let p = &compiled.decoded.project;
|
||||
let team = identity(p, "team");
|
||||
let sailor = identity(p, "sailor");
|
||||
assert_eq!(p.elements[&sailor].parent, Some(team));
|
||||
assert_eq!(p.elements[&sailor].description, "海");
|
||||
assert_eq!(compiled.index.lexical_owners[&sailor], team);
|
||||
for (id, prefix) in [
|
||||
(team, "oa_entity team \"Team\""),
|
||||
(sailor, "oa_actor sailor \"Sailor\""),
|
||||
] {
|
||||
let location = &compiled.index.declarations[&id];
|
||||
assert_eq!(location.path, "team.arcadia");
|
||||
let actual = &compiled.bundle.files[&location.path][location.span.start..location.span.end];
|
||||
assert!(actual.starts_with(prefix));
|
||||
assert!(actual.contains(&format!("id \"{id}\"")));
|
||||
assert!(actual.ends_with('}'));
|
||||
}
|
||||
assert!(compiled.bundle.files["project.arcadia"].starts_with("// 海岸\r\narcadia 8;\r\n"));
|
||||
assert!(compiled.bundle.files["team.arcadia"].starts_with("// Keep nested ownership é\r\n"));
|
||||
for text in compiled.bundle.files.values() {
|
||||
assert_eq!(text.matches('\n').count(), text.matches("\r\n").count());
|
||||
}
|
||||
assert!(compiled.diagnostics.is_empty());
|
||||
let again = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(again.bundle, compiled.bundle);
|
||||
assert_eq!(again.index, compiled.index);
|
||||
assert_eq!(again.decoded.project, *p);
|
||||
let mut stale = again.clone();
|
||||
stale.decoded.project.name = "stale".into();
|
||||
let error = source_project::update_bundle(&stale, p, ProjectLimits::default()).unwrap_err();
|
||||
assert_eq!(error[0].diagnostic.code, "stale_source_index");
|
||||
assert_eq!(input, before);
|
||||
}
|
||||
#[test]
|
||||
fn typed_analysis_future_headers_and_limits_still_refuse() {
|
||||
for word in [
|
||||
"quantity_kind",
|
||||
"unit",
|
||||
"calculation",
|
||||
"oa_property",
|
||||
"sa_property",
|
||||
"oa_check",
|
||||
"sa_check",
|
||||
"oa_analysis",
|
||||
"oa_case",
|
||||
"sa_analysis",
|
||||
"sa_case",
|
||||
"oa_engineering_property",
|
||||
"sa_engineering_property",
|
||||
"oa_analysis_check",
|
||||
"sa_analysis_check",
|
||||
"oa_analysis_case",
|
||||
"sa_analysis_case",
|
||||
] {
|
||||
let fragment = format!("{word} x \"X\" id \"00000000-0000-0000-0000-000000000002\" {{}}\n");
|
||||
let source = document("8", &fragment);
|
||||
assert!(parse(&source).is_err(), "{word}");
|
||||
let input = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
document("8", "include \"part.arcadia\";"),
|
||||
),
|
||||
("part.arcadia".into(), fragment.clone()),
|
||||
]),
|
||||
};
|
||||
let error = compile_bundle(&input, ProjectLimits::default()).unwrap_err();
|
||||
assert_eq!(error[0].path, "part.arcadia", "{word}");
|
||||
assert_eq!(error[0].diagnostic.code, "invalid_source");
|
||||
assert_eq!(error[0].diagnostic.span.start, 0);
|
||||
if word.contains("engineering_property")
|
||||
|| word.contains("analysis_check")
|
||||
|| word.contains("analysis_case")
|
||||
{
|
||||
assert_eq!(
|
||||
error[0].diagnostic.message,
|
||||
"Analysis records require the source8 authoring implementation."
|
||||
);
|
||||
}
|
||||
assert_eq!(input.files["part.arcadia"], fragment);
|
||||
}
|
||||
for version in ["0", "9", "99", "8.0"] {
|
||||
let error = parse(&document(version, "")).unwrap_err();
|
||||
assert_eq!(
|
||||
error[0].message,
|
||||
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
|
||||
);
|
||||
assert_eq!(error[0].span.start, 8);
|
||||
}
|
||||
let source = document("8", "");
|
||||
let error = parse_limited(
|
||||
&source,
|
||||
ParseLimits {
|
||||
max_source_bytes: source.len() - 1,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error[0].code, "source_limit");
|
||||
let error = parse_limited(
|
||||
&source,
|
||||
ParseLimits {
|
||||
max_records: 0,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error[0].code, "model_limit");
|
||||
assert!(parse("arcadia 8; project").is_err());
|
||||
}
|
||||
#[test]
|
||||
fn public_checked_save_reopens_ordinary_source_and_json_but_refuses_lost_definition() {
|
||||
let project = parse_with_report(include_str!("../../../examples/text-first/project.arcadia"))
|
||||
.unwrap()
|
||||
.project;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for name in ["ordinary.arcadia", "ordinary.json"] {
|
||||
let path = dir.path().join(name);
|
||||
let bytes = crate::persistence::save_checked(&path, &project, None).unwrap();
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
let decoded = crate::persistence::decode_with_report(&bytes).unwrap();
|
||||
assert_eq!(decoded.project, project);
|
||||
assert!(decoded.migration.is_none());
|
||||
assert_eq!(
|
||||
crate::persistence::save_checked(&path, &project, Some(&bytes)).unwrap(),
|
||||
bytes
|
||||
);
|
||||
}
|
||||
let mut with_definition = Project::empty();
|
||||
let id = Id::parse_str("188b911c-5f8b-431c-9eea-2d41f3953767").unwrap();
|
||||
with_definition.definitions.insert(
|
||||
id,
|
||||
crate::analysis::Definition {
|
||||
id,
|
||||
alias: "duration".into(),
|
||||
name: "Duration".into(),
|
||||
description: String::new(),
|
||||
body: crate::analysis::DefinitionBody::QuantityKind(
|
||||
crate::analysis::QuantityKindDefinition {
|
||||
dimension: crate::analysis_numeric::Dimension::new([0, 0, 1, 0, 0, 0, 0])
|
||||
.unwrap(),
|
||||
algebra: crate::analysis::QuantityAlgebra::Linear,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
let path = dir.path().join("protected.arcadia");
|
||||
let sentinel = b"external unfinished draft\r\n";
|
||||
std::fs::write(&path, sentinel).unwrap();
|
||||
let count = std::fs::read_dir(dir.path()).unwrap().count();
|
||||
let error =
|
||||
crate::persistence::save_checked(&path, &with_definition, Some(sentinel)).unwrap_err();
|
||||
assert!(
|
||||
matches!(error,crate::persistence::FileError::Invalid(message) if message=="Generated source does not reproduce the complete requested project.")
|
||||
);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), sentinel);
|
||||
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_preview_literals_compile_to_independent_complete_projects() {
|
||||
let entry = "// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"participants.arcadia\"; include \"children.arcadia\";\r\n}\r\n";
|
||||
let before_team = "// @team remains documentation\r\noa_entity team \"Team @team\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n";
|
||||
let after_team = "// @team remains documentation\r\noa_entity rescue_team \"Team @team\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n";
|
||||
let before_child = "oa_entity child \"Child\" id \"00000000-0000-0000-0000-000000000003\" { parent @team; }\r\n";
|
||||
let after_child = "oa_entity child \"Child\" id \"00000000-0000-0000-0000-000000000003\" { parent @rescue_team; }\r\n";
|
||||
let project_id = Id::from_u128(1);
|
||||
let team_id = Id::from_u128(2);
|
||||
let child_id = Id::from_u128(3);
|
||||
let mut expected = Project::empty();
|
||||
expected.id = project_id;
|
||||
expected.alias = "p".into();
|
||||
expected.name = "Project".into();
|
||||
for (id, alias, name, parent) in [
|
||||
(team_id, "team", "Team @team", None),
|
||||
(child_id, "child", "Child", Some(team_id)),
|
||||
] {
|
||||
expected.elements.insert(
|
||||
id,
|
||||
Element {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
parent,
|
||||
body: ElementBody::new(Kind::Entity, Layer::Oa).unwrap(),
|
||||
},
|
||||
);
|
||||
}
|
||||
for (team, child, alias) in [
|
||||
(before_team, before_child, "team"),
|
||||
(after_team, after_child, "rescue_team"),
|
||||
] {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: [
|
||||
("project.arcadia", entry),
|
||||
("participants.arcadia", team),
|
||||
("children.arcadia", child),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(path, source)| (path.into(), source.into()))
|
||||
.collect(),
|
||||
};
|
||||
expected.elements.get_mut(&team_id).unwrap().alias = alias.into();
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, expected);
|
||||
assert!(compiled.decoded.migration.is_none());
|
||||
assert!(compiled.diagnostics.is_empty());
|
||||
assert_eq!(compiled.bundle, bundle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
//! External consumer: the linked oa-core library is not built with cfg(test).
|
||||
//! Expected models use authored public values, never a parser or formatter oracle.
|
||||
use oa_core::analysis as a;
|
||||
use oa_core::analysis_numeric::Exact;
|
||||
use oa_core::language::{ParseLimits, Span};
|
||||
use oa_core::source_project::{ProjectLimits, SourceBundle, SourceDiagnostic};
|
||||
pub use oa_core::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// This existing helper constructs independent public model values only. It does
|
||||
// not import private parser/index/writer code or enable cfg(test) in oa-core.
|
||||
#[path = "../src/language/analysis_source/model_oracle.rs"]
|
||||
mod independent_model;
|
||||
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(n)
|
||||
}
|
||||
|
||||
fn empty_project() -> Project {
|
||||
let mut p = Project::empty();
|
||||
p.id = id(1);
|
||||
p.alias = "p".into();
|
||||
p.name = "Project".into();
|
||||
p
|
||||
}
|
||||
|
||||
fn document(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
|
||||
id(1)
|
||||
)
|
||||
}
|
||||
|
||||
fn single(body: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), document(body))]),
|
||||
}
|
||||
}
|
||||
|
||||
fn succeeds(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
expected: &Project,
|
||||
warnings: &[SourceDiagnostic],
|
||||
) {
|
||||
let original = bundle.clone();
|
||||
let checked = language::check_mixed_bundle(bundle, limits)
|
||||
.unwrap_or_else(|errors| panic!("expected complete read: {errors:#?}"));
|
||||
assert_eq!(checked.project(), expected);
|
||||
assert_eq!(checked.warnings(), warnings);
|
||||
assert_eq!(bundle, &original, "read success preserves every input byte");
|
||||
drop(checked);
|
||||
assert_eq!(bundle, &original);
|
||||
}
|
||||
|
||||
fn fails(bundle: &SourceBundle, limits: ProjectLimits) -> Vec<SourceDiagnostic> {
|
||||
let original = bundle.clone();
|
||||
let errors = match language::check_mixed_bundle(bundle, limits) {
|
||||
Ok(_) => panic!("invalid input returned a successful read witness"),
|
||||
Err(errors) => errors,
|
||||
};
|
||||
assert!(!errors.is_empty());
|
||||
assert_eq!(bundle, &original, "read refusal preserves every input byte");
|
||||
errors
|
||||
}
|
||||
|
||||
fn authored_span(source: &str, literal: &str) -> Span {
|
||||
assert_eq!(
|
||||
source.matches(literal).count(),
|
||||
1,
|
||||
"ambiguous authored span"
|
||||
);
|
||||
let start = source.find(literal).unwrap();
|
||||
Span {
|
||||
start,
|
||||
end: start + literal.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_at(
|
||||
bundle: &SourceBundle,
|
||||
errors: &[SourceDiagnostic],
|
||||
path: &str,
|
||||
span: Span,
|
||||
code: &str,
|
||||
message: &str,
|
||||
) {
|
||||
assert_eq!(errors.len(), 1, "{errors:#?}");
|
||||
let error = &errors[0];
|
||||
assert_eq!(error.path, path);
|
||||
assert_eq!(error.diagnostic.severity, validation::Severity::Error);
|
||||
assert_eq!(error.diagnostic.code, code);
|
||||
assert_eq!(error.diagnostic.message, message);
|
||||
assert_eq!(error.diagnostic.span, span);
|
||||
let source = bundle.files.get(path).map_or("", String::as_str);
|
||||
let prefix = &source[..span.start];
|
||||
assert_eq!(
|
||||
error.diagnostic.line,
|
||||
prefix.bytes().filter(|b| *b == b'\n').count() + 1
|
||||
);
|
||||
assert_eq!(
|
||||
error.diagnostic.column,
|
||||
prefix.rsplit('\n').next().unwrap().chars().count() + 1
|
||||
);
|
||||
}
|
||||
|
||||
fn rich(layer: Layer, crlf: bool) -> (SourceBundle, Project, Vec<SourceDiagnostic>) {
|
||||
let (ordinary, typed, work) = match layer {
|
||||
Layer::Oa => (
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/ordinary-oa.arcadia"),
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/typed-oa.arcadia"),
|
||||
"oa_activity",
|
||||
),
|
||||
Layer::Sa => (
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/ordinary-sa.arcadia"),
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/typed-sa.arcadia"),
|
||||
"sa_function",
|
||||
),
|
||||
};
|
||||
let mut expected = independent_model::project(layer);
|
||||
let mut main = ordinary
|
||||
.replace("\r\n", "\n")
|
||||
.replacen("arcadia 7;", "arcadia 8;", 1);
|
||||
assert!(main.starts_with("arcadia 8;"));
|
||||
// Forward typed uses refer back into nested ordinary Work in this file.
|
||||
main = main.replacen("{\n", "{\n include \"typed.arcadia\";\n", 1);
|
||||
if layer == Layer::Sa {
|
||||
let system = Element {
|
||||
id: id(999),
|
||||
alias: "system".into(),
|
||||
name: "System".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::Sa(SaBody::System),
|
||||
};
|
||||
let at = main.rfind('}').unwrap();
|
||||
main.insert_str(
|
||||
at,
|
||||
&format!(
|
||||
" sa_system system \"System\" id \"{}\" {{}}\n",
|
||||
system.id
|
||||
),
|
||||
);
|
||||
assert!(expected.elements.insert(system.id, system).is_none());
|
||||
}
|
||||
let mut typed = typed.replace("\r\n", "\n");
|
||||
// Textual argument order is independent of the model's parameter-ID map.
|
||||
assert!(typed.contains("@combine(a: @receive_time, b: @dispatch_time)"));
|
||||
typed = typed.replace(
|
||||
"@combine(a: @receive_time, b: @dispatch_time)",
|
||||
"@combine(b: @dispatch_time, a: @receive_time)",
|
||||
);
|
||||
if crlf {
|
||||
main = format!("// 港🌊\r\n{}", main.replace('\n', "\r\n"));
|
||||
typed = typed.replace('\n', "\r\n");
|
||||
}
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), main),
|
||||
("typed.arcadia".into(), typed),
|
||||
]),
|
||||
};
|
||||
// Two nested leaves, so four warnings, unlike the earlier one-Work fixture.
|
||||
// Existing validation orders both allocation warnings before coverage warnings.
|
||||
let mut warnings = Vec::new();
|
||||
for allocation in [true, false] {
|
||||
for (alias, display) in [("receive", "Receive"), ("dispatch", "Dispatch")] {
|
||||
let element = expected
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.alias == alias)
|
||||
.unwrap();
|
||||
let literal = format!("{work} {alias} \"{display}\" id \"{}\" {{}}", element.id);
|
||||
let source = &bundle.files["main.arcadia"];
|
||||
let span = authored_span(source, &literal);
|
||||
let prefix = &source[..span.start];
|
||||
warnings.push(SourceDiagnostic {
|
||||
path: "main.arcadia".into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning,
|
||||
code: if allocation { "unallocated_leaf" } else { "work_without_behavior_context" }.into(),
|
||||
object_id: Some(element.id),
|
||||
field: allocation.then(|| "allocation".into()),
|
||||
message: if allocation { format!("Allocate leaf '{display}' to a performer.") } else {
|
||||
"Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.".into()
|
||||
},
|
||||
span, line: prefix.bytes().filter(|b| *b == b'\n').count() + 1,
|
||||
column: prefix.rsplit('\n').next().unwrap().chars().count() + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
(bundle, expected, warnings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_complete_oa_sa_models_and_all_located_warnings() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for crlf in [false, true] {
|
||||
let (bundle, expected, warnings) = rich(layer, crlf);
|
||||
assert_eq!(expected.definitions.len(), 4);
|
||||
assert_eq!(warnings.len(), 4);
|
||||
succeeds(&bundle, ProjectLimits::default(), &expected, &warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calc(alias: &str, number: u128, body: &str) -> String {
|
||||
format!(
|
||||
"calculation {alias} \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body {body}; }}",
|
||||
id(number),
|
||||
id(1)
|
||||
)
|
||||
}
|
||||
|
||||
fn calculation_model(alias: &str, number: u128, body: a::Expr) -> a::Definition {
|
||||
a::Definition {
|
||||
id: id(number),
|
||||
alias: alias.into(),
|
||||
name: "Calculation".into(),
|
||||
description: String::new(),
|
||||
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
|
||||
parameters: vec![a::Parameter {
|
||||
id: a::ParameterId(id(1)),
|
||||
name: "x".into(),
|
||||
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
}],
|
||||
result: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
body: a::Binding::Expression(body),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_scoped_uuid_reuse_and_cross_file_call_are_not_global_collisions() {
|
||||
let mut bundle = single("include \"f.arcadia\"; include \"g.arcadia\";");
|
||||
bundle
|
||||
.files
|
||||
.insert("f.arcadia".into(), calc("f", 2, "@g(x: $x)"));
|
||||
bundle.files.insert("g.arcadia".into(), calc("g", 3, "$x"));
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model(
|
||||
"f",
|
||||
2,
|
||||
a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(id(3)),
|
||||
arguments: BTreeMap::from([(
|
||||
a::ParameterId(id(1)),
|
||||
a::Expr::Parameter(a::ParameterId(id(1))),
|
||||
)]),
|
||||
},
|
||||
),
|
||||
);
|
||||
expected.definitions.insert(
|
||||
id(3),
|
||||
calculation_model("g", 3, a::Expr::Parameter(a::ParameterId(id(1)))),
|
||||
);
|
||||
succeeds(&bundle, ProjectLimits::default(), &expected, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_explicit_identity_classes_versions_and_long_forms() {
|
||||
let empty = single("");
|
||||
succeeds(&empty, ProjectLimits::default(), &empty_project(), &[]);
|
||||
let ordinary = format!("oa_entity e \"Entity\" id \"{}\" {{}}", id(2));
|
||||
let typed = calc("f", 3, "$x");
|
||||
let valid = single(&format!("{ordinary}\n{typed}"));
|
||||
assert!(language::check_mixed_bundle(&valid, ProjectLimits::default()).is_ok());
|
||||
// Each deletion starts from the same valid complete input.
|
||||
for (literal, expected_start) in [
|
||||
(format!("id \"{}\"", id(1)), None),
|
||||
(format!("id \"{}\"", id(2)), Some("oa_entity")),
|
||||
(format!("id \"{}\"", id(3)), Some("calculation")),
|
||||
] {
|
||||
let mut missing = valid.clone();
|
||||
let text = missing.files.get_mut("main.arcadia").unwrap();
|
||||
*text = text.replacen(&literal, "", 1);
|
||||
let start = match expected_start {
|
||||
None => text.find('{').unwrap(),
|
||||
Some("oa_entity") => {
|
||||
text.find("oa_entity").unwrap()
|
||||
+ text[text.find("oa_entity").unwrap()..].find('{').unwrap()
|
||||
}
|
||||
Some(word) => text.find(word).unwrap(),
|
||||
};
|
||||
let errors = fails(&missing, ProjectLimits::default());
|
||||
error_at(
|
||||
&missing,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Mixed source8 requires explicit UUIDs.",
|
||||
);
|
||||
}
|
||||
let mut parameter = valid.clone();
|
||||
let text = parameter.files.get_mut("main.arcadia").unwrap();
|
||||
*text = text.replace(&format!("input x id \"{}\"", id(1)), "input x");
|
||||
let start = text.find("calculation").unwrap();
|
||||
error_at(
|
||||
¶meter,
|
||||
&fails(¶meter, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Mixed source8 requires explicit UUIDs.",
|
||||
);
|
||||
for version in ["3", "7", "9"] {
|
||||
let mut bad = empty.clone();
|
||||
*bad.files.get_mut("main.arcadia").unwrap() =
|
||||
document("").replacen("arcadia 8", &format!("arcadia {version}"), 1);
|
||||
error_at(
|
||||
&bad,
|
||||
&fails(&bad, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span { start: 8, end: 9 },
|
||||
"invalid_source",
|
||||
"Mixed source requires arcadia 8.",
|
||||
);
|
||||
}
|
||||
for word in [
|
||||
"oa_engineering_property",
|
||||
"sa_engineering_property",
|
||||
"oa_analysis_check",
|
||||
"sa_analysis_check",
|
||||
"oa_analysis_case",
|
||||
"sa_analysis_case",
|
||||
] {
|
||||
let bad = single(&format!("{word} v \"Value\" id \"{}\" {{}}", id(2)));
|
||||
let errors = fails(&bad, ProjectLimits::default());
|
||||
let start = bad.files["main.arcadia"].find(word).unwrap();
|
||||
assert_eq!(errors[0].path, "main.arcadia");
|
||||
assert_eq!(errors[0].diagnostic.code, "invalid_source");
|
||||
assert_eq!(
|
||||
errors[0].diagnostic.span,
|
||||
Span {
|
||||
start,
|
||||
end: start + 1
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
errors[0].diagnostic.message.contains("short"),
|
||||
"{errors:#?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn chain() -> SourceBundle {
|
||||
let mut bundle = single("include \"part.arcadia\";");
|
||||
bundle.files.insert(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
);
|
||||
bundle.files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
bundle
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_exact_input_file_edge_and_depth_boundaries() {
|
||||
let empty = single("");
|
||||
assert_eq!(empty.files["main.arcadia"].len(), 78);
|
||||
for cap in [78, 77] {
|
||||
let limits = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_source_bytes: cap,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
if cap == 78 {
|
||||
succeeds(&empty, limits, &empty_project(), &[]);
|
||||
} else {
|
||||
error_at(
|
||||
&empty,
|
||||
&fails(&empty, limits),
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"invalid_source",
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
}
|
||||
let bundle = chain();
|
||||
assert_eq!(bundle.files.values().map(String::len).sum::<usize>(), 132);
|
||||
let fits = ProjectLimits {
|
||||
max_files: 3,
|
||||
max_include_edges: 2,
|
||||
max_include_depth: 2,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 132,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
};
|
||||
succeeds(&bundle, fits, &empty_project(), &[]);
|
||||
for (limits, path, span, code, message) in [
|
||||
(
|
||||
ProjectLimits {
|
||||
max_files: 2,
|
||||
..fits
|
||||
},
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"project_limit",
|
||||
"Source project exceeds 2 files.",
|
||||
),
|
||||
(
|
||||
ProjectLimits {
|
||||
max_include_edges: 1,
|
||||
..fits
|
||||
},
|
||||
"part.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"project_limit",
|
||||
"Include graph exceeds 1 edges.",
|
||||
),
|
||||
(
|
||||
ProjectLimits {
|
||||
max_include_depth: 1,
|
||||
..fits
|
||||
},
|
||||
"part.arcadia",
|
||||
Span { start: 0, end: 23 },
|
||||
"project_limit",
|
||||
"Include depth exceeds 1.",
|
||||
),
|
||||
(
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 131,
|
||||
..fits.model
|
||||
},
|
||||
..fits
|
||||
},
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"invalid_source",
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
),
|
||||
] {
|
||||
error_at(&bundle, &fails(&bundle, limits), path, span, code, message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_paths_missing_cycles_and_detached_files_are_errors() {
|
||||
let good = chain();
|
||||
succeeds(&good, ProjectLimits::default(), &empty_project(), &[]);
|
||||
let mut missing = good.clone();
|
||||
missing.files.remove("last.arcadia");
|
||||
error_at(
|
||||
&missing,
|
||||
&fails(&missing, ProjectLimits::default()),
|
||||
"part.arcadia",
|
||||
Span { start: 0, end: 23 },
|
||||
"missing_include",
|
||||
"Included file 'last.arcadia' is missing.",
|
||||
);
|
||||
let mut detached = good.clone();
|
||||
detached
|
||||
.files
|
||||
.insert("unused.arcadia".into(), "// valid but detached\n".into());
|
||||
error_at(
|
||||
&detached,
|
||||
&fails(&detached, ProjectLimits::default()),
|
||||
"unused.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"unreachable_file",
|
||||
"Supplied source file is not reachable from the entry file.",
|
||||
);
|
||||
let mut cycle = good.clone();
|
||||
cycle
|
||||
.files
|
||||
.insert("last.arcadia".into(), "include \"part.arcadia\";".into());
|
||||
error_at(
|
||||
&cycle,
|
||||
&fails(&cycle, ProjectLimits::default()),
|
||||
"last.arcadia",
|
||||
Span { start: 0, end: 23 },
|
||||
"include_cycle",
|
||||
"Include cycle: part.arcadia -> last.arcadia -> part.arcadia.",
|
||||
);
|
||||
let mut invalid = good.clone();
|
||||
invalid
|
||||
.files
|
||||
.insert("../escape.arcadia".into(), String::new());
|
||||
error_at(
|
||||
&invalid,
|
||||
&fails(&invalid, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"invalid_path",
|
||||
"Non-canonical source path '../escape.arcadia'.",
|
||||
);
|
||||
let mut collision = good;
|
||||
collision
|
||||
.files
|
||||
.insert("PART.arcadia".into(), "// detached uppercase\n".into());
|
||||
let errors = fails(&collision, ProjectLimits::default());
|
||||
error_at(
|
||||
&collision,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"duplicate_path",
|
||||
"Case-colliding source paths 'PART.arcadia' and 'part.arcadia'.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_final_nested_model_limit_is_distinct_from_syntax_entries() {
|
||||
let bundle = single(&calc("f", 2, "$x"));
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model("f", 2, a::Expr::Parameter(a::ParameterId(id(1)))),
|
||||
);
|
||||
// Two declarations fit both policies. Parameter and expression make four
|
||||
// final records; this does not pretend to set the internal entry quota.
|
||||
let limits = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_records: 4,
|
||||
max_graph_objects: 1,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
succeeds(&bundle, limits, &expected, &[]);
|
||||
let errors = fails(
|
||||
&bundle,
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_records: 3,
|
||||
..limits.model
|
||||
},
|
||||
..limits
|
||||
},
|
||||
);
|
||||
error_at(
|
||||
&bundle,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"model_limit",
|
||||
&format!(
|
||||
"Model exceeds admission limits (1 graph objects, 3 total records, {} ordered references).",
|
||||
usize::MAX
|
||||
),
|
||||
);
|
||||
let errors = fails(
|
||||
&bundle,
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_graph_objects: 0,
|
||||
..limits.model
|
||||
},
|
||||
..limits
|
||||
},
|
||||
);
|
||||
assert_eq!(errors[0].diagnostic.code, "model_limit");
|
||||
let start = bundle.files["main.arcadia"].find("calculation").unwrap();
|
||||
assert_eq!(
|
||||
errors[0].diagnostic.span,
|
||||
Span {
|
||||
start,
|
||||
end: start + 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_typed_reference_caller_allowance_has_a_real_positive_control() {
|
||||
let bundle = single(&calc("f", 2, "$x"));
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model("f", 2, a::Expr::Parameter(a::ParameterId(id(1)))),
|
||||
);
|
||||
let fits = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_step_references: 1,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
succeeds(&bundle, fits, &expected, &[]);
|
||||
let start = bundle.files["main.arcadia"].find("$x").unwrap() + 1;
|
||||
error_at(
|
||||
&bundle,
|
||||
&fails(
|
||||
&bundle,
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_step_references: 0,
|
||||
..fits.model
|
||||
},
|
||||
..fits
|
||||
},
|
||||
),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Source8 references limit exceeded.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_malformed_duplicate_and_wrong_domain_references() {
|
||||
let calculation = calc("f", 2, "1");
|
||||
let valid = single(&calculation);
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model("f", 2, a::Expr::Number(Exact::from_integer(1))),
|
||||
);
|
||||
succeeds(&valid, ProjectLimits::default(), &expected, &[]);
|
||||
|
||||
let mut malformed = valid.clone();
|
||||
*malformed.files.get_mut("main.arcadia").unwrap() = document(&calc("f", 2, ""));
|
||||
let start = malformed.files["main.arcadia"].find("body ;").unwrap() + 5;
|
||||
error_at(
|
||||
&malformed,
|
||||
&fails(&malformed, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Expected an analysis expression.",
|
||||
);
|
||||
|
||||
let mut unresolved = single("include \"typed.arcadia\";");
|
||||
unresolved
|
||||
.files
|
||||
.insert("typed.arcadia".into(), calc("f", 2, "@missing(x: 1)"));
|
||||
let start = unresolved.files["typed.arcadia"].find("@missing").unwrap() + 1;
|
||||
error_at(
|
||||
&unresolved,
|
||||
&fails(&unresolved, ProjectLimits::default()),
|
||||
"typed.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 7,
|
||||
},
|
||||
"invalid_source",
|
||||
"Unknown global reference '@missing'.",
|
||||
);
|
||||
|
||||
let ordinary = format!("oa_entity q \"Entity\" id \"{}\" {{}}", id(3));
|
||||
let wrong = single(&format!("{ordinary}\n{}", calc("f", 2, "@q(x: 1)")));
|
||||
let start = wrong.files["main.arcadia"].find("@q").unwrap() + 1;
|
||||
error_at(
|
||||
&wrong,
|
||||
&fails(&wrong, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Wrong reference domain for '@q': Element(Entity, Oa).",
|
||||
);
|
||||
|
||||
let duplicate = single(&format!("{calculation}\n{}", calc("f", 3, "1")));
|
||||
let errors = fails(&duplicate, ProjectLimits::default());
|
||||
let second = calc("f", 3, "1");
|
||||
let span = authored_span(&duplicate.files["main.arcadia"], &second);
|
||||
error_at(
|
||||
&duplicate,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
span,
|
||||
"invalid_source",
|
||||
"Duplicate global alias.",
|
||||
);
|
||||
assert_eq!(errors[0].related.len(), 1);
|
||||
assert_eq!(errors[0].related[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
errors[0].related[0].span,
|
||||
authored_span(&duplicate.files["main.arcadia"], &calculation)
|
||||
);
|
||||
|
||||
let second = calc("g", 2, "1");
|
||||
let duplicate_id = single(&format!("{calculation}\n{second}"));
|
||||
error_at(
|
||||
&duplicate_id,
|
||||
&fails(&duplicate_id, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
authored_span(&duplicate_id.files["main.arcadia"], &second),
|
||||
"invalid_source",
|
||||
"Duplicate global identity.",
|
||||
);
|
||||
|
||||
let duplicate_parameter = calc("f", 2, "$x").replace(
|
||||
"result Number;",
|
||||
&format!("input x id \"{}\" : Number; result Number;", id(4)),
|
||||
);
|
||||
let bad = single(&duplicate_parameter);
|
||||
let at = bad.files["main.arcadia"].rfind("input x").unwrap() + 6;
|
||||
error_at(
|
||||
&bad,
|
||||
&fails(&bad, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start: at,
|
||||
end: at + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Duplicate parameter name.",
|
||||
);
|
||||
let second_parameter = format!("input y id \"{}\" : Number;", id(1));
|
||||
let duplicate_parameter_id = calc("f", 2, "$x").replace(
|
||||
"result Number;",
|
||||
&format!("{second_parameter} result Number;"),
|
||||
);
|
||||
let bad = single(&duplicate_parameter_id);
|
||||
let at = authored_span(&bad.files["main.arcadia"], &second_parameter).start + 11;
|
||||
error_at(
|
||||
&bad,
|
||||
&fails(&bad, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start: at,
|
||||
end: at + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Duplicate parameter identity.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_unicode_columns_are_not_utf8_byte_columns() {
|
||||
let source = "arcadia 8;\r\nproject p \"港🌊\" id \"00000000-0000-0000-0000-000000000001\" { oa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {} }\r\n";
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), source.into())]),
|
||||
};
|
||||
let mut expected = empty_project();
|
||||
expected.name = "港🌊".into();
|
||||
expected.elements.insert(
|
||||
id(2),
|
||||
Element {
|
||||
id: id(2),
|
||||
alias: "w".into(),
|
||||
name: "Work".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::Oa(OaBody::Activity),
|
||||
},
|
||||
);
|
||||
let warnings = [
|
||||
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
|
||||
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
|
||||
].map(|(code, field, message)| SourceDiagnostic {
|
||||
path: "main.arcadia".into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic { severity: validation::Severity::Warning, code: code.into(), object_id: Some(id(2)), field: field.map(str::to_owned), message: message.into(), span: Span { start: 76, end: 141 }, line: 2, column: 60 },
|
||||
});
|
||||
succeeds(&bundle, ProjectLimits::default(), &expected, &warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_ordinary_parity_uses_an_independent_complete_model() {
|
||||
let b = single(&format!(
|
||||
"oa_entity team \"Team\" id \"{}\" {{ oa_actor sailor \"Sailor\" id \"{}\" {{ description \"海\"; }} }}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
let mut expected = empty_project();
|
||||
for (n, alias, name, description, parent, body) in [
|
||||
(2, "team", "Team", "", None, ElementBody::Oa(OaBody::Entity)),
|
||||
(
|
||||
3,
|
||||
"sailor",
|
||||
"Sailor",
|
||||
"海",
|
||||
Some(id(2)),
|
||||
ElementBody::Oa(OaBody::Actor),
|
||||
),
|
||||
] {
|
||||
expected.elements.insert(
|
||||
id(n),
|
||||
Element {
|
||||
id: id(n),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
parent,
|
||||
body,
|
||||
},
|
||||
);
|
||||
}
|
||||
succeeds(&b, ProjectLimits::default(), &expected, &[]);
|
||||
let ordinary = language::parse_with_report(&b.files["main.arcadia"]).unwrap();
|
||||
assert!(ordinary.migration.is_none());
|
||||
assert_eq!(ordinary.project, expected);
|
||||
let compiled = source_project::compile_bundle(&b, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, expected);
|
||||
assert_eq!(compiled.bundle, b);
|
||||
}
|
||||
@@ -25,7 +25,10 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti
|
||||
assert_eq!(baseline.sa.name, "Approved SA name");
|
||||
assert_ne!(baseline.oa.name, decoded.project.named(trace.oa));
|
||||
let mut expected: Value = serde_json::from_slice(JSON4).unwrap();
|
||||
assert!(expected.as_object().unwrap().get("definitions").is_none());
|
||||
assert!(decoded.project.definitions.is_empty());
|
||||
expected["schema_version"] = oa_core::SCHEMA_VERSION.into();
|
||||
expected["definitions"] = serde_json::json!({});
|
||||
assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected);
|
||||
let reopened =
|
||||
persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap())
|
||||
|
||||
@@ -1036,16 +1036,19 @@ fn source7_source_bundle_migration_is_token_only_and_new_link_stays_with_caller(
|
||||
let f = chain(Layer::Oa);
|
||||
let mut bundle = source_project::format_bundle(&f.p);
|
||||
let entry = bundle.files.get_mut(&bundle.entry).unwrap();
|
||||
assert!(entry.starts_with("arcadia 8;"));
|
||||
*entry = entry
|
||||
.replacen("arcadia 7;", "arcadia // preserve this header\r\n 6;", 1)
|
||||
.replacen("arcadia 8;", "arcadia // preserve this header\r\n 6;", 1)
|
||||
.replace('\n', "\r\n");
|
||||
assert!(entry.starts_with("arcadia // preserve this header"));
|
||||
assert_eq!(entry.matches(" 6;").count(), 1);
|
||||
let old = bundle.clone();
|
||||
let compiled = source_project::compile_bundle(&bundle, Default::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, f.p);
|
||||
assert_eq!(compiled.decoded.migration.as_ref().unwrap().from_schema, 6);
|
||||
for (path, source) in &old.files {
|
||||
let expected = if path == &old.entry {
|
||||
source.replacen(" 6;", " 7;", 1)
|
||||
source.replacen(" 6;", " 8;", 1)
|
||||
} else {
|
||||
source.clone()
|
||||
};
|
||||
|
||||
@@ -633,6 +633,23 @@ a { text-decoration: none; }
|
||||
<dd class="text-suffix"><p><span class="docutils literal">.arcadia</span></p>
|
||||
</dd>
|
||||
</dl>
|
||||
<section id="staged-source8-rust-read-api">
|
||||
<h2>Staged source8 Rust read API</h2>
|
||||
<p>The core library separately offers <span class="docutils literal"><span class="pre">language::check_mixed_bundle(&SourceBundle,</span> ProjectLimits)</span> for complete ordinary/typed source8 validation. Its opaque
|
||||
<span class="docutils literal">CheckedMixed</span> result borrows the unchanged input and exposes only <span class="docutils literal">project()</span>
|
||||
and <span class="docutils literal">warnings()</span>. Explicit UUIDs are required for the project, every ordinary
|
||||
and typed declaration, and typed parameters; no IDs are generated. All supplied
|
||||
canonical files must be reachable from the entry. Errors return no admitted
|
||||
model; success preserves actual located warnings and the complete model.</p>
|
||||
<p>This bounded API selects both retained text and entry producer checks and carries
|
||||
the caller's whole-project policy through admission. It does not promise an
|
||||
all-heap memory bound. It performs no filesystem discovery, migration, formatting,
|
||||
source stabilization, indexing, saving or history installation. Existing ordinary
|
||||
parse/compile/update APIs, CLI commands, desktop and server flows remain separate;
|
||||
a read result does not grant their typed editing or save authority. Generic and
|
||||
typed depth policies and platform qualification limits remain unchanged. This
|
||||
staged capability does not replace the broader conformance obligations below.</p>
|
||||
</section>
|
||||
<section id="scope-and-conformance">
|
||||
<h2>1. Scope and conformance</h2>
|
||||
<p>This specification defines the shared Operational Analysis (OA) and System
|
||||
|
||||
@@ -6,6 +6,26 @@ Arcadia Studio OA / SA Language and Editing Contract
|
||||
:Core implementation: ``crates/oa-core``
|
||||
:Text suffix: ``.arcadia``
|
||||
|
||||
Staged source8 Rust read API
|
||||
----------------------------------------
|
||||
|
||||
The core library separately offers ``language::check_mixed_bundle(&SourceBundle,
|
||||
ProjectLimits)`` for complete ordinary/typed source8 validation. Its opaque
|
||||
``CheckedMixed`` result borrows the unchanged input and exposes only ``project()``
|
||||
and ``warnings()``. Explicit UUIDs are required for the project, every ordinary
|
||||
and typed declaration, and typed parameters; no IDs are generated. All supplied
|
||||
canonical files must be reachable from the entry. Errors return no admitted
|
||||
model; success preserves actual located warnings and the complete model.
|
||||
|
||||
This bounded API selects both retained text and entry producer checks and carries
|
||||
the caller's whole-project policy through admission. It does not promise an
|
||||
all-heap memory bound. It performs no filesystem discovery, migration, formatting,
|
||||
source stabilization, indexing, saving or history installation. Existing ordinary
|
||||
parse/compile/update APIs, CLI commands, desktop and server flows remain separate;
|
||||
a read result does not grant their typed editing or save authority. Generic and
|
||||
typed depth policies and platform qualification limits remain unchanged. This
|
||||
staged capability does not replace the broader conformance obligations below.
|
||||
|
||||
1. Scope and conformance
|
||||
------------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Private carried-baseline phase-check implementation
|
||||
|
||||
2026-09-25. Based on `18073a6f2eabce83fc83997f094e331eec835a16`; implements the limited prerequisite selected in `harness/analysis-carried-phase-contract-20260925.md` (parent evidence commit `a2c781773454ee932a9e292e6dc93f551f9f86c5`). Source and tests are authored; compilation/runtime acceptance belong to parent gates and are not claimed here.
|
||||
|
||||
`mixed/carried.rs` introduces an opaque non-Clone input backed by either a fixed borrowed bundle with captured policy or a borrowed allocation-owning RetainedSnapshot. Every new constructor is `cfg(test)`. The retained fixture owns the actual complete SourceBundle and computes exactly F map slots, zero lower occurrence slots and all source/key/entry UTF-8 bytes. The parser cannot substitute a different bundle at invocation. There is no production emitter/old-owner transition, accepted-output result, public source8 activation or editing authority.
|
||||
|
||||
The shared parser implementation has explicit legacy versus carried mode. Existing entry functions choose legacy and preserve their separate metadata checkpoints. Carried mode checks C initially, C+file metadata before traversal, C+U+M before every position projection and at complete syntax return, even when C is zero. Checks use existing logical Slots arithmetic and inherited ceilings; they are not additional retained reservations. A narrow read-only Budget policy getter preserves all Limits values. Every snapshot still has one fresh Budget and normal independent Usage/Exact work, with exact caller ProjectLimits.
|
||||
|
||||
ParsedMixedSyntax retains the guard. Consumed resolution seeds its Ledger from that guard once, then runs the existing syntax accounting and subsequent reservation/release sequence unchanged. Successful ownership naturally retains the guard through syntax inside resolved/indexed owners. A cfg(test) immutable `phase_state` observer exposes syntax/Slots for sibling tests; it exposes no mutation or production constructor.
|
||||
|
||||
Temporal scope is deliberately limited: current-unit parsing and syntax string decoding/copying can precede aggregate phase refusal, and retained syntax text is counted during resolution. Existing grammar event checks remain before their declared allocations; this prerequisite does not add universal parser allocation admission. Model/serde/raw validator heap, allocator capacity/RSS and stack frames keep their established exclusions. Stronger before-growth obligations, output creation, whole actual-output equality and accepted edited-output ownership remain future requirements. This work neither closes nor waives them.
|
||||
|
||||
Twelve authored tests cover independent E/H bundle counts and whole Project oracles, real retained guard identity, exact new Usage, once-only resolver +C retention, parser 13/12 and 25/24 boundaries, resolver 17/16 and 29/28 entry peaks, 112/111 and 214/213 text peaks, canonical original diagnostic paths/spans, initial C/file checks, postdecode syntax text and ordinary position-key copy accounting, unchanged legacy versus explicit carried-zero timing, all captured Limits/ProjectLimits fields, real byte/token/reference/frame/Exact/model/file/depth/include policies, and OA/SA Unicode/CRLF compatibility through both index families. Nonzero occurrence and impossible arithmetic cases are explicitly labeled fault injection; the actual retained bundle owner has no occurrence storage.
|
||||
|
||||
E is 78 source bytes with C=(1,0,102), U 1 / M 11 and final (13,0,110). H is 132 source bytes with C=(3,0,180), U 3 / M 19 and final (25,0,212); its final comment contains actual é and is 6 UTF-8 bytes. These expectations come from the independent parent oracle, not application outputs. Syntax-only exact caps are not reported as full-resolve successes. Parser default resource spans remain 0..1; resolver restores 0..0 or the original header/include extent.
|
||||
|
||||
Zero carried mode intentionally refuses E cap 11 at preprojection, while legacy cap 11 still produces syntax and then refuses resolution at U+M 12. This distinction preserves the old API timing without numerically bypassing the new carried contract. Borrowed policy capture from a still-live fixture witness is test-only propagation evidence, not proof old auxiliary B was consumed or dropped.
|
||||
|
||||
No Cargo/build/test execution was performed by the implementation agent. Targeted rustfmt on owned files and raw-LF normalization are authoring checks only. Parent owns source review integration, exact-source commits, compiler/focused/full-suite gates and publication. Inherited full-suite failures and formatting issues are not addressed by this patch; parent gates will compare actual outcomes. The documented environment-sensitive upstream stack observation remains a limitation. Existing small-stack controls were not edited or waived.
|
||||
|
||||
Independent test-source review corrected two pre-execution fixture mistakes: use the accepted oa_property/sa_property keywords, and distinguish the parser include-edge error span 0..1 from the traversal include-depth span 0..23. These are source-review corrections, not recorded runtime failures.
|
||||
|
||||
## Recorded regression and slim-guard candidate
|
||||
|
||||
Parent's `d11b99bb` focused run passed all 12 carried tests, but its full run aborted at the unchanged `ordinary_index::tests::small_stack_success_and_late_header_failure_leave_no_partial_owner` with STATUS_STACK_OVERFLOW. The predecessor `18073a6f` full run had completed with 230 passing tests and 16 inherited failures, including that control. This is a failed current full gate, not an inherited-failure waiver or an accepted repair.
|
||||
|
||||
The exact-artifact LLDB trace reproduces the overflow during Windows heap allocation under formatting/Exact::parse_decimal, then exact_literal, syntax::binding, declarations::parse, State::parse_typed, Parser::unit, new parse_impl, parse_with_profile, parse, accepted/indexed helpers and the existing worker. The diagnostic child's killed exit is not a test pass. Parent measured individual prologues of parse_impl 11,144 bytes, parse_with_profile 312, accepted 1,752, indexed 1,704 and worker 7,752; these are not the complete active stack. Raw evidence remains in the parent gate records, including `carried-debugger-d11b99b-1.log` and `carried-full-d11b99b-1.stack-frames.json`.
|
||||
|
||||
The selected narrow candidate consumes CarriedInput before entering the shared parser. Exact captured policy transfers into Budget and syntax.limits; the retained CarriedGuard holds only Borrowed or Retained(&RetainedSnapshot). This removes the duplicate inline SnapshotPolicy payload from parsed/resolved/indexed/result carriers, while retained backing still borrows the actual allocation-owning bundle. Borrowed source identity remains bound through ParsedMixedSyntax.bundle. Explicit Some mode still distinguishes carried zero from legacy None, and all checks/reservations/Usage remain unchanged. No heap allocation or new logical storage was introduced. The parser wrapper remains uninlined and unchanged.
|
||||
|
||||
Only carried tests' guard-shape inspection and direct injected-arithmetic access follow the new internal guard type; all expected counts, limits, spans, outcomes and existing stack tests remain unchanged. The candidate is not claimed to establish a sole overflow cause or a successful repair before parent exact-source gates. All original failure evidence remains retained.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Private retained-entry producer prerequisite
|
||||
|
||||
Base `97a8703adc75467a7f8e75f5c6cb00ca542b707c`; implements the accepted
|
||||
`harness/analysis-entry-producer-contract-20260925.md` in the parent source tree.
|
||||
This is fixture-only phase/producer evidence, not emitted-output admission.
|
||||
|
||||
The explicit entry route also enables text producers and derives C and exact
|
||||
policy from the existing carried guard. Budget adds only optional scalar C/M.
|
||||
Existing legacy, carried-phase and text-only entry paths retain their checkpoints.
|
||||
U keeps its original event sites and work-limit precedence; prospective aggregate
|
||||
checks latch without changing totals. F is installed before traversal. Private
|
||||
State helpers reserve unique value-end keys and lexical owner occurrences before
|
||||
mutation. Successful grammar events count projection slots with checked addition.
|
||||
The new route reserves token-end slots and projection together, without the old
|
||||
borrowed-work counting Vec. A scalar quota checks each projected envelope, field
|
||||
and actual value before growth. Synthetic bodies remain included in seven.
|
||||
Per-file actual map/owner lengths and final independent metadata reconcile M;
|
||||
resolver C then U+M transfer and text reconciliation are unchanged.
|
||||
|
||||
Six tests in `mixed::carried::entry_producer` cover independent literal E and H
|
||||
syntax caps 13/12 and 25/24 separately from resolver 17/16 and 29/28, owner/value
|
||||
prefix refusals 4/7, and the 344-byte nested syntax-only fixture (U13, M89, P30,
|
||||
C1; 103/102). They check node/element envelopes, nested positions and owner order,
|
||||
quota underflow/leftover, direct unchanged collections on refusal and repeated
|
||||
map-key replacement. Labeled arithmetic controls exercise overflow, sticky-first
|
||||
failure, prospective U and work-limit precedence. Whole OA/SA Project, warnings,
|
||||
Usage, numeric work and retained totals are compared with text-only under actual
|
||||
and explicit zero guards. Old-route diagnostics and original bytes are checked.
|
||||
All byte policies are roomy 4096 so earlier text limits do not mask entry tests.
|
||||
|
||||
Source review identified a possible internal overflow call overwriting an already
|
||||
latched error. The helper now returns the first failure; a direct fault control
|
||||
checks that behavior. Repeated map keys now perform a zero-delta reservation
|
||||
before replacement, preserving a latched refusal without modifying the map;
|
||||
a direct control checks that branch too. These were corrected before compilation,
|
||||
not a runtime bug
|
||||
observation. Test review also caught that the independent inventory walker adds
|
||||
its own existing scratch slots: exact-cap ordinary syntax controls remain at
|
||||
29/37/103, while a separate roomy owner of the same literal runs that walk.
|
||||
No live policy is widened and no exact-cap resolver success is implied.
|
||||
No prior tests, stack sizes, policies, inherited failure dispositions
|
||||
or unrelated formatting are changed. Only touched Rust files were directly
|
||||
formatted with skip_children. Parent owns compiler, full disposition, Clippy,
|
||||
format and prologue/stack acceptance; no runtime result is claimed here.
|
||||
|
||||
Allocator overhead, token storage under its existing token ceiling, delimiter and
|
||||
other work stacks, numeric/decoder internals and model/validator heap remain
|
||||
outside this retained metric. The extra shared projector wrapper and scalar
|
||||
Budget/State fields require measured parent stack checks; no heap indirection or
|
||||
larger stack is introduced. Final output acceptance remains a separate task.
|
||||
|
||||
Initial a629ae01 focused compilation failed before any test ran: the quota
|
||||
control's unwrap_err required Debug on its successful EditDeclaration vector.
|
||||
The test now matches Err explicitly and panics on unexpected Ok, preserving
|
||||
the same assertion without changing production types. Original compiler output
|
||||
and source-bound receipt remain with parent gate evidence.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Private actual-output rename acceptance
|
||||
|
||||
Base `29417130c71f70c4eaefe8ed4c879efba78eb72c`. Implements the parent-selected
|
||||
`harness/analysis-mixed-acceptance-contract-20260925.md` (root b86c8dbc) within the
|
||||
private experimental mixed source path. No public source8, save, history,
|
||||
installation or repeated carried-source emission is enabled.
|
||||
|
||||
A consumed PendingRename now verifies its actual complete bundle through the
|
||||
combined text/entry producer parser and existing complete resolver/admission.
|
||||
CarriedInput and its slim guard borrow that entire pending owner; covariance
|
||||
shortens the desired borrow to the verification scope without another parser
|
||||
lifetime, policy copy in guards, arbitrary tuple constructor or heap wrapper.
|
||||
Narrow immutable views supply the exact bundle, policy and retained O together.
|
||||
The existing fixture constructors and old parser modes remain available unchanged.
|
||||
|
||||
The actual resolved expression arena supplies E for equality reservation against
|
||||
its full live storage. Complete actual Project PartialEq compares every field
|
||||
with desired; old name-exempt equality and pending's original E are not used.
|
||||
A static entry/default-span diagnostic reports mismatch. Warning inventory uses
|
||||
an exhaustive borrowed diagnostic-field match, checks empty related/non-Error
|
||||
shapes and sums checked entries/text. O+W is checked as a componentwise live
|
||||
subset and against inherited ceilings without resetting or double-charging the
|
||||
full ledger. Actual model/warnings move after equality; expression/occurrence
|
||||
arenas, syntax and complete pending guard drop before the bundle moves into
|
||||
AcceptedRename. Its private fields have read-only accessors, exact policies,
|
||||
final retained slots and separate verification Usage/numeric work; no input or
|
||||
desired borrow remains.
|
||||
|
||||
Eleven new tests use the acceptance module filter. Independent minimal literal
|
||||
and whole model verify 152/151 through one unchanged profile; warning literals
|
||||
and full diagnostics verify 161/160 and final O+W=(2,0,157). Exact policy fields
|
||||
and a lexical scope dropping all inputs exercise returned ownership. Previously
|
||||
authored OA/SA/scoped-global swap and UTF-8/CRLF/all-file literals are reused,
|
||||
including an untouched fourth file. Equal-byte wrong alias/name, malformed
|
||||
source and parseable containment-cycle corruption separate equality, syntax and
|
||||
model-admission refusal. Actual E1->3 corruption plus a labeled internal ledger
|
||||
fault distinguishes reserving3 from trusting old1, without a fabricated late
|
||||
end-to-end equality cap. Other internal controls cover warning shapes, final
|
||||
subset and checked inventory overflow.
|
||||
|
||||
Scoped 128 KiB workers perform actual verification/assertions/drop for deep128
|
||||
and wide129 expression nodes (success and target-only mismatch), plus the genuine
|
||||
151-byte late refusal. All original prerequisites are prepared outside those
|
||||
workers. Separate E0 target-only deep/wide and signed-zero geometry/scalar/history
|
||||
cases exercise complete equality. Positive-zero bits in actual returned model
|
||||
help distinguish moving the parsed model from substituting desired intent.
|
||||
|
||||
Before execution, source review prompted correcting the carried module summary;
|
||||
parent readback caught an unnamed-source test slice, replaced with the exact
|
||||
independent 71-byte original literal. Local type inspection corrected the f32
|
||||
geometry bit assertion. These are pre-execution authoring corrections, not
|
||||
runtime failures. No prior tests or stack sizes are edited. Only touched Rust
|
||||
files were directly formatted with skip_children; compiler, full prior outcome
|
||||
comparison, Clippy, format and measured prologue/stack gates belong to the parent.
|
||||
No runtime result was claimed at that initial handoff; see the correction below.
|
||||
|
||||
The selected retained/decode/equality domain remains logical, not universal heap
|
||||
or before-every-allocation accounting. Existing traversal/path-copy, delimiter,
|
||||
ordinary-open, Pratt and duplicate-name scratch keep their independent controls
|
||||
and stated exclusions; model/validator/numeric/decoder internals and native stack
|
||||
remain separate domains. File counts do not tightly bound traversal path bytes.
|
||||
Actual lifetime coercion and stack headroom still require the parent gates.
|
||||
|
||||
## Initial focused failure and test-oracle correction
|
||||
|
||||
Parent's initial focused run of `4872141c036c16825f1c098bd7cf44709897e814`
|
||||
compiled successfully and completed 9 passes / 2 failures. The failing assertions
|
||||
incorrectly required empty warnings in the rich OA/SA and four-file fixtures.
|
||||
The original run, log and artifact remain preserved under the parent's
|
||||
`analysis-output-acceptance-gates-20260925/acceptance-focused-4872141-1` records;
|
||||
artifact cba18de5 prefix, 43,060,224 bytes. Those failures are not erased or called
|
||||
production failures. The other nine controls, including both late byte caps and
|
||||
scoped 128 KiB verification/drop, passed in that run.
|
||||
|
||||
Source-derived correction: validation.rs emits unallocated_leaf first, with
|
||||
field allocation and message "Allocate leaf 'Work' to a performer."; its later
|
||||
coverage.validate call emits work_without_behavior_context, field None, message
|
||||
"Include this leaf activity/function in a process/chain or scenario through a
|
||||
work action or exchange occurrence." Both are Warning, object ID2, empty related,
|
||||
and map to the full Work declaration. The first local derivation missed the
|
||||
coverage advisory; independent review caught it before the correction commit.
|
||||
The pre-existing project expected_leaf_work_warnings oracle confirms both/order.
|
||||
|
||||
New tests now require both complete mapped diagnostics, with independent literal
|
||||
spans: rich OA/SA main338..403 line6 column1, or Unicode/CRLF351..416 line7 column1;
|
||||
four-file b.arcadia25..90 line2 column1. Warning storage is (2,0,227) for main,
|
||||
(2,0,221) for b.arcadia. Final rich entries5/text1169,1212,1243,1286 for OA LF,
|
||||
OA transformed, SA LF, SA transformed; four-file entries6/text747. Production
|
||||
acceptance and all original test bodies remain unchanged. This is a test-only
|
||||
oracle repair plus this note; corrected runtime acceptance awaits parent gates.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Private mixed emitted-unverified transition
|
||||
|
||||
2026-09-25. Implementation candidate from `fa7a980418ef99d772e67c66c28dfa7b5181f875`, under the accepted root contract `harness/analysis-mixed-emission-contract-20260925.md` published at `6c0d9e80`. This note reports source work; parent owns compilation and runtime acceptance.
|
||||
|
||||
`RenamePlan::into_unchanged` and `emit_changed_unverified` consume the plan. Both recheck the entire current bundle and reuse the complete original-coordinate edit validator; wrong-branch calls refuse. Unchanged returns the original admitted owner. Changed refuses every explicit carried guard, including zero-baseline mode, because dropping that guard cannot release its externally owned retained allocation.
|
||||
|
||||
Changed emission computes O as one entry per output file and text equal to all final source bytes plus all owned keys plus entry bytes. It reserves O once against the existing live plan ledger B (which already contains R and M) before output copies. Each canonical file has one final String; scalar sizing walks borrow existing maps. Original-coordinate slices and replacement names are appended without parsing, decoding, normalization or sequential textual substitution. Length and complete-file checks precede the transition. The old names map, edits map and entire source witness explicitly drop before the opaque PendingRename is created. Caller-owned original source and desired Project remain external. Pending owns the bundle, exact policies, O, original expression count E and desired borrow; it has no extraction, parser/adoption or accepted-result API. E is provenance, not a proven equality bound on arbitrary future faulty output.
|
||||
|
||||
The split API avoids a common large enum, callback framework, heap indirection or lint suppression. No producer hooks, carried constructor, semantic verification, public source8 dispatch, persistence or history behavior changed. Original stack controls and inherited failure/formatting dispositions are not edited or claimed repaired.
|
||||
|
||||
## Source checks and authored tests
|
||||
|
||||
Independent source reviewer inspected production before tests completed. Direct targeted rustfmt only; no Cargo, executable tests or compiler run by the implementer. Eleven new tests are selected by `emission`: ten in the new emission child and one appended to carried tests.
|
||||
|
||||
- Actual predecessor-compatible E entry/text caps 17/114 versus 16/114 and 17/113, and H caps 29/216 versus 28/216 and 29/215; expected pending O values (1,0,105) and (3,0,183), complete literal bytes and source/entry/key ownership.
|
||||
- No-op identity, usage, numeric work, warnings and retained ledger below copy-overlap byte ceilings; desired lifetime ends while unchanged owner survives.
|
||||
- Both wrong branches, touched/untouched stale bytes, entry/path/file-set changes, equal independently owned current input, and selected reused edit-validator failures. Checked-add overflow is labeled arithmetic fault injection.
|
||||
- All exact Limits/ProjectLimits fields and a pending result escaping the original source-bundle scope.
|
||||
- Rich OA/SA global/scoped swaps with repeated/reordered labels, independently authored whole files in LF and Unicode/CRLF variants; four-file cross-family swap with an untouched comment file; full ordinary category output preserving prose, commented references and historical UUID; combined growth/shrinkage with an unchanged file.
|
||||
- Actual retained and borrowed-zero carried sources: changed refuses; no-op preserves the original guard through a subsequent changed refusal.
|
||||
- Scoped 128 KiB worker success/no-op/refusal and local drops, with parser prerequisites constructed normally. The same test also moves complete deep 128-node and wide 129-node owners into workers for changed emission and unchanged extraction/drop, asserting E and whole output bytes; the isolated storage refusal uses the minimal fixture. Existing full-pipeline stack tests remain unchanged.
|
||||
|
||||
Oracle source: independent emission and rename-plan reports published with the contract/evidence, not output obtained from this emitter. Pre-execution review corrected a policy fixture that exceeded its caller byte/reference ceiling. Raw-byte inspection corrected the category expected literal to preserve its two specific CRLF line endings among LF lines rather than converting the whole file. These were authoring corrections, not runtime failures. Parent gates must still establish compile, actual boundary behavior, whole bytes, unchanged full-suite dispositions, Clippy and formatting.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Private Pratt parser stack-frame repair candidate
|
||||
|
||||
2026-09-25. Separate correction after emission source `19a4082117b88caf332198e4c685bde91005cb52`. Parent recorded eleven focused emission tests passing, then a full-suite STATUS_STACK_OVERFLOW (0xc00000fd) in the unchanged ordinary-index 128 KiB control. The prior `fa7a9804` full run completed. This note preserves the failed candidate rather than treating focused success as acceptance.
|
||||
|
||||
Parent's exact 19a4082 artifact is 42,402,816 bytes, SHA256 `777457cfefb8a3046e3f0809cabd936cfb742abe792befad2c50e05dfac1bf6b`. Its LLDB trace reproduces overflow in Windows allocation beneath formatting in Exact::parse_decimal, then exact_literal, syntax::binding, declarations::parse, State::parse_typed, Parser::unit and parse_impl. No emitter call is active. A debugger process killed with exit 0 is not a test pass. Raw parent records live in `.local/analysis-mixed-emission-gates-20260925/emission-full-19a4082-1.*` and `emission-debugger-19a4082-1.*`; integration owns publication.
|
||||
|
||||
Parent measured fixed active prologues: binding 27,272 B, declarations::parse 22,664 B, Parser::unit 11,864 B, State::parse_typed 6,952 B. The previously measured outer frames (parse_impl 11,048 B, profile wrapper 200 B, parse 280 B, accepted 1,432 B, indexed 1,496 B, worker 6,920 B) match fa7. These partial native reservations are not total stack use. No inherited owner field or parser source changed in emission, so duplicated inline policy is not the cause established here. Environment-sensitive headroom remains a limitation; the failed full run still blocks acceptance.
|
||||
|
||||
## Narrow correction
|
||||
|
||||
`syntax.rs` keeps the same iterative binding driver, task/value Vec allocations, initial active check/unbound branch, per-popped-task active check, and final value extraction. The current Task::Expression body moves mechanically to a private prefix step borrowing those Vecs and Cursor. Remaining task arms move mechanically to a continuation step, also borrowing the same containers. No recursive parser call, new collection, clone, Box/Arc, numeric parsing change, work debit, depth limit, logical reservation or producer-timing claim is introduced. All cursor mutations, pushes/pops, opening/closing, validation, spans and error order remain at the same points. Steps return unit or the same Error; the driver retains ownership and cleanup of its original stacks.
|
||||
|
||||
The intended native-stack improvement is that numeric prefix work no longer runs below all continuation construction temporaries in one large function. This is a source-level rationale, not an accepted measurement. Parent must measure the new active driver+prefix prologue sum against 27,272 B (and driver+continuation where relevant), then run unchanged stack controls and required focused/full gates. No test, fixture, allowance, RUST_MIN_STACK, Exact implementation or forwarding wrapper was changed. Independent source review checks all moved arms; targeted rustfmt and diff checks are the only implementer tools, with no compiler/runtime execution.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Private complete-target mixed rename planning
|
||||
|
||||
2026-09-24. Descendant implementation of base `ee500908e3d77182ddd54e12524f9b6c6eb43b60`, under accepted parent contract `dc87babf` (`harness/analysis-mixed-rename-plan-contract-20260924.md` on the evidence branch). This note describes source and authored tests, not completed runtime acceptance.
|
||||
|
||||
`AuthoredMixedProject::plan_rename` consumes the complete admitted source witness and borrows an immutable whole desired Project. `RenamePlan` retains that witness, desired borrow, changed qualified names and one original-coordinate physical edit map. Read-only methods expose source, desired, names, edits, full-current-bundle validation and combined complete output-byte sizing. There is no emitter, rewritten-output admission, public activation or legacy adapter.
|
||||
|
||||
The whole-target gate uses exhaustive record patterns, collection keys/counts and parameter position/identity/type checks. Existing PartialEq semantics govern all unchanged payloads, including signed floating zero. Its expression work vectors run under an E-entry reservation, where E is the retained raw expression-site count. Current expression roots are calculation bindings, engineering-property base, case overrides and check predicates; historical semantic fields have no analysis Expr. E scratch is released before changed names are collected; the final namespace set is dropped before edits are collected. Successful retained growth is R+M entries, zero occurrence slots and zero copied text. Parsing, numeric and frame Usage remain unchanged.
|
||||
|
||||
Both authenticated occurrence families feed one map. Validation checks original token spelling/UTF-8 authority, whole Word spans, physical nonoverlap and exact required key/qualified-target membership. Bundle validation includes entry, every path/file and untouched bytes. Sizing subtracts the sum of removed bytes before adding replacements per file, then includes all files in the inherited byte check. Emission/re-admission lifecycle and aggregate output storage remain required successors.
|
||||
|
||||
Independent source review found and prompted two pre-compiler repairs: preserve canonical file attribution for edit-reservation failures, and check ordered parameter shape before calculation-body equality. No runtime failure claim is attached to these source findings. Existing historical mixed/ordinary failures and prior baseline results remain unchanged; this work does not overwrite their records.
|
||||
|
||||
The 17 authored tests cover independently specified complete rich OA/SA and Unicode/CRLF maps and final-name sets; all36 category edits including real comment-separated references and historical UUID exclusion; identity/map/parameter/non-name field rejections; QuantityKind/Unit payload gate controls; simultaneous global/scoped swaps and collisions; immutable exact target/source borrowing; no-op; unchanged Usage and R+M retention; stale touched/untouched/path/entry/file-set controls; safe corrupted UTF-8/file authority and same-spelling wrong-callee identity; signed-zero geometry, scalar bounds/defaults and historical real facets; deep/wide old equality and target-only trees with small-stack success/drop and late refusal.
|
||||
|
||||
Storage oracle: rich OA B=resolved+65, E15/Q11/R11/M27; added peak38 and retention38. Inherited B+37/B+38 tests run the entire predecessor pipeline before planning, and the negative checks the last ordinary diagram header main[412..413). A separate diagram moved to canonical last include z.arcadia checks precise included-file allocation diagnostics, contingent on its complete predecessor pipeline fitting the unchanged profile. No test replaces/tightens a live owner's Budget to manufacture a later-phase failure. Wide/deep equality tests assert authored E128/E129; no unsupported isolated E-cap claim is made.
|
||||
|
||||
Combined-size oracle uses original930 bytes: ordinary31-byte node alias shrinks to1 while three typed f aliases grow to9; complete924 fits although typed-only intermediate954 would not. A12-byte replacement produces933 and must fail under the same inherited930-byte cap, including the untouched213-byte file. This tests planned sizing only, never output correctness.
|
||||
|
||||
All source/tests require parent compilation, focused runtime tests and existing broader gates. Direct rustfmt is the only executed Rust tool during authoring. Parent owns commit/publication and verification evidence; no Cargo/build/test execution was performed by this implementation agent.
|
||||
|
||||
## Recorded first runtime failure and narrow test repair
|
||||
|
||||
Parent's `9388fcf2` focused run compiled and passed four tests before the 128 KiB deep/wide worker terminated with Windows stack-overflow exit 0xc00000fd. Parent separately ran the remaining 16 tests from that exact original artifact: all passed, explicitly excluding the crashing test. This is not a 17-test acceptance result.
|
||||
|
||||
Diagnostic source `9e8e77e1` established that deep admission, whole original clone, rename success and successful plan drop completed; failure followed entry into the collision-rejection helper. Diagnostic source `8159a17d` further located the overflow inside that helper's `authored(bundle)` call: its flushed before-admission marker appeared, but its after-admission/before-plan marker did not. Thus that failing helper invocation had not entered rename planning. These observations do not prove a particular compiler frame size or establish model cloning as the cause.
|
||||
|
||||
The bounded repair removes diagnostic markers and restores the general helper. Only this small-stack test directly creates each fresh owner at worker scope and matches each planning result there. It still uses 128 KiB, deep 128 and wide 129 expression nodes, complete admission on the worker, success/plan-drop, late final-namespace collision/refusal-drop and non-name semantic refusal/drop for both shapes. Prior plan, diagnostic and desired owners are explicitly dropped before subsequent cases. No fixture depth/width or stack allowance is weakened; no production code changes. The repaired full 17-test focused run and broader gates remain parent-owned and pending.
|
||||
|
||||
The direct-call candidate at `70c20c3` also aborted the small-stack test after the same first four focused tests passed. That candidate is **not** an accepted repair. The next test-only candidate uses a compact mode loop: each shape runs no-op success, final-namespace collision and non-name refusal through one shared source local, one desired clone and one planning result expression, with explicit plan/failure/desired drops. Complete admission remains inside the 128 KiB worker for every case; depths and expected outcomes are unchanged. This removes repeated case-specific large debug temporaries as a bounded layout experiment; no exact compiler frame size or overflow cause is claimed. If it still fails, actual-frame diagnostics are required before further rearrangement. Parent-focused verification remains pending.
|
||||
|
||||
Parent's `f1c0956` debugger evidence supersedes the unlocated layout hypothesis: run3 backtrace reaches `mixed::parse_with_profile -> Parser::unit -> parse_typed -> declarations::parse -> syntax::binding -> Cursor::exact_literal -> Budget::exact -> Exact::parse_decimal -> BigInt::parse_bytes -> Windows heap allocation`, before any rename-plan call. The same artifact's debugger run2 passed, so the full admission-on-128-KiB observation remains an **UNRESOLVED environment-sensitive stack boundary**, not a demonstrated planner defect or a repaired production issue. Raw evidence is the parent-owned `rename-debugger-f1c0956-3.log` and receipt; all preceding failed full-pipeline observations remain valid and preserved.
|
||||
|
||||
The final narrowly scoped planner evaluator prepares each fresh complete admitted owner and immutable desired Project on the normal test thread. For all six deep128/wide129 × no-op/collision/non-name cases, a scoped128-KiB worker moves the complete owner, borrows desired, invokes planning, checks the exact expected outcome, and drops its successful plan or failure there. No owner/result escapes, policy resets or leaks occur. Desired is externally owned and destroyed outside the worker. Depth, width, worker stack allowance and all planning/drop cases are unchanged. This test now claims only planner comparison/ownership/drop behavior; it makes **no claim** that the complete upstream parse/admission pipeline reliably fits128KiB under every observed environment. Existing separate admission/index small-stack tests remain in the parent full-suite gates. Final focused/full runtime acceptance is still parent-owned and pending.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Public mixed read implementation
|
||||
|
||||
Base7b06b8ea, selected root contract bcddaba3. Public
|
||||
language::check_mixed_bundle returns opaque non-Clone CheckedMixed borrowing the
|
||||
complete original SourceBundle, with only immutable project/warnings accessors.
|
||||
Its private resolved field retains syntax, expression/occurrence vectors, ledger
|
||||
and admission evidence; there is no index, output, save or extraction authority.
|
||||
|
||||
The shared parser mixed hooks now compile in production. Ordinary callers still
|
||||
select None. The fresh constructor binds the actual bundle and exact ProjectLimits,
|
||||
derives existing internal policy once, selects Some(Borrowed) with zero carry and
|
||||
both producers, then consumes syntax through complete resolution/admission.
|
||||
The borrowed guard retains the actual source reference even when test-only
|
||||
Pending/Retained variants are absent. No duplicate live policy/model is added.
|
||||
Public schema/UUID requirement wording replaces private-staging wording only;
|
||||
all limiter phase/category/span transport remains inherited.
|
||||
|
||||
Private legacy index/update/write, ordinary-index/rename/emission/acceptance and
|
||||
fixture/pending constructors remain test-only. Shared parse/lower code remains
|
||||
private. Necessary retained fields have individual documented non-test dead-code
|
||||
allowances; no module-wide suppression, fake read, or new public lint-only API.
|
||||
Exact production compiler lint findings remain a parent gate, not inferred clean
|
||||
from source inspection. Existing298 tests are not edited.
|
||||
|
||||
One internal public_read test invokes the actual production parse helper and
|
||||
observes borrowedC0, both producer inventories, every inherited/derived policy
|
||||
field (including262144 entries independent of model records1), full empty-Project
|
||||
and warning parity, Usage/numeric parity and unchanged input. Three rustdoc blocks
|
||||
provide a compiling real usage example plus source-drop and source-mutation
|
||||
compile-fail cases with later result use. Independent worker owns the non-test
|
||||
mixed_source8_read integration target and its separate complete-model oracles.
|
||||
|
||||
Established docs/language.rst gains a bounded staged-read note without changing
|
||||
its broader conformance/version obligations. Existing docutils HTML5 generation
|
||||
with halt/report level2 regenerated language.html successfully. An initial local
|
||||
RST generation refused the new section formatting; the section separator and
|
||||
blank line were corrected before successful regeneration. No Rust compiler,
|
||||
Cargo test, CLI, debugger or publication was run by this worker.
|
||||
|
||||
Parent owns pinned compilation, all old/new runtime tests, doctests, strict
|
||||
Clippy, formatting disposition and production Parser stack regression checks.
|
||||
DEPTH_128_VALIDATED=false, existing depth policies, logical-accounting exclusions,
|
||||
full-pipeline limitation and deferred Linux qualification remain unchanged.
|
||||
## First non-test compile correction
|
||||
|
||||
Parent's integration-list build at ddf823a stopped with E0063: the semantic review
|
||||
row-key reader's Parser initializer still gated mixed:None under cfg(test).
|
||||
That initializer now supplies None in production too, preserving its ordinary
|
||||
facet-reading behavior. Source inventory found the other shared Parser initializers
|
||||
in language.rs and mixed.rs already coherent; language::legacy uses its own
|
||||
separate Parser type. No API, test or runtime policy changed. The failed build
|
||||
record remains preserved; no compiler was run for this repair by the worker.
|
||||
## Non-test integration result and warning cleanup
|
||||
|
||||
Parent's actual non-test integration suite at 8555e417 passed all ten tests with
|
||||
the independent complete-model and diagnostic assertions unchanged. Compilation
|
||||
reported an unused Budget::limits observer and a redundant private language self
|
||||
import shadowing the integration helper's public glob export. The observer is
|
||||
now cfg(test), matching all real callers; the integration import drops only self.
|
||||
No semantic implementation, policy, fixture or expected result changed. Parent
|
||||
explicitly authorized that one test import-line edit after its independent worker
|
||||
finished. This is warning cleanup, not a claim that later lint/full/doctest gates
|
||||
already passed. No compiler was run by this worker.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Private retained-syntax text producer prerequisite
|
||||
|
||||
2026-09-25. Candidate on source baseline `8d52dab97c92cc31baae9f20d8c324b55818aa5d`, selected root contract `harness/analysis-text-producer-contract-20260925.md` at local-only `17836587`. Root publication is parent-owned and server capacity is not reported recovered. This note records authored work, not compiler/runtime acceptance.
|
||||
|
||||
An explicit fixture-only `parse_text_producers` route derives bundle, guard and exact policies from CarriedInput. Existing legacy and carried-phase routes pass disabled mode. Budget adds only optional scalar carried/owned/temporary text counts; source/token/entry/reference/numeric Usage remains separate. Named storage checks use the inherited byte ceiling before copy/decode and preserve failed-reservation totals. Quoted text reserves raw payload P, reconciles decoded D<=P, and transfers the existing reservation continuously into owned text without a second fallible reservation. The conservative escaped-fit refusal is intentional only for this new route.
|
||||
|
||||
One shared raw scanner body has a bounded before-validation/refund hook. Its original Iterator API uses a no-op hook. A completed quoted token reserves before the existing serde validation; the temporary result String drops at the statement end before refund. Unclosed tokens skip decoding. New-mode storage refusal may precede malformed escape classification; old routes retain that classification. There is no new decoder or numeric conversion.
|
||||
|
||||
Typed and ordinary retained names, quoted fields, AST words/references/keys and projected key/reference copies reserve before creation. All three consumed UUID spelling sites drop then refund: ordinary optional IDs, typed identities, and ordinary HistoricalUuid. Nested field originals provisionally count as owned text alongside retained clones, then explicitly drop/refund; top-level fields and field_spans retain both copies. The declaration-kind spelling is borrowed from the original source. Existing duplicate description checks precede replacement/second grammar decoding.
|
||||
|
||||
Successful syntax has no outstanding decoder/scanner temporary reservation. Resolver seeds C once, transfers producer T once, and changes the existing complete text walk into an independent checked sum/equality check for this mode. Existing routes keep progressive text reservation. A cfg(test), read-only project inventory helper invokes that same bounded walk on syntax-only fixtures; it cannot replace bundle/policy/baseline authority.
|
||||
|
||||
Scope remains logical retained String lengths and named result temporaries. Decoder internal scratch/capacity, existing duplicate-name coordination copies, traversal/entry/metadata producer timing, model conversion/validation, Exact internals, allocator overhead and native stack retain their explicit separate domains. No PendingRename extraction, emitted-output parser transition, accepted result, public source8, persistence or history operation is added.
|
||||
|
||||
## Six focused authored tests
|
||||
|
||||
All new names are under `mixed::carried::text_producer`, selected by filter `text_producer`. Independent literal/count expectations came from the source-only text-producer oracle, not application outputs.
|
||||
|
||||
- Empty real owner E complete cap146/145, finalT8; escaped project cap491/490, finalT41; independent syntax inventory, complete expected Project and Usage parity, and unchanged carried-phase112 control.
|
||||
- Three-file escaped include cap358/357, C253/T45, final resolved298 and entry25. A separately labeled scanner-only state replay distinguishes scanner reservation from the identical later grammar threshold.
|
||||
- Projection-dominant syntax cap304/303, finalT83 and independent inventory304; nested/reference/all-three-UUID syntax fixture cap453/452, finalT49/inventory432. The arbitrary payload fixture is explicitly not asserted to admit a Project.
|
||||
- Raw/escaped Unicode, simple escapes, surrogate pairs and empty strings: rawP cap/refund and scanner hook order; malformed escapes/surrogates, unclosed tokens and explicit legacy error precedence. Arithmetic baselines are labeled separately from real-owner provenance.
|
||||
- Explicit zero-C stronger route OA/SA complete model, warnings, final retained storage, Usage and numeric-work parity; checked overflow/refund fault injections.
|
||||
- Project and typed duplicate descriptions with enough room for the first grammar decode but not a second, asserting the existing duplicate errors rather than a false successful overwrite.
|
||||
|
||||
No old test or worker-stack allowance changed. Implementer ran no Cargo, test executable or rustfmt; parent owns formatting, compiler, prologue/headroom and runtime gates. Independent production review found no initial semantic blocker; final review binds parent-formatted source and authored tests before commit. Preserve all prior recorded failures and compare actual full-suite dispositions rather than claiming they are unchanged from source inspection.
|
||||
|
||||
Parent gates at initial source6b0ef99 passed all six focused tests and completed
|
||||
the full library with259 passed /16 inherited failures. Strict Clippy rejected
|
||||
the ordinary quoted hook's is_some/expect pattern. The successor uses direct
|
||||
if-let matching with the same token span and decoder; no tests, limits or
|
||||
accounting operations changed. Final source gates remain separately required.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Ordinary source8 compatibility implementation
|
||||
|
||||
Base `edf096cd33350865ecfc3c65ec266a6c0d3c4000`, selected parent contract
|
||||
`harness/ordinary-source8-compatibility-contract-20260925.md` at root fb9b0c5c.
|
||||
The public writer already emits8. The ordinary Parser::unit version list now
|
||||
accepts8 and its unsupported-version diagnostic names the accurate range.
|
||||
Current ordinary resolution already checks model limits and returns no migration;
|
||||
3–7 retain their existing migration paths. No new converter or dispatch adapter
|
||||
was introduced. Comments distinguish this ordinary repair from private mixed
|
||||
analysis authoring.
|
||||
|
||||
Production change is intentionally confined to that version boundary. Long
|
||||
analysis constructors retain their explicit conversion refusal, and short typed
|
||||
declarations remain outside the ordinary grammar. Mixed cfg gates, formatter,
|
||||
public SourceIndex/update semantics, ID stabilization, resource/depth policies
|
||||
and codec switches are unchanged. Ordinary8 is not complete analysis/source8.
|
||||
|
||||
Six new tests use the ordinary_source8 module filter. They reuse the substantial
|
||||
six-file source-project example and historical reviewed-prose fixture for complete
|
||||
model/current formatter and bundle/index idempotence. Other groups cover3–7
|
||||
migration to stable8; nested included declarations with omitted IDs, UTF-8/CRLF,
|
||||
comments and actual returned index/identity spans; stale compiled input; short
|
||||
and long analysis rejection, future/malformed headers and caller limits. Public
|
||||
checked-save exercises both source and JSON, actual returned bytes/reopen/repeat
|
||||
save, and rejection of the real formatter's omitted definition catalog while
|
||||
preserving existing destination bytes. These are authored evaluators, not passing
|
||||
execution claims. The three-file CLI preview literals additionally compile to an independently constructed complete Project before and after only the team alias changes; actual CLI execution remains a separate parent gate.
|
||||
|
||||
The prior test exception below repairs fixture construction without weakening
|
||||
its semantics; all other prior test bodies/expectations remain unchanged. The16 formerly header-masked
|
||||
library workflows must run their intended nested/update/migration/transaction
|
||||
assertions; new downstream failures must be investigated on their own merits.
|
||||
Parent owns focused/full/integration/CLI/non-test consumer/Clippy/format gates,
|
||||
including previously unreached ordinary JSON branches. DEPTH_128_VALIDATED stays
|
||||
false, and ordinary8 does not qualify the private depth candidate, user-deferred
|
||||
Linux or the separately recorded full-pipeline setup limitation. Source/public
|
||||
save equality and no-staging-before-validation safeguards remain mandatory.
|
||||
|
||||
Only changed Rust files were directly formatted with skip_children; source review
|
||||
found no production blocker. Compiler/runtime acceptance remains parent-owned.
|
||||
|
||||
## Authorized pre-execution evaluator repair
|
||||
|
||||
Independent integration inventory found source7_contract's
|
||||
source7_source_bundle_migration_is_token_only_and_new_link_stays_with_caller
|
||||
attempted to replace formatted header7 with commented6, although the formatter
|
||||
already emits8. That replacement was a no-op; the later expected7 was stale too.
|
||||
Parent authorized this one existing evaluator correction: first assert actual
|
||||
header8, replace8 with the original commented6 fixture, assert the comment and
|
||||
single6 token, and expect only6->8 during migration. All exact file/comment/module,
|
||||
whole-model, caller ownership and recompilation assertions remain, as does the
|
||||
original test name. This is a static finding before execution, not a reported
|
||||
runtime failure; no other legacy evaluator or formatter is changed.
|
||||
|
||||
## Focused fixture repair after initial execution
|
||||
|
||||
Parent's focused run at 33b86a4d completed five tests and failed one: the example
|
||||
bundle has no trace records, contrary to the new fixture's nonempty assertion.
|
||||
The repair explicitly authors a Participant trace from fixed OA rescue-team UUID
|
||||
102 to fixed SA system UUID301, checks endpoint kinds and the complete trace,
|
||||
and includes it in the unchanged whole-model source/bundle roundtrip assertions.
|
||||
Historical reviewed-prose trace/baseline coverage remains. Independent source
|
||||
review also identified the actual short case spellings oa_analysis/sa_analysis;
|
||||
both now appear in the ordinary refusal table alongside the invalid case aliases.
|
||||
Production and the other evaluators are unchanged. These corrections are not a
|
||||
runtime pass claim; the initial five-pass/one-failure record remains preserved.
|
||||
## Obsolete private header assertions after full execution
|
||||
|
||||
Parent's full run at 520e50ce completed 298 tests: 294 passed and four failed.
|
||||
All 16 previously header-masked ordinary workflows passed. The four failures
|
||||
were private analysis tests that still expected an empty ordinary schema8
|
||||
project to be rejected. The scoped repair accepts ordinary8 with no migration,
|
||||
compares complete ordinary OA/SA projects where the existing fixed-ID fixtures
|
||||
provide that oracle, and explicitly embeds actual typed source or canonical
|
||||
text in schema8 to preserve public typed refusal. Declaration refusal coverage
|
||||
now includes versions3 through8. Existing numeric/lowering/model assertions stay.
|
||||
Only these evaluator names change:
|
||||
|
||||
- inherited_version_root_and_short_word_contracts_do_not_enable_public8 ->
|
||||
inherited_version_root_and_short_word_contracts_do_not_enable_public_typed8
|
||||
- public_source_headers_and_existing_numeric_token_controls_are_unchanged ->
|
||||
public_ordinary_source8_preserves_typed_refusal_and_numeric_controls
|
||||
|
||||
Production is unchanged. The initial 294-pass/four-failure evidence remains;
|
||||
repaired outcomes require the parent's next runtime gate.
|
||||
## Historical schema4 JSON oracle reconciliation
|
||||
|
||||
Parent's r3_migration integration run at 0c07f53a completed four passes and one
|
||||
failure. Its whole-JSON expected value updated only schema_version, omitting the
|
||||
current definitions catalog. Project4::into_current already creates an empty
|
||||
catalog; Project serialization requires that schema8 field. The test now asserts
|
||||
that the original fixture has no definitions field and the decoded catalog is
|
||||
empty, then adds exactly definitions:{} to expected JSON. Complete JSON equality,
|
||||
historical baseline/prose checks, migration and reopen checks remain unchanged.
|
||||
No production behavior or frozen historical fixture changes.
|
||||
|
||||
The parent reports completed core, six other selected integration gates and both
|
||||
actual CLI operations at 0c07f53a; that evidence remains reusable because this
|
||||
repair changes only the affected integration evaluator and this note. The initial
|
||||
r3 four-pass/one-failure record remains preserved. Parent owns the affected r3
|
||||
inventory/rerun and all-target Clippy/format checks on the repaired pin; this note
|
||||
does not claim their outcome or repeat unrelated runtime acceptance.
|
||||
Reference in New Issue
Block a user