Compare commits

...
12 changed files with 764 additions and 33 deletions
+89 -18
View File
@@ -1018,6 +1018,16 @@ 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>> { ) -> 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 +1039,15 @@ 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,
} }
impl<F: Fn(usize) -> Lexeme> Positions<'_, F> { impl<F: Fn(usize) -> Lexeme, H: FnMut(usize, Span) -> Result<(), Box<Diagnostic>>>
Positions<'_, F, H>
{
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 +1061,28 @@ 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); 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>> {
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,21 +1097,28 @@ 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,
}; };
unit.declarations unit.declarations
.iter() .iter()
@@ -1132,7 +1156,7 @@ 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)?),
}, },
}, },
)) ))
@@ -1196,11 +1220,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 +1264,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 +1297,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 +1311,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,
@@ -1328,6 +1394,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 +1437,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 +1584,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 +1655,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 {
@@ -38,6 +38,12 @@ pub struct Budget {
pub usage: Usage, pub usage: Usage,
numeric: WorkMeter, numeric: WorkMeter,
failed: Option<Error>, failed: Option<Error>,
text: Option<ProducerText>,
}
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 +68,97 @@ 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,
}) })
} }
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
@@ -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,
@@ -93,6 +93,19 @@ fn lexeme(token: lex::Token) -> Lexeme {
} }
impl State<'_> { impl State<'_> {
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 +263,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)
} }
fn parse_impl<'s>( fn parse_impl<'s>(
@@ -258,6 +271,7 @@ 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,
) -> 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 +293,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)?;
} }
@@ -355,12 +377,18 @@ 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_text(
source, source,
&ordinary, &ordinary,
|at| lexeme(tokens[at]), |at| lexeme(tokens[at]),
tokens.len(), tokens.len(),
Some(&state.value_ends), Some(&state.value_ends),
|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 {
@@ -393,6 +421,9 @@ fn parse_impl<'s>(
} }
traversal.finish()?; traversal.finish()?;
budget.active().map_err(at_entry)?; budget.active().map_err(at_entry)?;
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)
@@ -85,7 +85,28 @@ 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,
)
}
/// 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,
)
} }
#[cfg(test)] #[cfg(test)]
@@ -139,3 +160,6 @@ impl<'s> CarriedInput<'s> {
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[cfg(test)]
mod text_producer;
@@ -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)
} }
+38 -8
View File
@@ -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,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.