Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edf096cd33 | ||
|
|
4872141c03 | ||
|
|
29417130c7 | ||
|
|
a629ae01d4 | ||
|
|
97a8703adc | ||
|
|
6b0ef99eca |
+153
-22
@@ -1018,6 +1018,28 @@ fn edit_positions_from_tokens(
|
|||||||
token: impl Fn(usize) -> Lexeme,
|
token: impl Fn(usize) -> Lexeme,
|
||||||
count: usize,
|
count: usize,
|
||||||
value_ends: Option<&BTreeMap<usize, 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>> {
|
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||||
let mut ends: Vec<_> = (0..count).map(|at| token(at).span.end).collect();
|
let mut ends: Vec<_> = (0..count).map(|at| token(at).span.end).collect();
|
||||||
let mut stack = Vec::new();
|
let mut stack = Vec::new();
|
||||||
@@ -1029,12 +1051,32 @@ fn edit_positions_from_tokens(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct Positions<'a, F> {
|
struct Positions<'a, F, H> {
|
||||||
token_at: F,
|
token_at: F,
|
||||||
ends: Vec<usize>,
|
ends: Vec<usize>,
|
||||||
value_ends: Option<&'a BTreeMap<usize, 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 {
|
fn token(&self, start: usize) -> usize {
|
||||||
let mut low = 0;
|
let mut low = 0;
|
||||||
let mut high = self.ends.len();
|
let mut high = self.ends.len();
|
||||||
@@ -1048,24 +1090,30 @@ fn edit_positions_from_tokens(
|
|||||||
}
|
}
|
||||||
low
|
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
|
fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, ast)| {
|
.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 start = (self.token_at)(self.token(value.span.start) - 1).span.start;
|
||||||
let end = (self.token_at)(self.token(value.span.end)).span.end;
|
let end = (self.token_at)(self.token(value.span.end)).span.end;
|
||||||
(
|
(self.text)(key.len(), Span { start, end })?;
|
||||||
|
Ok((
|
||||||
key.clone(),
|
key.clone(),
|
||||||
EditField {
|
EditField {
|
||||||
span: Span { start, end },
|
span: Span { start, end },
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
)
|
))
|
||||||
})
|
})
|
||||||
.collect()
|
.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 index = self.token(ast.offset());
|
||||||
let span = Span {
|
let span = Span {
|
||||||
start: ast.offset(),
|
start: ast.offset(),
|
||||||
@@ -1080,23 +1128,33 @@ fn edit_positions_from_tokens(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let kind = match ast {
|
let kind = match ast {
|
||||||
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)),
|
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)?),
|
||||||
Ast::Array(items, _) => {
|
Ast::Array(items, _) => EditKind::List(
|
||||||
EditKind::List(items.iter().map(|item| self.value(item)).collect())
|
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),
|
Ast::HistoricalUuid(id, _) => EditKind::HistoricalUuid(*id),
|
||||||
_ => EditKind::Atom,
|
_ => EditKind::Atom,
|
||||||
};
|
};
|
||||||
EditValue { span, kind }
|
Ok(EditValue { span, kind })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let positions = Positions {
|
let mut positions = Positions {
|
||||||
token_at: &token,
|
token_at: &token,
|
||||||
ends,
|
ends,
|
||||||
value_ends,
|
value_ends,
|
||||||
|
text,
|
||||||
|
source,
|
||||||
|
quota: &mut quota,
|
||||||
};
|
};
|
||||||
unit.declarations
|
let result = unit
|
||||||
|
.declarations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|declaration| {
|
.map(|declaration| {
|
||||||
if declaration.id_insertion.is_some() {
|
if declaration.id_insertion.is_some() {
|
||||||
@@ -1109,6 +1167,7 @@ fn edit_positions_from_tokens(
|
|||||||
},
|
},
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
positions.consume(7, declaration.span.start)?;
|
||||||
let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) {
|
let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) {
|
||||||
&["kind", "alias", "id_keyword", "id"]
|
&["kind", "alias", "id_keyword", "id"]
|
||||||
} else {
|
} else {
|
||||||
@@ -1132,12 +1191,23 @@ fn edit_positions_from_tokens(
|
|||||||
start: declaration.body.start - 1,
|
start: declaration.body.start - 1,
|
||||||
end: declaration.body.end + 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> {
|
struct Parser<'a> {
|
||||||
source: &'a str,
|
source: &'a str,
|
||||||
@@ -1196,11 +1266,21 @@ impl Parser<'_> {
|
|||||||
if self.current().class != TokenClass::String {
|
if self.current().class != TokenClass::String {
|
||||||
return Err(self.error("Expected a double-quoted string."));
|
return Err(self.error("Expected a double-quoted string."));
|
||||||
}
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
let span = self.current().span;
|
||||||
|
#[cfg(test)]
|
||||||
|
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();
|
let value = serde_json::from_str(self.text()).unwrap();
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
fn identifier(&mut self) -> Result<String, Error> {
|
fn identifier(&mut self) -> Result<String, Error> {
|
||||||
|
self.own_text(self.text().len(), self.current().span)?;
|
||||||
let value = self.text().to_owned();
|
let value = self.text().to_owned();
|
||||||
if !crate::valid_alias(&value) {
|
if !crate::valid_alias(&value) {
|
||||||
return Err(self.error(std::format!("Invalid or reserved alias '{value}'.")));
|
return Err(self.error(std::format!("Invalid or reserved alias '{value}'.")));
|
||||||
@@ -1230,11 +1310,32 @@ impl Parser<'_> {
|
|||||||
message: "Expected UUID in 8-4-4-4-12 hexadecimal form.".into(),
|
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",
|
code: "invalid_source",
|
||||||
offset,
|
offset,
|
||||||
message: "Invalid UUID identity.".into(),
|
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> {
|
||||||
|
#[cfg(test)]
|
||||||
|
if let Some(mixed) = &mut self.mixed {
|
||||||
|
mixed.own_text(bytes, span)?;
|
||||||
|
}
|
||||||
|
#[cfg(not(test))]
|
||||||
|
let _ = (bytes, span);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn release_text(&mut self, bytes: usize) {
|
||||||
|
#[cfg(test)]
|
||||||
|
if let Some(mixed) = &mut self.mixed {
|
||||||
|
mixed.release_text(bytes);
|
||||||
|
}
|
||||||
|
#[cfg(not(test))]
|
||||||
|
let _ = bytes;
|
||||||
}
|
}
|
||||||
fn fields(&mut self) -> Result<BTreeMap<String, Ast>, Error> {
|
fn fields(&mut self) -> Result<BTreeMap<String, Ast>, Error> {
|
||||||
self.take("{")?;
|
self.take("{")?;
|
||||||
@@ -1242,6 +1343,13 @@ impl Parser<'_> {
|
|||||||
while self.text() != "}" {
|
while self.text() != "}" {
|
||||||
let offset = self.current().span.start;
|
let offset = self.current().span.start;
|
||||||
let (key, value, _) = self.field()?;
|
let (key, value, _) = self.field()?;
|
||||||
|
self.own_text(
|
||||||
|
key.len(),
|
||||||
|
Span {
|
||||||
|
start: offset,
|
||||||
|
end: offset,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
if fields.insert(key.clone(), value).is_some() {
|
if fields.insert(key.clone(), value).is_some() {
|
||||||
return Err(Error {
|
return Err(Error {
|
||||||
code: "invalid_source",
|
code: "invalid_source",
|
||||||
@@ -1249,12 +1357,16 @@ impl Parser<'_> {
|
|||||||
message: std::format!("Duplicate field '{key}'."),
|
message: std::format!("Duplicate field '{key}'."),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let bytes = key.len();
|
||||||
|
drop(key);
|
||||||
|
self.release_text(bytes);
|
||||||
}
|
}
|
||||||
self.take("}")?;
|
self.take("}")?;
|
||||||
Ok(fields)
|
Ok(fields)
|
||||||
}
|
}
|
||||||
fn field(&mut self) -> Result<(String, Ast, Span), Error> {
|
fn field(&mut self) -> Result<(String, Ast, Span), Error> {
|
||||||
self.syntax_entry()?;
|
self.syntax_entry()?;
|
||||||
|
self.own_text(self.text().len(), self.current().span)?;
|
||||||
let key = self.text().to_owned();
|
let key = self.text().to_owned();
|
||||||
if !matches!(
|
if !matches!(
|
||||||
self.current().class,
|
self.current().class,
|
||||||
@@ -1271,6 +1383,10 @@ impl Parser<'_> {
|
|||||||
.span
|
.span
|
||||||
.end;
|
.end;
|
||||||
self.take(";")?;
|
self.take(";")?;
|
||||||
|
#[cfg(test)]
|
||||||
|
if let Some(mixed) = &mut self.mixed {
|
||||||
|
mixed.projection(1, Span { start, end })?;
|
||||||
|
}
|
||||||
Ok((key, value, Span { start, end }))
|
Ok((key, value, Span { start, end }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1304,7 +1420,12 @@ impl Parser<'_> {
|
|||||||
.span
|
.span
|
||||||
.end;
|
.end;
|
||||||
if let Some(mixed) = &mut self.mixed {
|
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
|
result
|
||||||
@@ -1328,6 +1449,9 @@ impl Parser<'_> {
|
|||||||
offset,
|
offset,
|
||||||
message: "Expected a UUID after uuid.".into(),
|
message: "Expected a UUID after uuid.".into(),
|
||||||
})?;
|
})?;
|
||||||
|
let bytes = value.len();
|
||||||
|
drop(value);
|
||||||
|
self.release_text(bytes);
|
||||||
Ok(Ast::HistoricalUuid(id, offset))
|
Ok(Ast::HistoricalUuid(id, offset))
|
||||||
}
|
}
|
||||||
"@" => {
|
"@" => {
|
||||||
@@ -1368,6 +1492,7 @@ impl Parser<'_> {
|
|||||||
Ok(Ast::Literal(value, offset))
|
Ok(Ast::Literal(value, offset))
|
||||||
}
|
}
|
||||||
TokenClass::Identifier | TokenClass::Keyword => {
|
TokenClass::Identifier | TokenClass::Keyword => {
|
||||||
|
self.own_text(self.text().len(), self.current().span)?;
|
||||||
let value = self.text().to_owned();
|
let value = self.text().to_owned();
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
Ok(Ast::Word(value, offset))
|
Ok(Ast::Word(value, offset))
|
||||||
@@ -1514,6 +1639,7 @@ impl Parser<'_> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let (key, value, span) = self.field()?;
|
let (key, value, span) = self.field()?;
|
||||||
|
self.own_text(key.len(), span)?;
|
||||||
let declaration = &mut declarations[owner];
|
let declaration = &mut declarations[owner];
|
||||||
if declaration.fields.insert(key.clone(), value).is_some() {
|
if declaration.fields.insert(key.clone(), value).is_some() {
|
||||||
return Err(Error {
|
return Err(Error {
|
||||||
@@ -1584,8 +1710,8 @@ impl Parser<'_> {
|
|||||||
return Err(self.error("Use the short source8 analysis declaration word."));
|
return Err(self.error("Use the short source8 analysis declaration word."));
|
||||||
}
|
}
|
||||||
self.syntax_entry()?;
|
self.syntax_entry()?;
|
||||||
let word = self.text().to_owned();
|
let word = self.current().text(self.source);
|
||||||
let kind = declaration_kind(&word)
|
let kind = declaration_kind(word)
|
||||||
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
|
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(mixed) = &mut self.mixed {
|
if let Some(mixed) = &mut self.mixed {
|
||||||
@@ -1626,7 +1752,12 @@ impl Parser<'_> {
|
|||||||
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
|
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(mixed) = &mut self.mixed {
|
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 {
|
declarations.push(Declaration {
|
||||||
kind,
|
kind,
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ pub struct Budget {
|
|||||||
pub usage: Usage,
|
pub usage: Usage,
|
||||||
numeric: WorkMeter,
|
numeric: WorkMeter,
|
||||||
failed: Option<Error>,
|
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 {
|
impl Budget {
|
||||||
pub fn new(limits: Limits) -> Result<Self, Error> {
|
pub fn new(limits: Limits) -> Result<Self, Error> {
|
||||||
@@ -62,8 +73,153 @@ impl Budget {
|
|||||||
.map_err(|e| error(Span::default(), e.to_string()))?,
|
.map_err(|e| error(Span::default(), e.to_string()))?,
|
||||||
),
|
),
|
||||||
failed: None,
|
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.
|
/// Immutable exact policy snapshot for private deliberate-snapshot continuation.
|
||||||
pub(super) fn limits(&self) -> Limits {
|
pub(super) fn limits(&self) -> Limits {
|
||||||
self.limits
|
self.limits
|
||||||
@@ -105,6 +261,9 @@ impl Budget {
|
|||||||
span,
|
span,
|
||||||
"syntax entries",
|
"syntax entries",
|
||||||
)?;
|
)?;
|
||||||
|
if let Some(e) = &self.entries {
|
||||||
|
self.check_producer_entries(e.carried, e.metadata, 1, span)?;
|
||||||
|
}
|
||||||
self.usage.entries += 1;
|
self.usage.entries += 1;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ impl Cursor<'_, '_> {
|
|||||||
{
|
{
|
||||||
return Err(error(span, "Expected a valid reference or parameter name."));
|
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();
|
let name = self.text().to_owned();
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
Ok(Reference { name, span })
|
Ok(Reference { name, span })
|
||||||
@@ -87,8 +88,8 @@ impl Cursor<'_, '_> {
|
|||||||
{
|
{
|
||||||
return Err(error(span, "Expected a quoted string."));
|
return Err(error(span, "Expected a quoted string."));
|
||||||
}
|
}
|
||||||
let value =
|
let raw = &self.source[span.start..span.end];
|
||||||
serde_json::from_str(self.text()).map_err(|_| error(span, "Invalid quoted string."))?;
|
let value = self.budget.quoted_text(raw, span)?;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,9 @@ impl Cursor<'_, '_> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let id = Id::parse_str(&spelling).map_err(|_| error(literal, "Invalid UUID identity."))?;
|
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 {
|
Ok(IdentitySyntax {
|
||||||
value: Some(id),
|
value: Some(id),
|
||||||
keyword: Some(keyword),
|
keyword: Some(keyword),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Source8 significant-token adapter over the shared raw scanner.
|
//! 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};
|
use super::{Budget, Error, Span, error};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -33,7 +33,20 @@ pub fn tokens(source: &str, budget: &mut Budget) -> Result<Vec<Token>, Error> {
|
|||||||
start: offset,
|
start: offset,
|
||||||
end: 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;
|
offset = token.span.end;
|
||||||
let class = match token.class {
|
let class = match token.class {
|
||||||
RawClass::Word => Class::Word,
|
RawClass::Word => Class::Word,
|
||||||
|
|||||||
@@ -68,8 +68,11 @@ pub(in crate::language) struct State<'a> {
|
|||||||
budget: &'a mut Budget,
|
budget: &'a mut Budget,
|
||||||
model: &'a mut ModelCounts,
|
model: &'a mut ModelCounts,
|
||||||
typed: Vec<declarations::Declaration>,
|
typed: Vec<declarations::Declaration>,
|
||||||
pub(in crate::language) value_ends: BTreeMap<usize, usize>,
|
value_ends: BTreeMap<usize, usize>,
|
||||||
pub(in crate::language) lexical_owners: Vec<Option<usize>>,
|
lexical_owners: Vec<Option<usize>>,
|
||||||
|
pending_projection: usize,
|
||||||
|
reserved_values: usize,
|
||||||
|
reserved_owners: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ordinary_error(error: super::Error) -> Error {
|
fn ordinary_error(error: super::Error) -> Error {
|
||||||
@@ -93,6 +96,55 @@ fn lexeme(token: lex::Token) -> Lexeme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State<'_> {
|
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> {
|
pub(in crate::language) fn token(&self, at: usize) -> Option<Lexeme> {
|
||||||
self.tokens.get(at).copied().map(lexeme)
|
self.tokens.get(at).copied().map(lexeme)
|
||||||
}
|
}
|
||||||
@@ -250,7 +302,7 @@ fn parse_with_profile(
|
|||||||
limits: ProjectLimits,
|
limits: ProjectLimits,
|
||||||
profile: Limits,
|
profile: Limits,
|
||||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||||
parse_impl(bundle, limits, profile, None)
|
parse_impl(bundle, limits, profile, None, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_impl<'s>(
|
fn parse_impl<'s>(
|
||||||
@@ -258,6 +310,8 @@ fn parse_impl<'s>(
|
|||||||
limits: ProjectLimits,
|
limits: ProjectLimits,
|
||||||
profile: Limits,
|
profile: Limits,
|
||||||
carried: Option<carried::CarriedGuard<'s>>,
|
carried: Option<carried::CarriedGuard<'s>>,
|
||||||
|
text_producers: bool,
|
||||||
|
entry_producers: bool,
|
||||||
) -> Result<ParsedMixedSyntax<'s>, Vec<SourceDiagnostic>> {
|
) -> Result<ParsedMixedSyntax<'s>, Vec<SourceDiagnostic>> {
|
||||||
let entry_source = bundle.files.get(&bundle.entry).map_or("", String::as_str);
|
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 at_entry = |error| located(&bundle.entry, entry_source, ordinary_error(error));
|
||||||
@@ -279,6 +333,14 @@ fn parse_impl<'s>(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let mut budget = Budget::new(profile).map_err(at_entry)?;
|
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 {
|
if let Some(input) = &carried {
|
||||||
input.check(&budget, 0, 0).map_err(at_entry)?;
|
input.check(&budget, 0, 0).map_err(at_entry)?;
|
||||||
}
|
}
|
||||||
@@ -291,7 +353,14 @@ fn parse_impl<'s>(
|
|||||||
.index_bytes(bytes, Span::default())
|
.index_bytes(bytes, Span::default())
|
||||||
.map_err(at_entry)?;
|
.map_err(at_entry)?;
|
||||||
let mut metadata = bundle.files.len();
|
let mut metadata = bundle.files.len();
|
||||||
if let Some(input) = &carried {
|
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)?;
|
input.check(&budget, 0, metadata).map_err(at_entry)?;
|
||||||
} else {
|
} else {
|
||||||
budget
|
budget
|
||||||
@@ -315,6 +384,7 @@ fn parse_impl<'s>(
|
|||||||
.expect("traversal checked source");
|
.expect("traversal checked source");
|
||||||
let failure = |error| located(path, source, ordinary_error(error));
|
let failure = |error| located(path, source, ordinary_error(error));
|
||||||
let tokens = lex::tokens(source, &mut budget).map_err(failure)?;
|
let tokens = lex::tokens(source, &mut budget).map_err(failure)?;
|
||||||
|
let metadata_before = budget.producer_entries();
|
||||||
let mut parser = Parser {
|
let mut parser = Parser {
|
||||||
source,
|
source,
|
||||||
tokens: Vec::new(),
|
tokens: Vec::new(),
|
||||||
@@ -329,6 +399,9 @@ fn parse_impl<'s>(
|
|||||||
typed: Vec::new(),
|
typed: Vec::new(),
|
||||||
value_ends: BTreeMap::new(),
|
value_ends: BTreeMap::new(),
|
||||||
lexical_owners: Vec::new(),
|
lexical_owners: Vec::new(),
|
||||||
|
pending_projection: 0,
|
||||||
|
reserved_values: 0,
|
||||||
|
reserved_owners: 0,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let ordinary = parser
|
let ordinary = parser
|
||||||
@@ -338,7 +411,19 @@ fn parse_impl<'s>(
|
|||||||
drop(parser);
|
drop(parser);
|
||||||
// Projected ordinary AST nodes/fields are bounded before constructing a
|
// Projected ordinary AST nodes/fields are bounded before constructing a
|
||||||
// second position representation. Count references as slots, not syntax.
|
// 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
|
metadata = metadata
|
||||||
.checked_add(tokens.len())
|
.checked_add(tokens.len())
|
||||||
.and_then(|n| n.checked_add(state.value_ends.len()))
|
.and_then(|n| n.checked_add(state.value_ends.len()))
|
||||||
@@ -355,12 +440,19 @@ fn parse_impl<'s>(
|
|||||||
.lowering_entries(metadata, Span::default())
|
.lowering_entries(metadata, Span::default())
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
}
|
}
|
||||||
let positions = language::edit_positions_from_tokens(
|
let positions = language::edit_positions_from_tokens_with_producers(
|
||||||
source,
|
source,
|
||||||
&ordinary,
|
&ordinary,
|
||||||
|at| lexeme(tokens[at]),
|
|at| lexeme(tokens[at]),
|
||||||
tokens.len(),
|
tokens.len(),
|
||||||
Some(&state.value_ends),
|
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| {
|
.map_err(|e| {
|
||||||
vec![SourceDiagnostic {
|
vec![SourceDiagnostic {
|
||||||
@@ -369,6 +461,23 @@ fn parse_impl<'s>(
|
|||||||
related: Vec::new(),
|
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 {
|
for (raw, span) in &ordinary.includes {
|
||||||
traversal.include(path, raw, *span)?;
|
traversal.include(path, raw, *span)?;
|
||||||
}
|
}
|
||||||
@@ -393,6 +502,15 @@ fn parse_impl<'s>(
|
|||||||
}
|
}
|
||||||
traversal.finish()?;
|
traversal.finish()?;
|
||||||
budget.active().map_err(at_entry)?;
|
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 {
|
if let Some(input) = &carried {
|
||||||
input
|
input
|
||||||
.check(&budget, budget.usage.entries, metadata)
|
.check(&budget, budget.usage.entries, metadata)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Fixture-constructible carried phase authority, not an emitted-output result.
|
//! Complete pending-backed authority and fixture-only carried snapshots.
|
||||||
//! Checks existing logical phase totals; does not meter every parser allocation.
|
//! Checks existing logical phase totals; does not meter every parser allocation.
|
||||||
use super::*;
|
use super::*;
|
||||||
use project::accounting::{Ledger, Slots};
|
use project::accounting::{Ledger, Slots};
|
||||||
|
use project::ordinary_index::rename::emission::PendingRename;
|
||||||
|
|
||||||
struct SnapshotPolicy {
|
struct SnapshotPolicy {
|
||||||
limits: Limits,
|
limits: Limits,
|
||||||
@@ -15,6 +16,7 @@ struct RetainedSnapshot {
|
|||||||
retained: Slots,
|
retained: Slots,
|
||||||
}
|
}
|
||||||
enum SnapshotBacking<'s> {
|
enum SnapshotBacking<'s> {
|
||||||
|
Pending(&'s PendingRename<'s>),
|
||||||
Borrowed {
|
Borrowed {
|
||||||
bundle: &'s SourceBundle,
|
bundle: &'s SourceBundle,
|
||||||
policy: SnapshotPolicy,
|
policy: SnapshotPolicy,
|
||||||
@@ -27,6 +29,7 @@ pub(super) struct CarriedInput<'s> {
|
|||||||
backing: SnapshotBacking<'s>,
|
backing: SnapshotBacking<'s>,
|
||||||
}
|
}
|
||||||
enum GuardBacking<'s> {
|
enum GuardBacking<'s> {
|
||||||
|
Pending(&'s PendingRename<'s>),
|
||||||
Borrowed,
|
Borrowed,
|
||||||
Retained(&'s RetainedSnapshot),
|
Retained(&'s RetainedSnapshot),
|
||||||
}
|
}
|
||||||
@@ -37,8 +40,26 @@ pub(super) struct CarriedGuard<'s> {
|
|||||||
backing: GuardBacking<'s>,
|
backing: GuardBacking<'s>,
|
||||||
}
|
}
|
||||||
impl<'s> CarriedInput<'s> {
|
impl<'s> CarriedInput<'s> {
|
||||||
|
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>) {
|
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
|
||||||
match self.backing {
|
match self.backing {
|
||||||
|
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 } => (
|
SnapshotBacking::Borrowed { bundle, policy } => (
|
||||||
bundle,
|
bundle,
|
||||||
policy,
|
policy,
|
||||||
@@ -62,6 +83,7 @@ impl<'s> CarriedInput<'s> {
|
|||||||
impl CarriedGuard<'_> {
|
impl CarriedGuard<'_> {
|
||||||
pub(super) fn slots(&self) -> Slots {
|
pub(super) fn slots(&self) -> Slots {
|
||||||
match &self.backing {
|
match &self.backing {
|
||||||
|
GuardBacking::Pending(pending) => pending.carried_slots(),
|
||||||
GuardBacking::Borrowed => Slots::default(),
|
GuardBacking::Borrowed => Slots::default(),
|
||||||
GuardBacking::Retained(owner) => owner.retained,
|
GuardBacking::Retained(owner) => owner.retained,
|
||||||
}
|
}
|
||||||
@@ -85,7 +107,45 @@ impl CarriedGuard<'_> {
|
|||||||
/// Input already binds the actual bundle; callers cannot supply a replacement.
|
/// Input already binds the actual bundle; callers cannot supply a replacement.
|
||||||
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||||
let (bundle, policy, guard) = input.into_parts();
|
let (bundle, policy, guard) = input.into_parts();
|
||||||
parse_impl(bundle, policy.project_limits, policy.limits, Some(guard))
|
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)]
|
#[cfg(test)]
|
||||||
@@ -139,3 +199,9 @@ impl<'s> CarriedInput<'s> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod text_producer;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod entry_producer;
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,17 @@ use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
pub(in crate::language::analysis_source) mod accounting;
|
pub(in crate::language::analysis_source) mod accounting;
|
||||||
use accounting::{Ledger, Slots, add};
|
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)
|
||||||
|
}
|
||||||
pub(in crate::language::analysis_source) mod ordinary_index;
|
pub(in crate::language::analysis_source) mod ordinary_index;
|
||||||
|
|
||||||
/// Read-only authority: every component originates in the consumed syntax.
|
/// Read-only authority: every component originates in the consumed syntax.
|
||||||
|
|||||||
@@ -104,9 +104,14 @@ struct Walk<'a, 'b> {
|
|||||||
budget: &'b Budget,
|
budget: &'b Budget,
|
||||||
work: Vec<Item<'a>>,
|
work: Vec<Item<'a>>,
|
||||||
lower: LowerStorage,
|
lower: LowerStorage,
|
||||||
|
text_total: Option<usize>,
|
||||||
}
|
}
|
||||||
impl<'a> Walk<'a, '_> {
|
impl<'a> Walk<'a, '_> {
|
||||||
fn text(&mut self, text: &str, at: Span) -> Result<(), TypedError> {
|
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
|
self.ledger
|
||||||
.reserve(self.budget, Slots::text(text.len()), at)
|
.reserve(self.budget, Slots::text(text.len()), at)
|
||||||
}
|
}
|
||||||
@@ -236,6 +241,19 @@ pub(super) fn syntax(
|
|||||||
ledger: &mut Ledger,
|
ledger: &mut Ledger,
|
||||||
) -> Result<LowerStorage, Failure> {
|
) -> Result<LowerStorage, Failure> {
|
||||||
let at = Span::default();
|
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
|
ledger
|
||||||
.reserve(
|
.reserve(
|
||||||
&syntax.budget,
|
&syntax.budget,
|
||||||
@@ -251,6 +269,7 @@ pub(super) fn syntax(
|
|||||||
budget: &syntax.budget,
|
budget: &syntax.budget,
|
||||||
work: Vec::new(),
|
work: Vec::new(),
|
||||||
lower: LowerStorage::default(),
|
lower: LowerStorage::default(),
|
||||||
|
text_total: producer_text.map(|_| 0),
|
||||||
};
|
};
|
||||||
for (file, unit) in syntax.files.iter().enumerate() {
|
for (file, unit) in syntax.files.iter().enumerate() {
|
||||||
let mut count = || -> Result<(), TypedError> {
|
let mut count = || -> Result<(), TypedError> {
|
||||||
@@ -348,5 +367,12 @@ pub(super) fn syntax(
|
|||||||
};
|
};
|
||||||
count().map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
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)
|
Ok(walk.lower)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
//! Owned output only. No parser, extraction or accepted-result conversion.
|
//! Owned unverified emission; consumed actual-output verification is a private child.
|
||||||
|
pub(in crate::language::analysis_source) mod acceptance;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::language::analysis_source::budget::Limits;
|
use crate::language::analysis_source::budget::Limits;
|
||||||
use crate::source_project::ProjectLimits;
|
use crate::source_project::ProjectLimits;
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<'_> {
|
impl Iterator for Scanner<'_> {
|
||||||
type Item = Token;
|
type Item = Token;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<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 source = self.source;
|
||||||
let bytes = source.as_bytes();
|
let bytes = source.as_bytes();
|
||||||
let start = self.cursor;
|
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 mut legacy_number_part = false;
|
||||||
let class = if matches!(first, b' ' | b'\t' | b'\r' | b'\n') {
|
let class = if matches!(first, b' ' | b'\t' | b'\r' | b'\n') {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
@@ -123,11 +140,24 @@ impl Iterator for Scanner<'_> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if closed && serde_json::from_str::<String>(&source[start..self.cursor]).is_ok() {
|
let valid = if closed {
|
||||||
Class::String
|
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 {
|
} else {
|
||||||
Class::Error
|
false
|
||||||
}
|
};
|
||||||
|
if valid { Class::String } else { Class::Error }
|
||||||
} else if b"{}();,[]:=@$+-*/<>!".contains(&first) {
|
} else if b"{}();,[]:=@$+-*/<>!".contains(&first) {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
if matches!(first, b'<' | b'>' | b'=' | b'!') && bytes.get(self.cursor) == Some(&b'=') {
|
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'.';
|
legacy_number_part = first == b'.';
|
||||||
Class::Error
|
Class::Error
|
||||||
};
|
};
|
||||||
Some(Token {
|
Ok(Some(Token {
|
||||||
class,
|
class,
|
||||||
span: Span {
|
span: Span {
|
||||||
start,
|
start,
|
||||||
end: self.cursor,
|
end: self.cursor,
|
||||||
},
|
},
|
||||||
legacy_number_part,
|
legacy_number_part,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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.
|
||||||
Reference in New Issue
Block a user