Compare commits

...
14 changed files with 1147 additions and 45 deletions
+4 -31
View File
@@ -3,10 +3,9 @@ mod raw_tokens;
#[path = "language/semantic_source.rs"]
mod semantic_source;
mod source3_bridge;
// Staged typed analysis only. Public mixed source8 admission waits for complete
// declaration lowering, source updates and the root-owned resource gate.
#[cfg(test)]
// Mixed source8 is exposed only through the separate borrowed read-only checker.
mod analysis_source;
pub use analysis_source::{CheckedMixed, check_mixed_bundle};
#[path = "migration_v2/source.rs"]
mod v2_source;
use crate::*;
@@ -480,7 +479,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
cursor: 0,
value_depth: 0,
limits,
#[cfg(test)]
mixed: None,
}
.document()
@@ -916,7 +914,6 @@ pub(crate) fn parse_unit(
cursor: 0,
value_depth: 0,
limits,
#[cfg(test)]
mixed: None,
}
.unit(entry, true)
@@ -1215,12 +1212,10 @@ struct Parser<'a> {
cursor: usize,
value_depth: usize,
limits: ParseLimits,
#[cfg(test)]
mixed: Option<analysis_source::mixed::State<'a>>,
}
impl Parser<'_> {
fn token(&self, index: usize) -> Option<Lexeme> {
#[cfg(test)]
if let Some(mixed) = &self.mixed {
return mixed.token(index);
}
@@ -1236,7 +1231,6 @@ impl Parser<'_> {
})
}
fn syntax_entry(&mut self) -> Result<(), Error> {
#[cfg(test)]
{
let span = self.current().span;
if let Some(mixed) = &mut self.mixed {
@@ -1266,9 +1260,7 @@ impl Parser<'_> {
if self.current().class != TokenClass::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)?;
@@ -1290,9 +1282,8 @@ impl Parser<'_> {
}
fn optional_id(&mut self) -> Result<Id, Error> {
if self.text() != "id" {
#[cfg(test)]
if self.mixed.is_some() {
return Err(self.error("Private mixed syntax requires explicit IDs."));
return Err(self.error("Mixed source8 requires explicit UUIDs."));
}
return Ok(Id::new_v4());
}
@@ -1321,21 +1312,15 @@ impl Parser<'_> {
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> {
self.take("{")?;
@@ -1383,7 +1368,6 @@ impl Parser<'_> {
.span
.end;
self.take(";")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.projection(1, Span { start, end })?;
}
@@ -1412,7 +1396,6 @@ impl Parser<'_> {
self.value_depth += 1;
let result = self.value_inner();
self.value_depth -= 1;
#[cfg(test)]
if let Ok(ast) = &result {
let end = self
.token(self.cursor - 1)
@@ -1433,7 +1416,6 @@ impl Parser<'_> {
fn value_inner(&mut self) -> Result<Ast, Error> {
let token = self.current();
let offset = token.span.start;
#[cfg(test)]
if let Some(mixed) = &self.mixed
&& (token.class == TokenClass::Number || matches!(self.text(), "-" | "+"))
{
@@ -1541,17 +1523,14 @@ impl Parser<'_> {
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
let mut header = if entry {
self.take("arcadia")?;
#[cfg(test)]
let mixed_mode = self.mixed.is_some();
#[cfg(not(test))]
let mixed_mode = false;
if if mixed_mode {
self.text() != "8"
} else {
!matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8")
} {
return Err(self.error(if mixed_mode {
"Private mixed syntax requires arcadia 8."
"Mixed source requires arcadia 8."
} else {
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
}));
@@ -1561,7 +1540,6 @@ impl Parser<'_> {
self.take(";")?;
let start = self.current().span.start;
self.take("project")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.record(false, Span { start, end: start })?;
}
@@ -1606,7 +1584,6 @@ impl Parser<'_> {
let mut open: Vec<usize> = Vec::new();
loop {
let offset = self.current().span.start;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed
&& mixed.typed_header(self.cursor, open.is_empty())
{
@@ -1658,7 +1635,6 @@ impl Parser<'_> {
if !includes_allowed {
return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
}
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.include(Span {
start: offset,
@@ -1705,7 +1681,6 @@ impl Parser<'_> {
header = Some(entry_header);
continue;
}
#[cfg(test)]
if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) {
return Err(self.error("Use the short source8 analysis declaration word."));
}
@@ -1713,7 +1688,6 @@ impl Parser<'_> {
let word = self.current().text(self.source);
let kind = declaration_kind(word)
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.record(
matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)),
@@ -1750,7 +1724,6 @@ impl Parser<'_> {
let body_start = self.current().span.end;
self.take("{")?;
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
let span = Span {
start: offset,
@@ -1,14 +1,16 @@
//! Unresolved typed source8 syntax, staged for the complete source adapter.
//! This module does not admit a Project or enable public source8.
//! Private typed syntax and complete read-only mixed source8 resolution.
mod budget;
mod cursor;
mod declarations;
#[cfg(test)]
mod index;
mod lex;
mod lower;
pub(super) mod mixed;
mod syntax;
#[cfg(test)]
mod update;
#[cfg(test)]
mod write;
#[cfg(test)]
@@ -18,6 +20,7 @@ use super::Span;
use crate::analysis::{BinaryOp, UnaryOp};
use crate::analysis_numeric::Exact;
use budget::Budget;
pub use mixed::project::{CheckedMixed, check_mixed_bundle};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
@@ -104,6 +107,7 @@ pub enum Binding {
Expression(Expression),
}
#[cfg(test)]
pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
let tokens = lex::tokens(source, budget)?;
let mut cursor = cursor::Cursor::new(source, &tokens, budget);
@@ -221,6 +221,7 @@ impl Budget {
Ok(value)
}
/// Immutable exact policy snapshot for private deliberate-snapshot continuation.
#[cfg(test)]
pub(super) fn limits(&self) -> Limits {
self.limits
}
@@ -294,6 +295,7 @@ impl Budget {
}
}
}
#[cfg(test)]
pub fn numeric_units(&self) -> u64 {
self.numeric.usage().numeric_units
}
@@ -42,6 +42,7 @@ impl Cursor<'_, '_> {
self.cursor += 1;
Ok(span)
}
#[cfg(test)]
pub(super) fn end(&self) -> Result<(), Error> {
if self.cursor != self.tokens.len() {
Err(error(
@@ -2,12 +2,15 @@
//! single complete-Project semantic admission after merging these additions.
use super::declarations::{self as d, Declaration, DeclarationKind};
use super::*;
#[cfg(test)]
use crate::Project;
use crate::analysis as a;
use crate::analysis_numeric::Dimension;
use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody};
use crate::{Element, ElementBody, Id, Kind, Layer, SharedBody};
use std::collections::{BTreeMap, BTreeSet};
/// Caller-assigned file key; spans are original offsets within that file.
#[cfg(test)]
pub(super) struct Unit {
pub file: usize,
pub declarations: Vec<Declaration>,
@@ -180,6 +183,7 @@ impl Tables {
Ok(())
}
#[cfg(test)]
fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> {
let at = Span::default();
self.insert(project.id, &project.alias, Category::Project, at, budget)?;
@@ -270,6 +274,7 @@ impl Tables {
/// Inputs must come from the same live Budget and stabilization pass. No scan,
/// exact conversion or reference charge occurs here. The ordinary skeleton is
/// not semantically admitted: its typed forward references may need additions.
#[cfg(test)]
pub(super) fn lower(
ordinary: &Project,
units: &[Unit],
@@ -13,7 +13,7 @@
//! value-end entries, projected value/field/header/declaration positions, lexical
//! owner slots and the file table. Checks do not recharge syntax or latch errors;
//! every failure must return immediately. Tokens themselves use the token bound.
//! This deliberately bounded staging policy is not calibrated public source8.
//! The public read checker selects both producers; editing remains separately staged.
use super::{
budget::{Budget, Limits},
@@ -53,11 +53,15 @@ pub(super) struct ParsedMixedSyntax<'s> {
struct FileSyntax<'s> {
path: &'s str,
source: &'s str,
// Retained original indexing metadata; the read result preserves its accounted ownership.
#[cfg_attr(not(test), allow(dead_code))]
tokens: Vec<lex::Token>,
ordinary: SourceUnit,
typed: Vec<declarations::Declaration>,
positions: Vec<(Id, EditDeclaration)>,
// A lexical owner is an occurrence index, not only a possibly repeated UUID.
// Retained original indexing metadata; the read result preserves its accounted ownership.
#[cfg_attr(not(test), allow(dead_code))]
lexical_owners: Vec<Option<usize>>,
}
@@ -256,7 +260,7 @@ impl State<'_> {
return Err(Error {
code: "invalid_source",
offset: declaration.span.start,
message: "Private mixed syntax requires explicit IDs.".into(),
message: "Mixed source8 requires explicit UUIDs.".into(),
});
}
self.typed.push(declaration);
@@ -276,17 +280,22 @@ pub(in crate::language) fn long_analysis_word(word: &str) -> bool {
)
}
#[cfg(test)]
pub(super) fn parse(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let profile = Limits::source8(ParseLimits {
let profile = source_profile(limits);
parse_with_profile(bundle, limits, profile)
}
fn source_profile(limits: ProjectLimits) -> Limits {
Limits::source8(ParseLimits {
// Storage is not the caller's final model record/ordered-step accounting.
max_source_bytes: limits.model.max_source_bytes,
max_step_references: limits.model.max_step_references,
..ParseLimits::UNLIMITED
});
parse_with_profile(bundle, limits, profile)
})
}
fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
@@ -297,6 +306,7 @@ fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
}]
}
#[cfg(test)]
fn parse_with_profile(
bundle: &SourceBundle,
limits: ProjectLimits,
@@ -2,6 +2,7 @@
//! Checks existing logical phase totals; does not meter every parser allocation.
use super::*;
use project::accounting::{Ledger, Slots};
#[cfg(test)]
use project::ordinary_index::rename::emission::PendingRename;
struct SnapshotPolicy {
@@ -10,17 +11,20 @@ struct SnapshotPolicy {
}
/// The allocation-owning bundle guard. No production constructor exists yet.
#[cfg(test)]
struct RetainedSnapshot {
bundle: SourceBundle,
policy: SnapshotPolicy,
retained: Slots,
}
enum SnapshotBacking<'s> {
#[cfg(test)]
Pending(&'s PendingRename<'s>),
Borrowed {
bundle: &'s SourceBundle,
policy: SnapshotPolicy,
},
#[cfg(test)]
Retained(&'s RetainedSnapshot),
}
@@ -29,8 +33,12 @@ pub(super) struct CarriedInput<'s> {
backing: SnapshotBacking<'s>,
}
enum GuardBacking<'s> {
#[cfg(test)]
Pending(&'s PendingRename<'s>),
Borrowed,
Borrowed {
_bundle: &'s SourceBundle,
},
#[cfg(test)]
Retained(&'s RetainedSnapshot),
}
@@ -40,6 +48,18 @@ pub(super) struct CarriedGuard<'s> {
backing: GuardBacking<'s>,
}
impl<'s> CarriedInput<'s> {
fn fresh(bundle: &'s SourceBundle, limits: ProjectLimits) -> Self {
Self {
backing: SnapshotBacking::Borrowed {
bundle,
policy: SnapshotPolicy {
limits: source_profile(limits),
project_limits: limits,
},
},
}
}
#[cfg(test)]
pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self {
Self {
backing: SnapshotBacking::Pending(pending),
@@ -47,6 +67,7 @@ impl<'s> CarriedInput<'s> {
}
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
match self.backing {
#[cfg(test)]
SnapshotBacking::Pending(pending) => {
let (limits, project_limits) = pending.carried_policy();
(
@@ -64,9 +85,10 @@ impl<'s> CarriedInput<'s> {
bundle,
policy,
CarriedGuard {
backing: GuardBacking::Borrowed,
backing: GuardBacking::Borrowed { _bundle: bundle },
},
),
#[cfg(test)]
SnapshotBacking::Retained(owner) => (
&owner.bundle,
SnapshotPolicy {
@@ -83,8 +105,10 @@ impl<'s> CarriedInput<'s> {
impl CarriedGuard<'_> {
pub(super) fn slots(&self) -> Slots {
match &self.backing {
#[cfg(test)]
GuardBacking::Pending(pending) => pending.carried_slots(),
GuardBacking::Borrowed => Slots::default(),
GuardBacking::Borrowed { .. } => Slots::default(),
#[cfg(test)]
GuardBacking::Retained(owner) => owner.retained,
}
}
@@ -105,6 +129,7 @@ impl CarriedGuard<'_> {
}
/// Input already binds the actual bundle; callers cannot supply a replacement.
#[cfg(test)]
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let (bundle, policy, guard) = input.into_parts();
parse_impl(
@@ -205,3 +230,12 @@ mod text_producer;
#[cfg(test)]
mod entry_producer;
pub(super) fn parse_fresh(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
parse_entry_producers(CarriedInput::fresh(bundle, limits))
}
#[cfg(test)]
mod public_read;
@@ -0,0 +1,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.
//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy.
//! Complete-Project admission and opaque public read-only mixed source8 witness.
//! The separate typed index/edit chain remains test-only; no replacement policy.
use super::super::{
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
declarations as d, error, lower,
@@ -22,17 +22,108 @@ pub(super) fn text_producer_inventory(syntax: &ParsedMixedSyntax<'_>) -> Result<
accounting::syntax(syntax, &mut ledger)?;
Ok(ledger.live)
}
#[cfg(test)]
pub(in crate::language::analysis_source) mod ordinary_index;
/// An admitted mixed source8 model borrowing its complete original source bundle.
///
/// This non-Clone result grants immutable validation queries only. It is not an
/// editable source index, stabilized output, or permission to format/save a typed
/// model through the ordinary source APIs. All input UUIDs must be explicit.
///
/// The source cannot be mutated while a later query uses the result:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let mut bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// bundle.files.clear();
/// assert_eq!(checked.project().alias, "p");
/// ```
///
/// Nor can the result outlive that source:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let checked = {
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap()
/// };
/// assert_eq!(checked.project().alias, "p");
/// ```
pub struct CheckedMixed<'s> {
resolved: ResolvedMixedProject<'s>,
}
impl CheckedMixed<'_> {
/// The complete admitted model, without mutation or ownership extraction.
pub fn project(&self) -> &Project {
self.resolved.project()
}
/// Actual located non-error validation diagnostics in validation order.
pub fn warnings(&self) -> &[SourceDiagnostic] {
self.resolved.warnings()
}
}
/// Validate a complete mixed ordinary/typed source8 bundle without changing bytes.
///
/// Every project, ordinary/typed declaration and typed parameter requires an
/// explicit UUID. Supplied files must be canonical and reachable from the entry.
/// Errors return no partial admitted model. There is no migration, ID generation,
/// filesystem discovery, formatting or editable-index construction.
///
/// Caller policy is applied across the entire bundle with the existing bounded
/// source8 syntax/storage/numeric policy; this is not an all-heap memory bound.
/// Ordinary parse/compile/edit/save APIs and CLI discovery remain separate.
///
/// ```
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// assert_eq!(checked.project().alias, "p");
/// assert!(checked.warnings().is_empty());
/// ```
pub fn check_mixed_bundle(
bundle: &crate::source_project::SourceBundle,
limits: crate::source_project::ProjectLimits,
) -> Result<CheckedMixed<'_>, Vec<SourceDiagnostic>> {
let syntax = super::carried::parse_fresh(bundle, limits)?;
let resolved = resolve(syntax).map_err(|failure| failure.diagnostics)?;
Ok(CheckedMixed { resolved })
}
/// Read-only authority: every component originates in the consumed syntax.
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
syntax: ParsedMixedSyntax<'s>,
project: Project,
warnings: Vec<SourceDiagnostic>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_limits: analysis::AdmissionLimits,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_usage: analysis::AdmissionUsage,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
expressions: Vec<lower::ExpressionSite>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
occurrences: Vec<lower::Occurrence>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
storage: Ledger,
}
impl ResolvedMixedProject<'_> {
@@ -51,10 +142,12 @@ impl ResolvedMixedProject<'_> {
/// Complete original bundle authority with a typed-only occurrence view.
/// Ordinary globals are catalogued, but their authored occurrences are not indexed.
/// This owner cannot be converted into the legacy typed-only edit authority.
#[cfg(test)]
pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> {
resolved: ResolvedMixedProject<'s>,
data: super::super::index::IndexData,
}
#[cfg(test)]
impl<'s> ResolvedMixedProject<'s> {
pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> {
let input = IndexInput {
@@ -72,6 +165,7 @@ impl<'s> ResolvedMixedProject<'s> {
})
}
}
#[cfg(test)]
impl IndexedMixedProject<'_> {
pub fn project(&self) -> &Project {
self.resolved.project()
@@ -108,12 +202,14 @@ impl IndexedMixedProject<'_> {
/// Constructor-only borrowed adapter. Its private file table prevents fabrication;
/// no API returns it and no alternative source/token table can be installed.
#[cfg(test)]
pub(in crate::language::analysis_source) struct IndexInput<'a> {
files: &'a [FileSyntax<'a>],
pub(in crate::language::analysis_source) project: &'a Project,
pub(in crate::language::analysis_source) expressions: &'a [lower::ExpressionSite],
pub(in crate::language::analysis_source) occurrences: &'a [lower::Occurrence],
}
#[cfg(test)]
impl IndexInput<'_> {
pub(in crate::language::analysis_source) fn declarations(
&self,
@@ -140,6 +236,7 @@ pub(in crate::language::analysis_source) struct Failure {
}
impl Failure {
/// Canonical attribution when justified; otherwise a safe explicit start fallback.
#[cfg(test)]
fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self {
failure.file = failure.file.filter(|&f| f < syntax.files.len());
if failure.file.is_none_or(|file| {
@@ -194,7 +194,6 @@ pub(super) fn row_key(source: &str, origins: &BTreeMap<Id, Id>) -> Option<String
cursor: 0,
value_depth: 0,
limits: ParseLimits::UNLIMITED,
#[cfg(test)]
mixed: None,
};
let ast = parser.value().ok()?;
+812
View File
@@ -0,0 +1,812 @@
//! External consumer: the linked oa-core library is not built with cfg(test).
//! Expected models use authored public values, never a parser or formatter oracle.
use oa_core::analysis as a;
use oa_core::analysis_numeric::Exact;
use oa_core::language::{ParseLimits, Span};
use oa_core::source_project::{ProjectLimits, SourceBundle, SourceDiagnostic};
pub use oa_core::*;
use std::collections::BTreeMap;
// This existing helper constructs independent public model values only. It does
// not import private parser/index/writer code or enable cfg(test) in oa-core.
#[path = "../src/language/analysis_source/model_oracle.rs"]
mod independent_model;
fn id(n: u128) -> Id {
Id::from_u128(n)
}
fn empty_project() -> Project {
let mut p = Project::empty();
p.id = id(1);
p.alias = "p".into();
p.name = "Project".into();
p
}
fn document(body: &str) -> String {
format!(
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
id(1)
)
}
fn single(body: &str) -> SourceBundle {
SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([("main.arcadia".into(), document(body))]),
}
}
fn succeeds(
bundle: &SourceBundle,
limits: ProjectLimits,
expected: &Project,
warnings: &[SourceDiagnostic],
) {
let original = bundle.clone();
let checked = language::check_mixed_bundle(bundle, limits)
.unwrap_or_else(|errors| panic!("expected complete read: {errors:#?}"));
assert_eq!(checked.project(), expected);
assert_eq!(checked.warnings(), warnings);
assert_eq!(bundle, &original, "read success preserves every input byte");
drop(checked);
assert_eq!(bundle, &original);
}
fn fails(bundle: &SourceBundle, limits: ProjectLimits) -> Vec<SourceDiagnostic> {
let original = bundle.clone();
let errors = match language::check_mixed_bundle(bundle, limits) {
Ok(_) => panic!("invalid input returned a successful read witness"),
Err(errors) => errors,
};
assert!(!errors.is_empty());
assert_eq!(bundle, &original, "read refusal preserves every input byte");
errors
}
fn authored_span(source: &str, literal: &str) -> Span {
assert_eq!(
source.matches(literal).count(),
1,
"ambiguous authored span"
);
let start = source.find(literal).unwrap();
Span {
start,
end: start + literal.len(),
}
}
fn error_at(
bundle: &SourceBundle,
errors: &[SourceDiagnostic],
path: &str,
span: Span,
code: &str,
message: &str,
) {
assert_eq!(errors.len(), 1, "{errors:#?}");
let error = &errors[0];
assert_eq!(error.path, path);
assert_eq!(error.diagnostic.severity, validation::Severity::Error);
assert_eq!(error.diagnostic.code, code);
assert_eq!(error.diagnostic.message, message);
assert_eq!(error.diagnostic.span, span);
let source = bundle.files.get(path).map_or("", String::as_str);
let prefix = &source[..span.start];
assert_eq!(
error.diagnostic.line,
prefix.bytes().filter(|b| *b == b'\n').count() + 1
);
assert_eq!(
error.diagnostic.column,
prefix.rsplit('\n').next().unwrap().chars().count() + 1
);
}
fn rich(layer: Layer, crlf: bool) -> (SourceBundle, Project, Vec<SourceDiagnostic>) {
let (ordinary, typed, work) = match layer {
Layer::Oa => (
include_str!("../src/language/analysis_source/lower/fixtures/ordinary-oa.arcadia"),
include_str!("../src/language/analysis_source/lower/fixtures/typed-oa.arcadia"),
"oa_activity",
),
Layer::Sa => (
include_str!("../src/language/analysis_source/lower/fixtures/ordinary-sa.arcadia"),
include_str!("../src/language/analysis_source/lower/fixtures/typed-sa.arcadia"),
"sa_function",
),
};
let mut expected = independent_model::project(layer);
let mut main = ordinary
.replace("\r\n", "\n")
.replacen("arcadia 7;", "arcadia 8;", 1);
assert!(main.starts_with("arcadia 8;"));
// Forward typed uses refer back into nested ordinary Work in this file.
main = main.replacen("{\n", "{\n include \"typed.arcadia\";\n", 1);
if layer == Layer::Sa {
let system = Element {
id: id(999),
alias: "system".into(),
name: "System".into(),
description: String::new(),
parent: None,
body: ElementBody::Sa(SaBody::System),
};
let at = main.rfind('}').unwrap();
main.insert_str(
at,
&format!(
" sa_system system \"System\" id \"{}\" {{}}\n",
system.id
),
);
assert!(expected.elements.insert(system.id, system).is_none());
}
let mut typed = typed.replace("\r\n", "\n");
// Textual argument order is independent of the model's parameter-ID map.
assert!(typed.contains("@combine(a: @receive_time, b: @dispatch_time)"));
typed = typed.replace(
"@combine(a: @receive_time, b: @dispatch_time)",
"@combine(b: @dispatch_time, a: @receive_time)",
);
if crlf {
main = format!("// 港🌊\r\n{}", main.replace('\n', "\r\n"));
typed = typed.replace('\n', "\r\n");
}
let bundle = SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([
("main.arcadia".into(), main),
("typed.arcadia".into(), typed),
]),
};
// Two nested leaves, so four warnings, unlike the earlier one-Work fixture.
// Existing validation orders both allocation warnings before coverage warnings.
let mut warnings = Vec::new();
for allocation in [true, false] {
for (alias, display) in [("receive", "Receive"), ("dispatch", "Dispatch")] {
let element = expected
.elements
.values()
.find(|e| e.alias == alias)
.unwrap();
let literal = format!("{work} {alias} \"{display}\" id \"{}\" {{}}", element.id);
let source = &bundle.files["main.arcadia"];
let span = authored_span(source, &literal);
let prefix = &source[..span.start];
warnings.push(SourceDiagnostic {
path: "main.arcadia".into(), related: Vec::new(),
diagnostic: language::Diagnostic {
severity: validation::Severity::Warning,
code: if allocation { "unallocated_leaf" } else { "work_without_behavior_context" }.into(),
object_id: Some(element.id),
field: allocation.then(|| "allocation".into()),
message: if allocation { format!("Allocate leaf '{display}' to a performer.") } else {
"Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.".into()
},
span, line: prefix.bytes().filter(|b| *b == b'\n').count() + 1,
column: prefix.rsplit('\n').next().unwrap().chars().count() + 1,
},
});
}
}
(bundle, expected, warnings)
}
#[test]
fn public_mixed_read_complete_oa_sa_models_and_all_located_warnings() {
for layer in [Layer::Oa, Layer::Sa] {
for crlf in [false, true] {
let (bundle, expected, warnings) = rich(layer, crlf);
assert_eq!(expected.definitions.len(), 4);
assert_eq!(warnings.len(), 4);
succeeds(&bundle, ProjectLimits::default(), &expected, &warnings);
}
}
}
fn calc(alias: &str, number: u128, body: &str) -> String {
format!(
"calculation {alias} \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body {body}; }}",
id(number),
id(1)
)
}
fn calculation_model(alias: &str, number: u128, body: a::Expr) -> a::Definition {
a::Definition {
id: id(number),
alias: alias.into(),
name: "Calculation".into(),
description: String::new(),
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
parameters: vec![a::Parameter {
id: a::ParameterId(id(1)),
name: "x".into(),
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
}],
result: a::AnalysisType::Scalar(a::ScalarType::Number),
body: a::Binding::Expression(body),
}),
}
}
#[test]
fn public_mixed_read_scoped_uuid_reuse_and_cross_file_call_are_not_global_collisions() {
let mut bundle = single("include \"f.arcadia\"; include \"g.arcadia\";");
bundle
.files
.insert("f.arcadia".into(), calc("f", 2, "@g(x: $x)"));
bundle.files.insert("g.arcadia".into(), calc("g", 3, "$x"));
let mut expected = empty_project();
expected.definitions.insert(
id(2),
calculation_model(
"f",
2,
a::Expr::Call {
definition: a::DefinitionRef::Local(id(3)),
arguments: BTreeMap::from([(
a::ParameterId(id(1)),
a::Expr::Parameter(a::ParameterId(id(1))),
)]),
},
),
);
expected.definitions.insert(
id(3),
calculation_model("g", 3, a::Expr::Parameter(a::ParameterId(id(1)))),
);
succeeds(&bundle, ProjectLimits::default(), &expected, &[]);
}
#[test]
fn public_mixed_read_explicit_identity_classes_versions_and_long_forms() {
let empty = single("");
succeeds(&empty, ProjectLimits::default(), &empty_project(), &[]);
let ordinary = format!("oa_entity e \"Entity\" id \"{}\" {{}}", id(2));
let typed = calc("f", 3, "$x");
let valid = single(&format!("{ordinary}\n{typed}"));
assert!(language::check_mixed_bundle(&valid, ProjectLimits::default()).is_ok());
// Each deletion starts from the same valid complete input.
for (literal, expected_start) in [
(format!("id \"{}\"", id(1)), None),
(format!("id \"{}\"", id(2)), Some("oa_entity")),
(format!("id \"{}\"", id(3)), Some("calculation")),
] {
let mut missing = valid.clone();
let text = missing.files.get_mut("main.arcadia").unwrap();
*text = text.replacen(&literal, "", 1);
let start = match expected_start {
None => text.find('{').unwrap(),
Some("oa_entity") => {
text.find("oa_entity").unwrap()
+ text[text.find("oa_entity").unwrap()..].find('{').unwrap()
}
Some(word) => text.find(word).unwrap(),
};
let errors = fails(&missing, ProjectLimits::default());
error_at(
&missing,
&errors,
"main.arcadia",
Span {
start,
end: start + 1,
},
"invalid_source",
"Mixed source8 requires explicit UUIDs.",
);
}
let mut parameter = valid.clone();
let text = parameter.files.get_mut("main.arcadia").unwrap();
*text = text.replace(&format!("input x id \"{}\"", id(1)), "input x");
let start = text.find("calculation").unwrap();
error_at(
&parameter,
&fails(&parameter, ProjectLimits::default()),
"main.arcadia",
Span {
start,
end: start + 1,
},
"invalid_source",
"Mixed source8 requires explicit UUIDs.",
);
for version in ["3", "7", "9"] {
let mut bad = empty.clone();
*bad.files.get_mut("main.arcadia").unwrap() =
document("").replacen("arcadia 8", &format!("arcadia {version}"), 1);
error_at(
&bad,
&fails(&bad, ProjectLimits::default()),
"main.arcadia",
Span { start: 8, end: 9 },
"invalid_source",
"Mixed source requires arcadia 8.",
);
}
for word in [
"oa_engineering_property",
"sa_engineering_property",
"oa_analysis_check",
"sa_analysis_check",
"oa_analysis_case",
"sa_analysis_case",
] {
let bad = single(&format!("{word} v \"Value\" id \"{}\" {{}}", id(2)));
let errors = fails(&bad, ProjectLimits::default());
let start = bad.files["main.arcadia"].find(word).unwrap();
assert_eq!(errors[0].path, "main.arcadia");
assert_eq!(errors[0].diagnostic.code, "invalid_source");
assert_eq!(
errors[0].diagnostic.span,
Span {
start,
end: start + 1
}
);
assert!(
errors[0].diagnostic.message.contains("short"),
"{errors:#?}"
);
}
}
fn chain() -> SourceBundle {
let mut bundle = single("include \"part.arcadia\";");
bundle.files.insert(
"part.arcadia".into(),
"include \"last.arcadia\";\r\n".into(),
);
bundle.files.insert("last.arcadia".into(), "//é\r\n".into());
bundle
}
#[test]
fn public_mixed_read_exact_input_file_edge_and_depth_boundaries() {
let empty = single("");
assert_eq!(empty.files["main.arcadia"].len(), 78);
for cap in [78, 77] {
let limits = ProjectLimits {
model: ParseLimits {
max_source_bytes: cap,
..ParseLimits::UNLIMITED
},
..ProjectLimits::default()
};
if cap == 78 {
succeeds(&empty, limits, &empty_project(), &[]);
} else {
error_at(
&empty,
&fails(&empty, limits),
"main.arcadia",
Span { start: 0, end: 1 },
"invalid_source",
"Source8 index/output byte limit exceeded.",
);
}
}
let bundle = chain();
assert_eq!(bundle.files.values().map(String::len).sum::<usize>(), 132);
let fits = ProjectLimits {
max_files: 3,
max_include_edges: 2,
max_include_depth: 2,
model: ParseLimits {
max_source_bytes: 132,
..ParseLimits::UNLIMITED
},
};
succeeds(&bundle, fits, &empty_project(), &[]);
for (limits, path, span, code, message) in [
(
ProjectLimits {
max_files: 2,
..fits
},
"main.arcadia",
Span { start: 0, end: 0 },
"project_limit",
"Source project exceeds 2 files.",
),
(
ProjectLimits {
max_include_edges: 1,
..fits
},
"part.arcadia",
Span { start: 0, end: 1 },
"project_limit",
"Include graph exceeds 1 edges.",
),
(
ProjectLimits {
max_include_depth: 1,
..fits
},
"part.arcadia",
Span { start: 0, end: 23 },
"project_limit",
"Include depth exceeds 1.",
),
(
ProjectLimits {
model: ParseLimits {
max_source_bytes: 131,
..fits.model
},
..fits
},
"main.arcadia",
Span { start: 0, end: 1 },
"invalid_source",
"Source8 index/output byte limit exceeded.",
),
] {
error_at(&bundle, &fails(&bundle, limits), path, span, code, message);
}
}
#[test]
fn public_mixed_read_paths_missing_cycles_and_detached_files_are_errors() {
let good = chain();
succeeds(&good, ProjectLimits::default(), &empty_project(), &[]);
let mut missing = good.clone();
missing.files.remove("last.arcadia");
error_at(
&missing,
&fails(&missing, ProjectLimits::default()),
"part.arcadia",
Span { start: 0, end: 23 },
"missing_include",
"Included file 'last.arcadia' is missing.",
);
let mut detached = good.clone();
detached
.files
.insert("unused.arcadia".into(), "// valid but detached\n".into());
error_at(
&detached,
&fails(&detached, ProjectLimits::default()),
"unused.arcadia",
Span { start: 0, end: 0 },
"unreachable_file",
"Supplied source file is not reachable from the entry file.",
);
let mut cycle = good.clone();
cycle
.files
.insert("last.arcadia".into(), "include \"part.arcadia\";".into());
error_at(
&cycle,
&fails(&cycle, ProjectLimits::default()),
"last.arcadia",
Span { start: 0, end: 23 },
"include_cycle",
"Include cycle: part.arcadia -> last.arcadia -> part.arcadia.",
);
let mut invalid = good.clone();
invalid
.files
.insert("../escape.arcadia".into(), String::new());
error_at(
&invalid,
&fails(&invalid, ProjectLimits::default()),
"main.arcadia",
Span { start: 0, end: 0 },
"invalid_path",
"Non-canonical source path '../escape.arcadia'.",
);
let mut collision = good;
collision
.files
.insert("PART.arcadia".into(), "// detached uppercase\n".into());
let errors = fails(&collision, ProjectLimits::default());
error_at(
&collision,
&errors,
"main.arcadia",
Span { start: 0, end: 0 },
"duplicate_path",
"Case-colliding source paths 'PART.arcadia' and 'part.arcadia'.",
);
}
#[test]
fn public_mixed_read_final_nested_model_limit_is_distinct_from_syntax_entries() {
let bundle = single(&calc("f", 2, "$x"));
let mut expected = empty_project();
expected.definitions.insert(
id(2),
calculation_model("f", 2, a::Expr::Parameter(a::ParameterId(id(1)))),
);
// Two declarations fit both policies. Parameter and expression make four
// final records; this does not pretend to set the internal entry quota.
let limits = ProjectLimits {
model: ParseLimits {
max_records: 4,
max_graph_objects: 1,
..ParseLimits::UNLIMITED
},
..ProjectLimits::default()
};
succeeds(&bundle, limits, &expected, &[]);
let errors = fails(
&bundle,
ProjectLimits {
model: ParseLimits {
max_records: 3,
..limits.model
},
..limits
},
);
error_at(
&bundle,
&errors,
"main.arcadia",
Span { start: 0, end: 1 },
"model_limit",
&format!(
"Model exceeds admission limits (1 graph objects, 3 total records, {} ordered references).",
usize::MAX
),
);
let errors = fails(
&bundle,
ProjectLimits {
model: ParseLimits {
max_graph_objects: 0,
..limits.model
},
..limits
},
);
assert_eq!(errors[0].diagnostic.code, "model_limit");
let start = bundle.files["main.arcadia"].find("calculation").unwrap();
assert_eq!(
errors[0].diagnostic.span,
Span {
start,
end: start + 1
}
);
}
#[test]
fn public_mixed_read_typed_reference_caller_allowance_has_a_real_positive_control() {
let bundle = single(&calc("f", 2, "$x"));
let mut expected = empty_project();
expected.definitions.insert(
id(2),
calculation_model("f", 2, a::Expr::Parameter(a::ParameterId(id(1)))),
);
let fits = ProjectLimits {
model: ParseLimits {
max_step_references: 1,
..ParseLimits::UNLIMITED
},
..ProjectLimits::default()
};
succeeds(&bundle, fits, &expected, &[]);
let start = bundle.files["main.arcadia"].find("$x").unwrap() + 1;
error_at(
&bundle,
&fails(
&bundle,
ProjectLimits {
model: ParseLimits {
max_step_references: 0,
..fits.model
},
..fits
},
),
"main.arcadia",
Span {
start,
end: start + 1,
},
"invalid_source",
"Source8 references limit exceeded.",
);
}
#[test]
fn public_mixed_read_malformed_duplicate_and_wrong_domain_references() {
let calculation = calc("f", 2, "1");
let valid = single(&calculation);
let mut expected = empty_project();
expected.definitions.insert(
id(2),
calculation_model("f", 2, a::Expr::Number(Exact::from_integer(1))),
);
succeeds(&valid, ProjectLimits::default(), &expected, &[]);
let mut malformed = valid.clone();
*malformed.files.get_mut("main.arcadia").unwrap() = document(&calc("f", 2, ""));
let start = malformed.files["main.arcadia"].find("body ;").unwrap() + 5;
error_at(
&malformed,
&fails(&malformed, ProjectLimits::default()),
"main.arcadia",
Span {
start,
end: start + 1,
},
"invalid_source",
"Expected an analysis expression.",
);
let mut unresolved = single("include \"typed.arcadia\";");
unresolved
.files
.insert("typed.arcadia".into(), calc("f", 2, "@missing(x: 1)"));
let start = unresolved.files["typed.arcadia"].find("@missing").unwrap() + 1;
error_at(
&unresolved,
&fails(&unresolved, ProjectLimits::default()),
"typed.arcadia",
Span {
start,
end: start + 7,
},
"invalid_source",
"Unknown global reference '@missing'.",
);
let ordinary = format!("oa_entity q \"Entity\" id \"{}\" {{}}", id(3));
let wrong = single(&format!("{ordinary}\n{}", calc("f", 2, "@q(x: 1)")));
let start = wrong.files["main.arcadia"].find("@q").unwrap() + 1;
error_at(
&wrong,
&fails(&wrong, ProjectLimits::default()),
"main.arcadia",
Span {
start,
end: start + 1,
},
"invalid_source",
"Wrong reference domain for '@q': Element(Entity, Oa).",
);
let duplicate = single(&format!("{calculation}\n{}", calc("f", 3, "1")));
let errors = fails(&duplicate, ProjectLimits::default());
let second = calc("f", 3, "1");
let span = authored_span(&duplicate.files["main.arcadia"], &second);
error_at(
&duplicate,
&errors,
"main.arcadia",
span,
"invalid_source",
"Duplicate global alias.",
);
assert_eq!(errors[0].related.len(), 1);
assert_eq!(errors[0].related[0].path, "main.arcadia");
assert_eq!(
errors[0].related[0].span,
authored_span(&duplicate.files["main.arcadia"], &calculation)
);
let second = calc("g", 2, "1");
let duplicate_id = single(&format!("{calculation}\n{second}"));
error_at(
&duplicate_id,
&fails(&duplicate_id, ProjectLimits::default()),
"main.arcadia",
authored_span(&duplicate_id.files["main.arcadia"], &second),
"invalid_source",
"Duplicate global identity.",
);
let duplicate_parameter = calc("f", 2, "$x").replace(
"result Number;",
&format!("input x id \"{}\" : Number; result Number;", id(4)),
);
let bad = single(&duplicate_parameter);
let at = bad.files["main.arcadia"].rfind("input x").unwrap() + 6;
error_at(
&bad,
&fails(&bad, ProjectLimits::default()),
"main.arcadia",
Span {
start: at,
end: at + 1,
},
"invalid_source",
"Duplicate parameter name.",
);
let second_parameter = format!("input y id \"{}\" : Number;", id(1));
let duplicate_parameter_id = calc("f", 2, "$x").replace(
"result Number;",
&format!("{second_parameter} result Number;"),
);
let bad = single(&duplicate_parameter_id);
let at = authored_span(&bad.files["main.arcadia"], &second_parameter).start + 11;
error_at(
&bad,
&fails(&bad, ProjectLimits::default()),
"main.arcadia",
Span {
start: at,
end: at + 1,
},
"invalid_source",
"Duplicate parameter identity.",
);
}
#[test]
fn public_mixed_read_unicode_columns_are_not_utf8_byte_columns() {
let source = "arcadia 8;\r\nproject p \"港🌊\" id \"00000000-0000-0000-0000-000000000001\" { oa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {} }\r\n";
let bundle = SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([("main.arcadia".into(), source.into())]),
};
let mut expected = empty_project();
expected.name = "港🌊".into();
expected.elements.insert(
id(2),
Element {
id: id(2),
alias: "w".into(),
name: "Work".into(),
description: String::new(),
parent: None,
body: ElementBody::Oa(OaBody::Activity),
},
);
let warnings = [
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
].map(|(code, field, message)| SourceDiagnostic {
path: "main.arcadia".into(), related: Vec::new(),
diagnostic: language::Diagnostic { severity: validation::Severity::Warning, code: code.into(), object_id: Some(id(2)), field: field.map(str::to_owned), message: message.into(), span: Span { start: 76, end: 141 }, line: 2, column: 60 },
});
succeeds(&bundle, ProjectLimits::default(), &expected, &warnings);
}
#[test]
fn public_mixed_read_ordinary_parity_uses_an_independent_complete_model() {
let b = single(&format!(
"oa_entity team \"Team\" id \"{}\" {{ oa_actor sailor \"Sailor\" id \"{}\" {{ description \"海\"; }} }}",
id(2),
id(3)
));
let mut expected = empty_project();
for (n, alias, name, description, parent, body) in [
(2, "team", "Team", "", None, ElementBody::Oa(OaBody::Entity)),
(
3,
"sailor",
"Sailor",
"海",
Some(id(2)),
ElementBody::Oa(OaBody::Actor),
),
] {
expected.elements.insert(
id(n),
Element {
id: id(n),
alias: alias.into(),
name: name.into(),
description: description.into(),
parent,
body,
},
);
}
succeeds(&b, ProjectLimits::default(), &expected, &[]);
let ordinary = language::parse_with_report(&b.files["main.arcadia"]).unwrap();
assert!(ordinary.migration.is_none());
assert_eq!(ordinary.project, expected);
let compiled = source_project::compile_bundle(&b, ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, expected);
assert_eq!(compiled.bundle, b);
}
+17
View File
@@ -633,6 +633,23 @@ a { text-decoration: none; }
<dd class="text-suffix"><p><span class="docutils literal">.arcadia</span></p>
</dd>
</dl>
<section id="staged-source8-rust-read-api">
<h2>Staged source8 Rust read API</h2>
<p>The core library separately offers <span class="docutils literal"><span class="pre">language::check_mixed_bundle(&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">
<h2>1. Scope and conformance</h2>
<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``
:Text suffix: ``.arcadia``
Staged source8 Rust read API
----------------------------------------
The core library separately offers ``language::check_mixed_bundle(&SourceBundle,
ProjectLimits)`` for complete ordinary/typed source8 validation. Its opaque
``CheckedMixed`` result borrows the unchanged input and exposes only ``project()``
and ``warnings()``. Explicit UUIDs are required for the project, every ordinary
and typed declaration, and typed parameters; no IDs are generated. All supplied
canonical files must be reachable from the entry. Errors return no admitted
model; success preserves actual located warnings and the complete model.
This bounded API selects both retained text and entry producer checks and carries
the caller's whole-project policy through admission. It does not promise an
all-heap memory bound. It performs no filesystem discovery, migration, formatting,
source stabilization, indexing, saving or history installation. Existing ordinary
parse/compile/update APIs, CLI commands, desktop and server flows remain separate;
a read result does not grant their typed editing or save authority. Generic and
typed depth policies and platform qualification limits remain unchanged. This
staged capability does not replace the broader conformance obligations below.
1. Scope and conformance
------------------------
@@ -0,0 +1,63 @@
# Public mixed read implementation
Base7b06b8ea, selected root contract bcddaba3. Public
language::check_mixed_bundle returns opaque non-Clone CheckedMixed borrowing the
complete original SourceBundle, with only immutable project/warnings accessors.
Its private resolved field retains syntax, expression/occurrence vectors, ledger
and admission evidence; there is no index, output, save or extraction authority.
The shared parser mixed hooks now compile in production. Ordinary callers still
select None. The fresh constructor binds the actual bundle and exact ProjectLimits,
derives existing internal policy once, selects Some(Borrowed) with zero carry and
both producers, then consumes syntax through complete resolution/admission.
The borrowed guard retains the actual source reference even when test-only
Pending/Retained variants are absent. No duplicate live policy/model is added.
Public schema/UUID requirement wording replaces private-staging wording only;
all limiter phase/category/span transport remains inherited.
Private legacy index/update/write, ordinary-index/rename/emission/acceptance and
fixture/pending constructors remain test-only. Shared parse/lower code remains
private. Necessary retained fields have individual documented non-test dead-code
allowances; no module-wide suppression, fake read, or new public lint-only API.
Exact production compiler lint findings remain a parent gate, not inferred clean
from source inspection. Existing298 tests are not edited.
One internal public_read test invokes the actual production parse helper and
observes borrowedC0, both producer inventories, every inherited/derived policy
field (including262144 entries independent of model records1), full empty-Project
and warning parity, Usage/numeric parity and unchanged input. Three rustdoc blocks
provide a compiling real usage example plus source-drop and source-mutation
compile-fail cases with later result use. Independent worker owns the non-test
mixed_source8_read integration target and its separate complete-model oracles.
Established docs/language.rst gains a bounded staged-read note without changing
its broader conformance/version obligations. Existing docutils HTML5 generation
with halt/report level2 regenerated language.html successfully. An initial local
RST generation refused the new section formatting; the section separator and
blank line were corrected before successful regeneration. No Rust compiler,
Cargo test, CLI, debugger or publication was run by this worker.
Parent owns pinned compilation, all old/new runtime tests, doctests, strict
Clippy, formatting disposition and production Parser stack regression checks.
DEPTH_128_VALIDATED=false, existing depth policies, logical-accounting exclusions,
full-pipeline limitation and deferred Linux qualification remain unchanged.
## First non-test compile correction
Parent's integration-list build at ddf823a stopped with E0063: the semantic review
row-key reader's Parser initializer still gated mixed:None under cfg(test).
That initializer now supplies None in production too, preserving its ordinary
facet-reading behavior. Source inventory found the other shared Parser initializers
in language.rs and mixed.rs already coherent; language::legacy uses its own
separate Parser type. No API, test or runtime policy changed. The failed build
record remains preserved; no compiler was run for this repair by the worker.
## Non-test integration result and warning cleanup
Parent's actual non-test integration suite at 8555e417 passed all ten tests with
the independent complete-model and diagnostic assertions unchanged. Compilation
reported an unused Budget::limits observer and a redundant private language self
import shadowing the integration helper's public glob export. The observer is
now cfg(test), matching all real callers; the integration import drops only self.
No semantic implementation, policy, fixture or expected result changed. Parent
explicitly authorized that one test import-line edit after its independent worker
finished. This is warning cleanup, not a claim that later lint/full/doctest gates
already passed. No compiler was run by this worker.