feat(source): expose borrowed read-only mixed source eight validation

This commit is contained in:
2026-09-24 21:20:11 -07:00
parent 7b06b8ea1b
commit 042e25efc2
12 changed files with 314 additions and 44 deletions
+4 -31
View File
@@ -3,10 +3,9 @@ mod raw_tokens;
#[path = "language/semantic_source.rs"] #[path = "language/semantic_source.rs"]
mod semantic_source; mod semantic_source;
mod source3_bridge; mod source3_bridge;
// Staged typed analysis only. Public mixed source8 admission waits for complete // Mixed source8 is exposed only through the separate borrowed read-only checker.
// declaration lowering, source updates and the root-owned resource gate.
#[cfg(test)]
mod analysis_source; mod analysis_source;
pub use analysis_source::{CheckedMixed, check_mixed_bundle};
#[path = "migration_v2/source.rs"] #[path = "migration_v2/source.rs"]
mod v2_source; mod v2_source;
use crate::*; use crate::*;
@@ -480,7 +479,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
cursor: 0, cursor: 0,
value_depth: 0, value_depth: 0,
limits, limits,
#[cfg(test)]
mixed: None, mixed: None,
} }
.document() .document()
@@ -916,7 +914,6 @@ pub(crate) fn parse_unit(
cursor: 0, cursor: 0,
value_depth: 0, value_depth: 0,
limits, limits,
#[cfg(test)]
mixed: None, mixed: None,
} }
.unit(entry, true) .unit(entry, true)
@@ -1215,12 +1212,10 @@ struct Parser<'a> {
cursor: usize, cursor: usize,
value_depth: usize, value_depth: usize,
limits: ParseLimits, limits: ParseLimits,
#[cfg(test)]
mixed: Option<analysis_source::mixed::State<'a>>, mixed: Option<analysis_source::mixed::State<'a>>,
} }
impl Parser<'_> { impl Parser<'_> {
fn token(&self, index: usize) -> Option<Lexeme> { fn token(&self, index: usize) -> Option<Lexeme> {
#[cfg(test)]
if let Some(mixed) = &self.mixed { if let Some(mixed) = &self.mixed {
return mixed.token(index); return mixed.token(index);
} }
@@ -1236,7 +1231,6 @@ impl Parser<'_> {
}) })
} }
fn syntax_entry(&mut self) -> Result<(), Error> { fn syntax_entry(&mut self) -> Result<(), Error> {
#[cfg(test)]
{ {
let span = self.current().span; let span = self.current().span;
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
@@ -1266,9 +1260,7 @@ 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; let span = self.current().span;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
let raw = &self.source[span.start..span.end]; let raw = &self.source[span.start..span.end];
let value = mixed.quoted_text(raw, span)?; let value = mixed.quoted_text(raw, span)?;
@@ -1290,9 +1282,8 @@ impl Parser<'_> {
} }
fn optional_id(&mut self) -> Result<Id, Error> { fn optional_id(&mut self) -> Result<Id, Error> {
if self.text() != "id" { if self.text() != "id" {
#[cfg(test)]
if self.mixed.is_some() { if self.mixed.is_some() {
return Err(self.error("Private mixed syntax requires explicit IDs.")); return Err(self.error("Mixed source8 requires explicit UUIDs."));
} }
return Ok(Id::new_v4()); return Ok(Id::new_v4());
} }
@@ -1321,21 +1312,15 @@ impl Parser<'_> {
Ok(id) Ok(id)
} }
fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> { fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
mixed.own_text(bytes, span)?; mixed.own_text(bytes, span)?;
} }
#[cfg(not(test))]
let _ = (bytes, span);
Ok(()) Ok(())
} }
fn release_text(&mut self, bytes: usize) { fn release_text(&mut self, bytes: usize) {
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
mixed.release_text(bytes); 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("{")?;
@@ -1383,7 +1368,6 @@ impl Parser<'_> {
.span .span
.end; .end;
self.take(";")?; self.take(";")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
mixed.projection(1, Span { start, end })?; mixed.projection(1, Span { start, end })?;
} }
@@ -1412,7 +1396,6 @@ impl Parser<'_> {
self.value_depth += 1; self.value_depth += 1;
let result = self.value_inner(); let result = self.value_inner();
self.value_depth -= 1; self.value_depth -= 1;
#[cfg(test)]
if let Ok(ast) = &result { if let Ok(ast) = &result {
let end = self let end = self
.token(self.cursor - 1) .token(self.cursor - 1)
@@ -1433,7 +1416,6 @@ impl Parser<'_> {
fn value_inner(&mut self) -> Result<Ast, Error> { fn value_inner(&mut self) -> Result<Ast, Error> {
let token = self.current(); let token = self.current();
let offset = token.span.start; let offset = token.span.start;
#[cfg(test)]
if let Some(mixed) = &self.mixed if let Some(mixed) = &self.mixed
&& (token.class == TokenClass::Number || matches!(self.text(), "-" | "+")) && (token.class == TokenClass::Number || matches!(self.text(), "-" | "+"))
{ {
@@ -1541,17 +1523,14 @@ impl Parser<'_> {
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> { fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
let mut header = if entry { let mut header = if entry {
self.take("arcadia")?; self.take("arcadia")?;
#[cfg(test)]
let mixed_mode = self.mixed.is_some(); let mixed_mode = self.mixed.is_some();
#[cfg(not(test))]
let mixed_mode = false;
if if mixed_mode { if if mixed_mode {
self.text() != "8" self.text() != "8"
} else { } else {
!matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8") !matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8")
} { } {
return Err(self.error(if mixed_mode { return Err(self.error(if mixed_mode {
"Private mixed syntax requires arcadia 8." "Mixed source requires arcadia 8."
} else { } else {
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8." "Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
})); }));
@@ -1561,7 +1540,6 @@ impl Parser<'_> {
self.take(";")?; self.take(";")?;
let start = self.current().span.start; let start = self.current().span.start;
self.take("project")?; self.take("project")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
mixed.record(false, Span { start, end: start })?; mixed.record(false, Span { start, end: start })?;
} }
@@ -1606,7 +1584,6 @@ impl Parser<'_> {
let mut open: Vec<usize> = Vec::new(); let mut open: Vec<usize> = Vec::new();
loop { loop {
let offset = self.current().span.start; let offset = self.current().span.start;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed if let Some(mixed) = &mut self.mixed
&& mixed.typed_header(self.cursor, open.is_empty()) && mixed.typed_header(self.cursor, open.is_empty())
{ {
@@ -1658,7 +1635,6 @@ impl Parser<'_> {
if !includes_allowed { if !includes_allowed {
return Err(self.error("An include requires a source bundle; single-string parsing never reads files.")); return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
} }
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
mixed.include(Span { mixed.include(Span {
start: offset, start: offset,
@@ -1705,7 +1681,6 @@ impl Parser<'_> {
header = Some(entry_header); header = Some(entry_header);
continue; continue;
} }
#[cfg(test)]
if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) { if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) {
return Err(self.error("Use the short source8 analysis declaration word.")); return Err(self.error("Use the short source8 analysis declaration word."));
} }
@@ -1713,7 +1688,6 @@ impl Parser<'_> {
let word = self.current().text(self.source); 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)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
mixed.record( mixed.record(
matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)), matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)),
@@ -1750,7 +1724,6 @@ impl Parser<'_> {
let body_start = self.current().span.end; let body_start = self.current().span.end;
self.take("{")?; self.take("{")?;
let lexical_owner = open.last().map(|&owner| declarations[owner].id); let lexical_owner = open.last().map(|&owner| declarations[owner].id);
#[cfg(test)]
if let Some(mixed) = &mut self.mixed { if let Some(mixed) = &mut self.mixed {
let span = Span { let span = Span {
start: offset, start: offset,
@@ -1,14 +1,16 @@
//! Unresolved typed source8 syntax, staged for the complete source adapter. //! Private typed syntax and complete read-only mixed source8 resolution.
//! This module does not admit a Project or enable public source8.
mod budget; mod budget;
mod cursor; mod cursor;
mod declarations; mod declarations;
#[cfg(test)]
mod index; mod index;
mod lex; mod lex;
mod lower; mod lower;
pub(super) mod mixed; pub(super) mod mixed;
mod syntax; mod syntax;
#[cfg(test)]
mod update; mod update;
#[cfg(test)]
mod write; mod write;
#[cfg(test)] #[cfg(test)]
@@ -18,6 +20,7 @@ use super::Span;
use crate::analysis::{BinaryOp, UnaryOp}; use crate::analysis::{BinaryOp, UnaryOp};
use crate::analysis_numeric::Exact; use crate::analysis_numeric::Exact;
use budget::Budget; use budget::Budget;
pub use mixed::project::{CheckedMixed, check_mixed_bundle};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error { pub struct Error {
@@ -104,6 +107,7 @@ pub enum Binding {
Expression(Expression), Expression(Expression),
} }
#[cfg(test)]
pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> { pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
let tokens = lex::tokens(source, budget)?; let tokens = lex::tokens(source, budget)?;
let mut cursor = cursor::Cursor::new(source, &tokens, budget); let mut cursor = cursor::Cursor::new(source, &tokens, budget);
@@ -294,6 +294,7 @@ impl Budget {
} }
} }
} }
#[cfg(test)]
pub fn numeric_units(&self) -> u64 { pub fn numeric_units(&self) -> u64 {
self.numeric.usage().numeric_units self.numeric.usage().numeric_units
} }
@@ -42,6 +42,7 @@ impl Cursor<'_, '_> {
self.cursor += 1; self.cursor += 1;
Ok(span) Ok(span)
} }
#[cfg(test)]
pub(super) fn end(&self) -> Result<(), Error> { pub(super) fn end(&self) -> Result<(), Error> {
if self.cursor != self.tokens.len() { if self.cursor != self.tokens.len() {
Err(error( Err(error(
@@ -2,12 +2,15 @@
//! single complete-Project semantic admission after merging these additions. //! single complete-Project semantic admission after merging these additions.
use super::declarations::{self as d, Declaration, DeclarationKind}; use super::declarations::{self as d, Declaration, DeclarationKind};
use super::*; use super::*;
#[cfg(test)]
use crate::Project;
use crate::analysis as a; use crate::analysis as a;
use crate::analysis_numeric::Dimension; use crate::analysis_numeric::Dimension;
use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody}; use crate::{Element, ElementBody, Id, Kind, Layer, SharedBody};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
/// Caller-assigned file key; spans are original offsets within that file. /// Caller-assigned file key; spans are original offsets within that file.
#[cfg(test)]
pub(super) struct Unit { pub(super) struct Unit {
pub file: usize, pub file: usize,
pub declarations: Vec<Declaration>, pub declarations: Vec<Declaration>,
@@ -180,6 +183,7 @@ impl Tables {
Ok(()) Ok(())
} }
#[cfg(test)]
fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> { fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> {
let at = Span::default(); let at = Span::default();
self.insert(project.id, &project.alias, Category::Project, at, budget)?; self.insert(project.id, &project.alias, Category::Project, at, budget)?;
@@ -270,6 +274,7 @@ impl Tables {
/// Inputs must come from the same live Budget and stabilization pass. No scan, /// Inputs must come from the same live Budget and stabilization pass. No scan,
/// exact conversion or reference charge occurs here. The ordinary skeleton is /// exact conversion or reference charge occurs here. The ordinary skeleton is
/// not semantically admitted: its typed forward references may need additions. /// not semantically admitted: its typed forward references may need additions.
#[cfg(test)]
pub(super) fn lower( pub(super) fn lower(
ordinary: &Project, ordinary: &Project,
units: &[Unit], units: &[Unit],
@@ -13,7 +13,7 @@
//! value-end entries, projected value/field/header/declaration positions, lexical //! value-end entries, projected value/field/header/declaration positions, lexical
//! owner slots and the file table. Checks do not recharge syntax or latch errors; //! owner slots and the file table. Checks do not recharge syntax or latch errors;
//! every failure must return immediately. Tokens themselves use the token bound. //! every failure must return immediately. Tokens themselves use the token bound.
//! This deliberately bounded staging policy is not calibrated public source8. //! The public read checker selects both producers; editing remains separately staged.
use super::{ use super::{
budget::{Budget, Limits}, budget::{Budget, Limits},
@@ -53,11 +53,15 @@ pub(super) struct ParsedMixedSyntax<'s> {
struct FileSyntax<'s> { struct FileSyntax<'s> {
path: &'s str, path: &'s str,
source: &'s str, source: &'s str,
// Retained original indexing metadata; the read result preserves its accounted ownership.
#[cfg_attr(not(test), allow(dead_code))]
tokens: Vec<lex::Token>, tokens: Vec<lex::Token>,
ordinary: SourceUnit, ordinary: SourceUnit,
typed: Vec<declarations::Declaration>, typed: Vec<declarations::Declaration>,
positions: Vec<(Id, EditDeclaration)>, positions: Vec<(Id, EditDeclaration)>,
// A lexical owner is an occurrence index, not only a possibly repeated UUID. // A lexical owner is an occurrence index, not only a possibly repeated UUID.
// Retained original indexing metadata; the read result preserves its accounted ownership.
#[cfg_attr(not(test), allow(dead_code))]
lexical_owners: Vec<Option<usize>>, lexical_owners: Vec<Option<usize>>,
} }
@@ -256,7 +260,7 @@ impl State<'_> {
return Err(Error { return Err(Error {
code: "invalid_source", code: "invalid_source",
offset: declaration.span.start, offset: declaration.span.start,
message: "Private mixed syntax requires explicit IDs.".into(), message: "Mixed source8 requires explicit UUIDs.".into(),
}); });
} }
self.typed.push(declaration); self.typed.push(declaration);
@@ -276,17 +280,22 @@ pub(in crate::language) fn long_analysis_word(word: &str) -> bool {
) )
} }
#[cfg(test)]
pub(super) fn parse( pub(super) fn parse(
bundle: &SourceBundle, bundle: &SourceBundle,
limits: ProjectLimits, limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> { ) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let profile = Limits::source8(ParseLimits { let profile = source_profile(limits);
parse_with_profile(bundle, limits, profile)
}
fn source_profile(limits: ProjectLimits) -> Limits {
Limits::source8(ParseLimits {
// Storage is not the caller's final model record/ordered-step accounting. // Storage is not the caller's final model record/ordered-step accounting.
max_source_bytes: limits.model.max_source_bytes, max_source_bytes: limits.model.max_source_bytes,
max_step_references: limits.model.max_step_references, max_step_references: limits.model.max_step_references,
..ParseLimits::UNLIMITED ..ParseLimits::UNLIMITED
}); })
parse_with_profile(bundle, limits, profile)
} }
fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> { fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
@@ -297,6 +306,7 @@ fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
}] }]
} }
#[cfg(test)]
fn parse_with_profile( fn parse_with_profile(
bundle: &SourceBundle, bundle: &SourceBundle,
limits: ProjectLimits, limits: ProjectLimits,
@@ -2,6 +2,7 @@
//! 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};
#[cfg(test)]
use project::ordinary_index::rename::emission::PendingRename; use project::ordinary_index::rename::emission::PendingRename;
struct SnapshotPolicy { struct SnapshotPolicy {
@@ -10,17 +11,20 @@ struct SnapshotPolicy {
} }
/// The allocation-owning bundle guard. No production constructor exists yet. /// The allocation-owning bundle guard. No production constructor exists yet.
#[cfg(test)]
struct RetainedSnapshot { struct RetainedSnapshot {
bundle: SourceBundle, bundle: SourceBundle,
policy: SnapshotPolicy, policy: SnapshotPolicy,
retained: Slots, retained: Slots,
} }
enum SnapshotBacking<'s> { enum SnapshotBacking<'s> {
#[cfg(test)]
Pending(&'s PendingRename<'s>), Pending(&'s PendingRename<'s>),
Borrowed { Borrowed {
bundle: &'s SourceBundle, bundle: &'s SourceBundle,
policy: SnapshotPolicy, policy: SnapshotPolicy,
}, },
#[cfg(test)]
Retained(&'s RetainedSnapshot), Retained(&'s RetainedSnapshot),
} }
@@ -29,8 +33,12 @@ pub(super) struct CarriedInput<'s> {
backing: SnapshotBacking<'s>, backing: SnapshotBacking<'s>,
} }
enum GuardBacking<'s> { enum GuardBacking<'s> {
#[cfg(test)]
Pending(&'s PendingRename<'s>), Pending(&'s PendingRename<'s>),
Borrowed, Borrowed {
_bundle: &'s SourceBundle,
},
#[cfg(test)]
Retained(&'s RetainedSnapshot), Retained(&'s RetainedSnapshot),
} }
@@ -40,6 +48,18 @@ pub(super) struct CarriedGuard<'s> {
backing: GuardBacking<'s>, backing: GuardBacking<'s>,
} }
impl<'s> CarriedInput<'s> { impl<'s> CarriedInput<'s> {
fn fresh(bundle: &'s SourceBundle, limits: ProjectLimits) -> Self {
Self {
backing: SnapshotBacking::Borrowed {
bundle,
policy: SnapshotPolicy {
limits: source_profile(limits),
project_limits: limits,
},
},
}
}
#[cfg(test)]
pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self { pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self {
Self { Self {
backing: SnapshotBacking::Pending(pending), backing: SnapshotBacking::Pending(pending),
@@ -47,6 +67,7 @@ impl<'s> CarriedInput<'s> {
} }
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) { fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
match self.backing { match self.backing {
#[cfg(test)]
SnapshotBacking::Pending(pending) => { SnapshotBacking::Pending(pending) => {
let (limits, project_limits) = pending.carried_policy(); let (limits, project_limits) = pending.carried_policy();
( (
@@ -64,9 +85,10 @@ impl<'s> CarriedInput<'s> {
bundle, bundle,
policy, policy,
CarriedGuard { CarriedGuard {
backing: GuardBacking::Borrowed, backing: GuardBacking::Borrowed { _bundle: bundle },
}, },
), ),
#[cfg(test)]
SnapshotBacking::Retained(owner) => ( SnapshotBacking::Retained(owner) => (
&owner.bundle, &owner.bundle,
SnapshotPolicy { SnapshotPolicy {
@@ -83,8 +105,10 @@ 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 {
#[cfg(test)]
GuardBacking::Pending(pending) => pending.carried_slots(), GuardBacking::Pending(pending) => pending.carried_slots(),
GuardBacking::Borrowed => Slots::default(), GuardBacking::Borrowed { .. } => Slots::default(),
#[cfg(test)]
GuardBacking::Retained(owner) => owner.retained, GuardBacking::Retained(owner) => owner.retained,
} }
} }
@@ -105,6 +129,7 @@ impl CarriedGuard<'_> {
} }
/// Input already binds the actual bundle; callers cannot supply a replacement. /// Input already binds the actual bundle; callers cannot supply a replacement.
#[cfg(test)]
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( parse_impl(
@@ -205,3 +230,12 @@ mod text_producer;
#[cfg(test)] #[cfg(test)]
mod entry_producer; mod entry_producer;
pub(super) fn parse_fresh(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
parse_entry_producers(CarriedInput::fresh(bundle, limits))
}
#[cfg(test)]
mod public_read;
@@ -0,0 +1,65 @@
//! Real public-constructor wiring; arbitrary fixture profiles are not public policy.
use super::*;
#[test]
fn public_read_fresh_constructor_keeps_zero_carry_both_modes_and_exact_policy() {
let bundle = SourceBundle {
entry: "main.arcadia".into(),
files: [("main.arcadia".into(), "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n".into())].into(),
};
let original = bundle.clone();
for (source_bytes, references, expected_bytes, expected_tokens, expected_references) in [
(4096, 17, 4096, 4096, 17),
(usize::MAX, usize::MAX, 64 * 1024 * 1024, 1_048_576, 500_000),
] {
let limits = ProjectLimits {
max_files: 7,
max_include_depth: 9,
max_include_edges: 11,
model: ParseLimits {
max_source_bytes: source_bytes,
max_graph_objects: 3,
max_records: 1,
max_step_references: references,
},
};
// This is the same production helper the exported checker calls.
let syntax = parse_fresh(&bundle, limits).unwrap();
let guard = syntax.carried.as_ref().expect("explicit carried mode");
assert!(matches!(guard.backing, GuardBacking::Borrowed { .. }));
assert_eq!(guard.slots(), Slots::default());
assert_eq!(syntax.budget.producer_text(), Some((8, 0)));
assert_eq!(syntax.budget.producer_entries(), Some(11));
assert_eq!(syntax.metadata, 11);
let profile = syntax.budget.limits();
assert_eq!(profile.bytes, expected_bytes);
assert_eq!(profile.tokens, expected_tokens);
assert_eq!(profile.entries, 262_144);
assert_eq!(profile.references, expected_references);
assert_eq!(profile.frames, 256);
assert_eq!(profile.numeric_units, 50_000_000);
assert_eq!(syntax.limits.max_files, 7);
assert_eq!(syntax.limits.max_include_depth, 9);
assert_eq!(syntax.limits.max_include_edges, 11);
assert_eq!(syntax.limits.model.max_source_bytes, source_bytes);
assert_eq!(syntax.limits.model.max_graph_objects, 3);
assert_eq!(syntax.limits.model.max_records, 1);
assert_eq!(syntax.limits.model.max_step_references, references);
let prior = parse(&bundle, limits).unwrap();
assert_eq!(syntax.budget.usage, prior.budget.usage);
assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units());
let resolved = project::resolve(syntax).unwrap();
let prior = project::resolve(prior).unwrap();
let checked = project::check_mixed_bundle(&bundle, limits).unwrap();
let mut expected = crate::Project::empty();
expected.id = Id::from_u128(1);
expected.alias = "p".into();
expected.name = "Project".into();
assert_eq!(resolved.project(), &expected);
assert_eq!(prior.project(), &expected);
assert_eq!(checked.project(), &expected);
assert!(checked.warnings().is_empty());
assert_eq!(resolved.warnings(), prior.warnings());
}
assert_eq!(bundle, original);
}
@@ -1,5 +1,5 @@
//! Private complete-Project admission from the actual complete syntax witness. //! Complete-Project admission and opaque public read-only mixed source8 witness.
//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy. //! The separate typed index/edit chain remains test-only; no replacement policy.
use super::super::{ use super::super::{
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span, Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
declarations as d, error, lower, declarations as d, error, lower,
@@ -22,17 +22,108 @@ pub(super) fn text_producer_inventory(syntax: &ParsedMixedSyntax<'_>) -> Result<
accounting::syntax(syntax, &mut ledger)?; accounting::syntax(syntax, &mut ledger)?;
Ok(ledger.live) Ok(ledger.live)
} }
#[cfg(test)]
pub(in crate::language::analysis_source) mod ordinary_index; pub(in crate::language::analysis_source) mod ordinary_index;
/// An admitted mixed source8 model borrowing its complete original source bundle.
///
/// This non-Clone result grants immutable validation queries only. It is not an
/// editable source index, stabilized output, or permission to format/save a typed
/// model through the ordinary source APIs. All input UUIDs must be explicit.
///
/// The source cannot be mutated while a later query uses the result:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let mut bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// bundle.files.clear();
/// assert_eq!(checked.project().alias, "p");
/// ```
///
/// Nor can the result outlive that source:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let checked = {
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap()
/// };
/// assert_eq!(checked.project().alias, "p");
/// ```
pub struct CheckedMixed<'s> {
resolved: ResolvedMixedProject<'s>,
}
impl CheckedMixed<'_> {
/// The complete admitted model, without mutation or ownership extraction.
pub fn project(&self) -> &Project {
self.resolved.project()
}
/// Actual located non-error validation diagnostics in validation order.
pub fn warnings(&self) -> &[SourceDiagnostic] {
self.resolved.warnings()
}
}
/// Validate a complete mixed ordinary/typed source8 bundle without changing bytes.
///
/// Every project, ordinary/typed declaration and typed parameter requires an
/// explicit UUID. Supplied files must be canonical and reachable from the entry.
/// Errors return no partial admitted model. There is no migration, ID generation,
/// filesystem discovery, formatting or editable-index construction.
///
/// Caller policy is applied across the entire bundle with the existing bounded
/// source8 syntax/storage/numeric policy; this is not an all-heap memory bound.
/// Ordinary parse/compile/edit/save APIs and CLI discovery remain separate.
///
/// ```
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// assert_eq!(checked.project().alias, "p");
/// assert!(checked.warnings().is_empty());
/// ```
pub fn check_mixed_bundle(
bundle: &crate::source_project::SourceBundle,
limits: crate::source_project::ProjectLimits,
) -> Result<CheckedMixed<'_>, Vec<SourceDiagnostic>> {
let syntax = super::carried::parse_fresh(bundle, limits)?;
let resolved = resolve(syntax).map_err(|failure| failure.diagnostics)?;
Ok(CheckedMixed { resolved })
}
/// Read-only authority: every component originates in the consumed syntax. /// Read-only authority: every component originates in the consumed syntax.
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> { pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
syntax: ParsedMixedSyntax<'s>, syntax: ParsedMixedSyntax<'s>,
project: Project, project: Project,
warnings: Vec<SourceDiagnostic>, warnings: Vec<SourceDiagnostic>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_limits: analysis::AdmissionLimits, static_limits: analysis::AdmissionLimits,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_usage: analysis::AdmissionUsage, static_usage: analysis::AdmissionUsage,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
expressions: Vec<lower::ExpressionSite>, expressions: Vec<lower::ExpressionSite>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
occurrences: Vec<lower::Occurrence>, occurrences: Vec<lower::Occurrence>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
storage: Ledger, storage: Ledger,
} }
impl ResolvedMixedProject<'_> { impl ResolvedMixedProject<'_> {
@@ -51,10 +142,12 @@ impl ResolvedMixedProject<'_> {
/// Complete original bundle authority with a typed-only occurrence view. /// Complete original bundle authority with a typed-only occurrence view.
/// Ordinary globals are catalogued, but their authored occurrences are not indexed. /// Ordinary globals are catalogued, but their authored occurrences are not indexed.
/// This owner cannot be converted into the legacy typed-only edit authority. /// This owner cannot be converted into the legacy typed-only edit authority.
#[cfg(test)]
pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> { pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> {
resolved: ResolvedMixedProject<'s>, resolved: ResolvedMixedProject<'s>,
data: super::super::index::IndexData, data: super::super::index::IndexData,
} }
#[cfg(test)]
impl<'s> ResolvedMixedProject<'s> { impl<'s> ResolvedMixedProject<'s> {
pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> { pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> {
let input = IndexInput { let input = IndexInput {
@@ -72,6 +165,7 @@ impl<'s> ResolvedMixedProject<'s> {
}) })
} }
} }
#[cfg(test)]
impl IndexedMixedProject<'_> { impl IndexedMixedProject<'_> {
pub fn project(&self) -> &Project { pub fn project(&self) -> &Project {
self.resolved.project() self.resolved.project()
@@ -108,12 +202,14 @@ impl IndexedMixedProject<'_> {
/// Constructor-only borrowed adapter. Its private file table prevents fabrication; /// Constructor-only borrowed adapter. Its private file table prevents fabrication;
/// no API returns it and no alternative source/token table can be installed. /// no API returns it and no alternative source/token table can be installed.
#[cfg(test)]
pub(in crate::language::analysis_source) struct IndexInput<'a> { pub(in crate::language::analysis_source) struct IndexInput<'a> {
files: &'a [FileSyntax<'a>], files: &'a [FileSyntax<'a>],
pub(in crate::language::analysis_source) project: &'a Project, pub(in crate::language::analysis_source) project: &'a Project,
pub(in crate::language::analysis_source) expressions: &'a [lower::ExpressionSite], pub(in crate::language::analysis_source) expressions: &'a [lower::ExpressionSite],
pub(in crate::language::analysis_source) occurrences: &'a [lower::Occurrence], pub(in crate::language::analysis_source) occurrences: &'a [lower::Occurrence],
} }
#[cfg(test)]
impl IndexInput<'_> { impl IndexInput<'_> {
pub(in crate::language::analysis_source) fn declarations( pub(in crate::language::analysis_source) fn declarations(
&self, &self,
@@ -140,6 +236,7 @@ pub(in crate::language::analysis_source) struct Failure {
} }
impl Failure { impl Failure {
/// Canonical attribution when justified; otherwise a safe explicit start fallback. /// Canonical attribution when justified; otherwise a safe explicit start fallback.
#[cfg(test)]
fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self { fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self {
failure.file = failure.file.filter(|&f| f < syntax.files.len()); failure.file = failure.file.filter(|&f| f < syntax.files.len());
if failure.file.is_none_or(|file| { if failure.file.is_none_or(|file| {
+17
View File
@@ -633,6 +633,23 @@ a { text-decoration: none; }
<dd class="text-suffix"><p><span class="docutils literal">.arcadia</span></p> <dd class="text-suffix"><p><span class="docutils literal">.arcadia</span></p>
</dd> </dd>
</dl> </dl>
<section id="staged-source8-rust-read-api">
<h2>Staged source8 Rust read API</h2>
<p>The core library separately offers <span class="docutils literal"><span class="pre">language::check_mixed_bundle(&amp;SourceBundle,</span> ProjectLimits)</span> for complete ordinary/typed source8 validation. Its opaque
<span class="docutils literal">CheckedMixed</span> result borrows the unchanged input and exposes only <span class="docutils literal">project()</span>
and <span class="docutils literal">warnings()</span>. Explicit UUIDs are required for the project, every ordinary
and typed declaration, and typed parameters; no IDs are generated. All supplied
canonical files must be reachable from the entry. Errors return no admitted
model; success preserves actual located warnings and the complete model.</p>
<p>This bounded API selects both retained text and entry producer checks and carries
the caller's whole-project policy through admission. It does not promise an
all-heap memory bound. It performs no filesystem discovery, migration, formatting,
source stabilization, indexing, saving or history installation. Existing ordinary
parse/compile/update APIs, CLI commands, desktop and server flows remain separate;
a read result does not grant their typed editing or save authority. Generic and
typed depth policies and platform qualification limits remain unchanged. This
staged capability does not replace the broader conformance obligations below.</p>
</section>
<section id="scope-and-conformance"> <section id="scope-and-conformance">
<h2>1. Scope and conformance</h2> <h2>1. Scope and conformance</h2>
<p>This specification defines the shared Operational Analysis (OA) and System <p>This specification defines the shared Operational Analysis (OA) and System
+20
View File
@@ -6,6 +6,26 @@ Arcadia Studio OA / SA Language and Editing Contract
:Core implementation: ``crates/oa-core`` :Core implementation: ``crates/oa-core``
:Text suffix: ``.arcadia`` :Text suffix: ``.arcadia``
Staged source8 Rust read API
----------------------------------------
The core library separately offers ``language::check_mixed_bundle(&SourceBundle,
ProjectLimits)`` for complete ordinary/typed source8 validation. Its opaque
``CheckedMixed`` result borrows the unchanged input and exposes only ``project()``
and ``warnings()``. Explicit UUIDs are required for the project, every ordinary
and typed declaration, and typed parameters; no IDs are generated. All supplied
canonical files must be reachable from the entry. Errors return no admitted
model; success preserves actual located warnings and the complete model.
This bounded API selects both retained text and entry producer checks and carries
the caller's whole-project policy through admission. It does not promise an
all-heap memory bound. It performs no filesystem discovery, migration, formatting,
source stabilization, indexing, saving or history installation. Existing ordinary
parse/compile/update APIs, CLI commands, desktop and server flows remain separate;
a read result does not grant their typed editing or save authority. Generic and
typed depth policies and platform qualification limits remain unchanged. This
staged capability does not replace the broader conformance obligations below.
1. Scope and conformance 1. Scope and conformance
------------------------ ------------------------
@@ -0,0 +1,43 @@
# Public mixed read implementation
Base7b06b8ea, selected root contract bcddaba3. Public
language::check_mixed_bundle returns opaque non-Clone CheckedMixed borrowing the
complete original SourceBundle, with only immutable project/warnings accessors.
Its private resolved field retains syntax, expression/occurrence vectors, ledger
and admission evidence; there is no index, output, save or extraction authority.
The shared parser mixed hooks now compile in production. Ordinary callers still
select None. The fresh constructor binds the actual bundle and exact ProjectLimits,
derives existing internal policy once, selects Some(Borrowed) with zero carry and
both producers, then consumes syntax through complete resolution/admission.
The borrowed guard retains the actual source reference even when test-only
Pending/Retained variants are absent. No duplicate live policy/model is added.
Public schema/UUID requirement wording replaces private-staging wording only;
all limiter phase/category/span transport remains inherited.
Private legacy index/update/write, ordinary-index/rename/emission/acceptance and
fixture/pending constructors remain test-only. Shared parse/lower code remains
private. Necessary retained fields have individual documented non-test dead-code
allowances; no module-wide suppression, fake read, or new public lint-only API.
Exact production compiler lint findings remain a parent gate, not inferred clean
from source inspection. Existing298 tests are not edited.
One internal public_read test invokes the actual production parse helper and
observes borrowedC0, both producer inventories, every inherited/derived policy
field (including262144 entries independent of model records1), full empty-Project
and warning parity, Usage/numeric parity and unchanged input. Three rustdoc blocks
provide a compiling real usage example plus source-drop and source-mutation
compile-fail cases with later result use. Independent worker owns the non-test
mixed_source8_read integration target and its separate complete-model oracles.
Established docs/language.rst gains a bounded staged-read note without changing
its broader conformance/version obligations. Existing docutils HTML5 generation
with halt/report level2 regenerated language.html successfully. An initial local
RST generation refused the new section formatting; the section separator and
blank line were corrected before successful regeneration. No Rust compiler,
Cargo test, CLI, debugger or publication was run by this worker.
Parent owns pinned compilation, all old/new runtime tests, doctests, strict
Clippy, formatting disposition and production Parser stack regression checks.
DEPTH_128_VALIDATED=false, existing depth policies, logical-accounting exclusions,
full-pipeline limitation and deferred Linux qualification remain unchanged.