Compare commits

...
Author SHA1 Message Date
sorokin.oleg.edu f10490b13b Correct CLI migration test expectations for source8 2026-09-24 22:07:49 -07:00
sorokin.oleg.edu f232482293 Document mixed filesystem validation in the language guide 2026-09-24 21:58:07 -07:00
sorokin.oleg.edu 3fd87bd8be feat(source): add bounded read-only mixed filesystem discovery 2026-09-24 21:57:16 -07:00
sorokin.oleg.edu f83f3bf45b Add explicit read-only mixed source8 CLI checking 2026-09-24 21:57:06 -07:00
sorokin.oleg.edu bab6f96a5b chore(source): scope private policy observer and remove redundant import 2026-09-24 21:29:47 -07:00
sorokin.oleg.edu 8555e417ee fix(source): initialize mixed mode in production semantic row reader 2026-09-24 21:26:30 -07:00
sorokin.oleg.edu ddf823a3d3 Test public mixed reads through external model and diagnostic oracles 2026-09-24 21:21:05 -07:00
sorokin.oleg.edu 042e25efc2 feat(source): expose borrowed read-only mixed source eight validation 2026-09-24 21:20:11 -07:00
sorokin.oleg.edu 7b06b8ea1b test(migration): expect explicit empty schema eight definition catalog 2026-09-24 20:51:56 -07:00
sorokin.oleg.edu 0c07f53ab5 test(source): distinguish ordinary eight admission from typed refusal 2026-09-24 20:43:40 -07:00
sorokin.oleg.edu 520e50ce62 test(source): author explicit trace and reject actual typed case spellings 2026-09-24 20:38:33 -07:00
sorokin.oleg.edu 33b86a4d6b fix(source): admit ordinary schema eight with current roundtrip coverage 2026-09-24 20:31:39 -07:00
sorokin.oleg.edu edf096cd33 Correct accepted-output warning oracles 2026-09-24 20:04:15 -07:00
sorokin.oleg.edu 4872141c03 Add private actual-output rename acceptance 2026-09-24 19:58:09 -07:00
sorokin.oleg.edu 29417130c7 Keep quota refusal test independent of Debug success types 2026-09-24 19:26:36 -07:00
sorokin.oleg.edu a629ae01d4 Reserve retained parser entries at private producer boundaries 2026-09-24 19:25:08 -07:00
sorokin.oleg.edu 97a8703adc Use direct matching in mixed quoted text hook 2026-09-24 18:57:41 -07:00
sorokin.oleg.edu 6b0ef99eca Admit private syntax text before named producers grow 2026-09-24 18:52:39 -07:00
45 changed files with 5740 additions and 152 deletions
+66 -11
View File
@@ -170,6 +170,47 @@ fn disk_error(report: &mut Report, error: SourceFileError) {
report.fail(3, code, error.to_string());
}
/// Validate captured mixed source8 bytes without creating an editable candidate.
pub fn check_mixed(entry: &Path) -> Report {
let mut report = Report::empty(entry, "check-mixed");
let snapshot = match source_files::load_mixed_source(entry, LIMITS) {
Ok(snapshot) => snapshot,
Err(error) => {
disk_error(&mut report, error);
return report;
}
};
let diagnostics = if snapshot.diagnostics().is_empty() {
match language::check_mixed_bundle(snapshot.bundle(), LIMITS) {
Ok(checked) => checked.warnings().to_vec(),
Err(errors) => errors,
}
} else {
snapshot.diagnostics().to_vec()
};
if diagnostics
.iter()
.any(|d| d.diagnostic.severity == Severity::Error)
{
report.status = "invalid".into();
report.exit_code = if diagnostics.iter().any(|d| {
matches!(
d.diagnostic.code.as_str(),
"pending_save" | "missing_include" | "invalid_path"
)
}) {
3
} else {
1
};
}
report.diagnostics = diagnostics
.into_iter()
.map(|d| located(d, "input"))
.collect();
report
}
/// A candidate owns its original expectations and is consumed by saving.
/// Callers cannot replace its source/model/expectations after preparation.
pub struct Prepared {
@@ -406,14 +447,19 @@ fn unified_diff(path: &str, before: &str, after: &str) -> String {
out
}
/// Read-only mixed checking cannot acquire ordinary edit/write authority.
pub enum Dispatch {
Editable { operation: Operation, write: bool },
CheckMixed,
}
pub struct Arguments {
pub entry: PathBuf,
pub operation: Operation,
pub write: bool,
pub dispatch: Dispatch,
pub json: bool,
}
pub const HELP: &str = "arcadia check ROOT [--json]\narcadia lint ROOT [--json]\narcadia stabilize ROOT [--write] [--json]\narcadia rename-alias ROOT OLD NEW [--write] [--json]\n\nROOT is an explicit .arcadia entry file; includes resolve relative to it.\nMutation commands preview by default. --write applies this invocation's candidate.\nMissing UUIDs in a preview are provisional until written. No canonical formatting.\nExit: 0 valid/success (engineering warnings allowed), 1 invalid source or semantic\ncommand rejection, 2 usage, 3 disk/path/conflict/pending-journal/output failure.\nNo implicit journal recovery. Limits: 64 MiB, 256 files, include depth32/edges1024,\n100000 graph objects, 250000 records, 500000 ordered references.\n";
pub const HELP: &str = "arcadia check ROOT [--json]\narcadia lint ROOT [--json]\narcadia check-mixed ROOT [--json]\narcadia stabilize ROOT [--write] [--json]\narcadia rename-alias ROOT OLD NEW [--write] [--json]\n\nROOT is an explicit .arcadia entry file; includes resolve relative to it.\ncheck-mixed requires source8 and explicit UUIDs; it never migrates, stabilizes or writes.\nMutation commands preview by default. --write applies this invocation's candidate.\nMissing UUIDs in a preview are provisional until written. No canonical formatting.\nExit: 0 valid/success (engineering warnings allowed), 1 invalid source or semantic\ncommand rejection, 2 usage, 3 disk/path/conflict/pending-journal/output failure.\nNo implicit journal recovery. Limits: 64 MiB, 256 files, include depth32/edges1024,\n100000 graph objects, 250000 records, 500000 ordered references.\n";
pub fn arguments(args: &[std::ffi::OsString]) -> Result<Arguments, String> {
let mut positional = Vec::new();
@@ -433,19 +479,28 @@ pub fn arguments(args: &[std::ffi::OsString]) -> Result<Arguments, String> {
}
}
let command = positional.first().and_then(|s| s.to_str()).unwrap_or("");
let operation = match (command, positional.len()) {
("check" | "lint", 2) if !write => Operation::Check,
("stabilize", 2) => Operation::Stabilize,
("rename-alias", 4) => Operation::RenameAlias {
old: positional[2].to_str().ok_or("Alias must be UTF-8")?.into(),
new: positional[3].to_str().ok_or("Alias must be UTF-8")?.into(),
let dispatch = match (command, positional.len()) {
("check-mixed", 2) if !write => Dispatch::CheckMixed,
("check" | "lint", 2) if !write => Dispatch::Editable {
operation: Operation::Check,
write,
},
("stabilize", 2) => Dispatch::Editable {
operation: Operation::Stabilize,
write,
},
("rename-alias", 4) => Dispatch::Editable {
operation: Operation::RenameAlias {
old: positional[2].to_str().ok_or("Alias must be UTF-8")?.into(),
new: positional[3].to_str().ok_or("Alias must be UTF-8")?.into(),
},
write,
},
_ => return Err(HELP.into()),
};
Ok(Arguments {
entry: PathBuf::from(positional[1]),
operation,
write,
dispatch,
json,
})
}
+30 -19
View File
@@ -1,4 +1,4 @@
use oa_cli::{Report, arguments, prepare};
use oa_cli::{Dispatch, Report, arguments, check_mixed, prepare};
use std::{
io::{self, Write},
path::Path,
@@ -26,13 +26,21 @@ fn human_report(report: &Report, show_diff: bool) -> io::Result<()> {
d.severity, d.code, d.coordinate_space, d.message
)?;
}
writeln!(
err,
"{}: {} file changes; {} identities to stabilize",
report.status,
report.changes.len(),
report.identities_to_stabilize
)?;
if report.operation == "check-mixed" {
writeln!(
err,
"{}: mixed source8 check; no files changed",
report.status
)?;
} else {
writeln!(
err,
"{}: {} file changes; {} identities to stabilize",
report.status,
report.changes.len(),
report.identities_to_stabilize
)?;
}
if let Some(migration) = &report.migration {
writeln!(
err,
@@ -67,19 +75,22 @@ fn run() -> io::Result<u8> {
let mut displayed = false;
let report = match arguments(&args) {
Err(error) => Report::failure(Path::new(""), "usage", 2, "usage", error),
Ok(args) => match prepare(&args.entry, args.operation) {
Err(report) => *report,
Ok(prepared) => {
if args.write {
if !args.json {
human_report(prepared.report(), true)?;
displayed = true;
Ok(args) => match args.dispatch {
Dispatch::CheckMixed => check_mixed(&args.entry),
Dispatch::Editable { operation, write } => match prepare(&args.entry, operation) {
Err(report) => *report,
Ok(prepared) => {
if write {
if !args.json {
human_report(prepared.report(), true)?;
displayed = true;
}
prepared.write()
} else {
prepared.into_report()
}
prepared.write()
} else {
prepared.into_report()
}
}
},
},
};
if json {
+490
View File
@@ -0,0 +1,490 @@
//! Actual executable tests. Expected values are authored independently of CLI output.
use oa_core::language::Span;
use oa_core::source_project::{SourceBundle, SourceDiagnostic};
pub use oa_core::*;
use serde_json::{Value, json};
use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
process::{Command as ProcessCommand, Output},
};
#[path = "../../oa-core/src/language/analysis_source/model_oracle.rs"]
mod independent_model;
fn id(n: u128) -> Id {
Id::from_u128(n)
}
fn document(body: &str) -> String {
format!(
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
id(1)
)
}
fn authored_span(source: &str, literal: &str) -> Span {
assert_eq!(source.matches(literal).count(), 1);
let start = source.find(literal).unwrap();
Span {
start,
end: start + literal.len(),
}
}
struct Fixture(PathBuf);
impl Fixture {
fn new() -> Self {
let p = std::env::temp_dir().join(format!("arcadia-mixed-cli-{}", Id::new_v4()));
fs::create_dir(&p).unwrap();
Self(p)
}
fn put(&self, name: &str, bytes: impl AsRef<[u8]>) {
let p = self.0.join(name);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, bytes).unwrap();
}
fn entry(&self) -> PathBuf {
self.0.join("main.arcadia")
}
fn inventory(&self) -> BTreeMap<PathBuf, Option<Vec<u8>>> {
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, Option<Vec<u8>>>) {
for item in fs::read_dir(dir).unwrap() {
let p = item.unwrap().path();
if p.is_dir() {
out.insert(p.strip_prefix(root).unwrap().into(), None);
walk(root, &p, out);
} else {
out.insert(
p.strip_prefix(root).unwrap().into(),
Some(fs::read(p).unwrap()),
);
}
}
}
let mut out = BTreeMap::new();
walk(&self.0, &self.0, &mut out);
out
}
fn run(&self, options: &[&str]) -> Output {
let before = self.inventory();
let result = ProcessCommand::new(env!("CARGO_BIN_EXE_arcadia"))
.arg("check-mixed")
.arg(self.entry())
.args(options)
.output()
.unwrap();
assert_eq!(
self.inventory(),
before,
"all file bytes and directory entries preserved"
);
result
}
fn expect(&self, exit: i32, diagnostics: Vec<Value>) {
let output = self.run(&["--json"]);
assert_eq!(
output.status.code(),
Some(exit),
"{}",
String::from_utf8_lossy(&output.stdout)
);
assert!(
output.stderr.is_empty(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<Value>(&output.stdout).unwrap(),
self.report(exit, diagnostics)
);
}
fn report(&self, exit: i32, diagnostics: Vec<Value>) -> Value {
json!({"format_version":1,"entry":self.entry().to_string_lossy(),"operation":"check-mixed",
"status":if exit==0 {"checked"}else{"invalid"},"exit_code":exit,"written":false,
"diagnostics":diagnostics,"changes":[],"identities_to_stabilize":0,"migration":null,"notes":[]})
}
}
impl Drop for Fixture {
fn drop(&mut self) {
fs::remove_dir_all(&self.0).unwrap();
}
}
fn diagnostic(source: &str, path: &str, span: Span, code: &str, message: &str) -> Value {
let prefix = &source[..span.start];
json!({"severity":"error","code":code,"message":message,"coordinate_space":"input",
"location":{"path":path,"start":span.start,"end":span.end,
"line":prefix.bytes().filter(|b|*b==b'\n').count()+1,
"column":prefix.rsplit('\n').next().unwrap().chars().count()+1},
"related":[],"object_id":null,"field":null})
}
fn warning(d: &SourceDiagnostic) -> Value {
let at = &d.diagnostic;
assert!(d.related.is_empty());
json!({"severity":"warning","code":at.code,"message":at.message,"coordinate_space":"input",
"location":{"path":d.path,"start":at.span.start,"end":at.span.end,"line":at.line,"column":at.column},
"related":[],"object_id":at.object_id.unwrap().to_string(),"field":at.field})
}
fn rich(layer: Layer, crlf: bool) -> (SourceBundle, Project, Vec<SourceDiagnostic>) {
let (ordinary, typed, work) = match layer {
Layer::Oa => (
include_str!(
"../../oa-core/src/language/analysis_source/lower/fixtures/ordinary-oa.arcadia"
),
include_str!(
"../../oa-core/src/language/analysis_source/lower/fixtures/typed-oa.arcadia"
),
"oa_activity",
),
Layer::Sa => (
include_str!(
"../../oa-core/src/language/analysis_source/lower/fixtures/ordinary-sa.arcadia"
),
include_str!(
"../../oa-core/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 mixed_cli_complete_oa_sa_models_reports_and_unchanged_bytes() {
for layer in [Layer::Oa, Layer::Sa] {
for crlf in [false, true] {
let (mut bundle, expected, _) = rich(layer, crlf);
// Escaped include path and a decoy in a comment exercise real grammar discovery.
let main = bundle.files.get_mut("main.arcadia").unwrap();
*main = main.replace("typed.arcadia", "typed\\u002earcadia");
let comment = if crlf {
"// include \"missing.arcadia\"; 港🌊\r\n"
} else {
"// include \"missing.arcadia\"; 港🌊\n"
};
main.insert_str(0, comment);
// Rebuild only warning locations from unique authored declaration literals,
// never from parsed diagnostics or emitted reports.
let (_, _, mut warnings) = rich(layer, crlf);
for d in &mut warnings {
let e = &expected.elements[&d.diagnostic.object_id.unwrap()];
let work = if layer == Layer::Oa {
"oa_activity"
} else {
"sa_function"
};
let literal = format!("{work} {} \"{}\" id \"{}\" {{}}", e.alias, e.name, e.id);
let span = authored_span(main, &literal);
let prefix = &main[..span.start];
d.diagnostic.span = span;
d.diagnostic.line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
d.diagnostic.column = prefix.rsplit('\n').next().unwrap().chars().count() + 1;
}
let f = Fixture::new();
for (path, text) in &bundle.files {
f.put(path, text);
}
f.put("unrelated/keep.bin", [0, 255, 13, 10]);
let snapshot = source_files::load_mixed_source(&f.entry(), oa_cli::LIMITS).unwrap();
assert!(snapshot.diagnostics().is_empty());
assert_eq!(snapshot.bundle(), &bundle);
let checked = language::check_mixed_bundle(snapshot.bundle(), oa_cli::LIMITS)
.unwrap_or_else(|e| panic!("{e:#?}"));
assert_eq!(checked.project(), &expected);
assert_eq!(checked.warnings(), warnings);
f.expect(0, warnings.iter().map(warning).collect());
}
}
}
#[test]
fn mixed_cli_missing_id_classes_and_schema_are_exact_refusals() {
let ordinary = format!("oa_entity e \"Entity\" id \"{}\" {{}}", id(2));
let typed = format!(
"calculation f \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body $x; }}",
id(3),
id(1)
);
let valid = document(&format!("{ordinary}\n{typed}"));
let f = Fixture::new();
f.put("main.arcadia", &valid);
f.expect(0, vec![]);
for (needle, replacement, anchor) in [
(format!("id \"{}\"", id(1)), String::new(), "project"),
(format!("id \"{}\"", id(2)), String::new(), "oa_entity"),
(format!("id \"{}\"", id(3)), String::new(), "calculation"),
(
format!("input x id \"{}\"", id(1)),
"input x".into(),
"calculation",
),
] {
let source = valid.replacen(&needle, &replacement, 1);
let begin = source.find(anchor).unwrap();
let start = if anchor == "calculation" {
begin
} else {
begin + source[begin..].find('{').unwrap()
};
f.put("main.arcadia", &source);
f.expect(
1,
vec![diagnostic(
&source,
"main.arcadia",
Span {
start,
end: start + 1,
},
"invalid_source",
"Mixed source8 requires explicit UUIDs.",
)],
);
}
for schema in [3, 7, 9] {
let source = document("").replacen("arcadia 8", &format!("arcadia {schema}"), 1);
f.put("main.arcadia", &source);
f.expect(
1,
vec![diagnostic(
&source,
"main.arcadia",
Span { start: 8, end: 9 },
"invalid_source",
"Mixed source requires arcadia 8.",
)],
);
}
}
#[test]
fn mixed_cli_final_semantic_refusal_keeps_original_cross_file_coordinates() {
let f = Fixture::new();
f.put("main.arcadia", document("include \"part.arcadia\";"));
let part = format!(
"// 港🌊\r\ncalculation f \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body @missing(x: $x); }}\r\n",
id(2),
id(1)
);
let start = part.find("@missing").unwrap() + 1;
f.put("part.arcadia", &part);
f.expect(
1,
vec![diagnostic(
&part,
"part.arcadia",
Span {
start,
end: start + 7,
},
"invalid_source",
"Unknown global reference '@missing'.",
)],
);
}
#[test]
fn mixed_cli_disk_discovery_and_cycle_refusals_preserve_every_entry() {
let f = Fixture::new();
let source = document("include \"part.arcadia\";");
let span = authored_span(&source, "include \"part.arcadia\";");
f.put("main.arcadia", &source);
let missing = f.0.canonicalize().unwrap().join("part.arcadia");
let os_error = fs::symlink_metadata(&missing).unwrap_err();
f.expect(
3,
vec![diagnostic(
&source,
"main.arcadia",
span,
"missing_include",
&format!(
"Cannot load 'part.arcadia': Source file operation failed at {}: {os_error}",
missing.display()
),
)],
);
f.put("part.arcadia", [0xff]);
f.expect(
1,
vec![diagnostic(
&source,
"main.arcadia",
span,
"source_encoding",
"'part.arcadia' is not UTF-8 source.",
)],
);
let part = "include \"main.arcadia\";";
f.put("part.arcadia", part);
f.expect(
1,
vec![diagnostic(
part,
"part.arcadia",
Span { start: 0, end: 23 },
"include_cycle",
"Include cycle: main.arcadia -> part.arcadia -> main.arcadia.",
)],
);
let escape = document("include \"../escape.arcadia\";");
f.put("main.arcadia", &escape);
f.expect(
3,
vec![diagnostic(
&escape,
"main.arcadia",
authored_span(&escape, "include \"../escape.arcadia\";"),
"invalid_path",
"Include escapes the source project root.",
)],
);
f.put("main.arcadia", document(""));
let journal = f.0.join(".arcadia-save-main.arcadia.journal");
fs::create_dir(&journal).unwrap();
fs::write(journal.join("sentinel"), b"keep").unwrap();
let physical =
f.0.canonicalize()
.unwrap()
.join(".arcadia-save-main.arcadia.journal");
f.expect(
3,
vec![diagnostic(
&document(""),
"main.arcadia",
Span { start: 0, end: 0 },
"pending_save",
&format!(
"A save was interrupted. Review and recover {} before saving this project.",
physical.display()
),
)],
);
}
#[test]
fn mixed_cli_usage_and_human_output_never_suggest_an_edit_candidate() {
let f = Fixture::new();
f.put("main.arcadia", document(""));
let output = f.run(&[]);
assert!(output.status.success());
assert!(output.stdout.is_empty());
assert_eq!(
output.stderr,
b"checked: mixed source8 check; no files changed\n"
);
for options in [
vec!["--write", "--json"],
vec!["--unknown", "--json"],
vec!["--json", "--json"],
] {
let output = f.run(&options);
assert_eq!(output.status.code(), Some(2));
assert!(output.stderr.is_empty());
let message = if options[0] == "--write" {
oa_cli::HELP.to_string()
} else if options[0] == "--unknown" {
"Unknown or repeated option: --unknown".into()
} else {
"Unknown or repeated option: --json".into()
};
assert_eq!(
serde_json::from_slice::<Value>(&output.stdout).unwrap(),
json!({"format_version":1,"entry":"","operation":"usage","status":"rejected","exit_code":2,"written":false,
"diagnostics":[{"severity":"error","code":"usage","message":message,"coordinate_space":"operation","location":null,"related":[],"object_id":null,"field":null}],"changes":[],"identities_to_stabilize":0,"migration":null,"notes":[]})
);
}
}
#[test]
fn mixed_cli_direct_disk_error_uses_existing_operation_error_envelope() {
let f = Fixture::new();
f.put(".arcadia-save-reserved.arcadia", document(""));
let before = f.inventory();
let entry = f.0.join(".arcadia-save-reserved.arcadia");
let output = ProcessCommand::new(env!("CARGO_BIN_EXE_arcadia"))
.arg("check-mixed")
.arg(&entry)
.arg("--json")
.output()
.unwrap();
assert_eq!(f.inventory(), before);
assert_eq!(output.status.code(), Some(3));
assert!(output.stderr.is_empty());
assert_eq!(
serde_json::from_slice::<Value>(&output.stdout).unwrap(),
json!({"format_version":1,"entry":entry.to_string_lossy(),"operation":"check-mixed","status":"rejected","exit_code":3,"written":false,
"diagnostics":[{"severity":"error","code":"unsafe_path","message":"Unsafe source path: Non-canonical, reserved or overlong path '.arcadia-save-reserved.arcadia'.","coordinate_space":"operation","location":null,"related":[],"object_id":null,"field":null}],"changes":[],"identities_to_stabilize":0,"migration":null,"notes":[]})
);
}
+10 -3
View File
@@ -69,6 +69,8 @@ fn complete_alarm_example_preview_then_write_and_fixed_point() {
let source = include_str!(
"../../../harness/experiments/textual-authoring-20260911/alarm-response.arcadia"
);
assert!(source.starts_with("arcadia 7;"));
let migrated_source = source.replacen("arcadia 7;", "arcadia 8;", 1);
fixture.put("project.arcadia", source);
let (code, check) = fixture.cli(&["check"]);
assert_eq!(code, 0);
@@ -76,6 +78,8 @@ fn complete_alarm_example_preview_then_write_and_fixed_point() {
let (code, preview) = fixture.cli(&["stabilize"]);
assert_eq!(code, 0);
assert_eq!(preview["identities_to_stabilize"], 40);
assert_eq!(preview["migration"]["from_schema"], 7);
assert_eq!(preview["migration"]["to_schema"], 8);
assert_eq!(preview["status"], "preview");
assert_eq!(fixture.read("project.arcadia"), source);
assert!(
@@ -94,7 +98,10 @@ fn complete_alarm_example_preview_then_write_and_fixed_point() {
for (id, _) in project.identities() {
stripped = stripped.replace(&format!(" id \"{id}\""), "");
}
assert_eq!(stripped, source, "only identity clauses may be inserted");
assert_eq!(
stripped, migrated_source,
"only identity clauses and the schema 7→8 header token may change"
);
let (code, again) = fixture.cli(&["stabilize", "--write"]);
assert_eq!(code, 0);
assert_eq!(again["changes"], serde_json::json!([]));
@@ -306,9 +313,9 @@ fn rename_shows_missing_identity_and_separate_source_migration() {
let report = candidate.report();
assert_eq!(report.identities_to_stabilize, 2);
assert_eq!(report.migration.as_ref().unwrap().from_schema, 5);
assert_eq!(report.migration.as_ref().unwrap().to_schema, 7);
assert_eq!(report.migration.as_ref().unwrap().to_schema, 8);
assert!(report.changes[0].diff.contains("-arcadia 5;"));
assert!(report.changes[0].diff.contains("+arcadia 7;"));
assert!(report.changes[0].diff.contains("+arcadia 8;"));
assert!(report.changes[0].after.contains("oa_entity crew"));
assert_eq!(fixture.read("project.arcadia"), old);
assert_eq!(candidate.write().exit_code, 0);
+156 -47
View File
@@ -3,10 +3,10 @@ mod raw_tokens;
#[path = "language/semantic_source.rs"]
mod semantic_source;
mod source3_bridge;
// Staged syntax foundation only. Public source8 admission waits for complete
// declaration lowering, source updates and the root-owned resource gate.
#[cfg(test)]
// Mixed source8 is exposed only through the separate borrowed read-only checker.
mod analysis_source;
pub(crate) use analysis_source::MixedDiscovery;
pub use analysis_source::{CheckedMixed, check_mixed_bundle};
#[path = "migration_v2/source.rs"]
mod v2_source;
use crate::*;
@@ -338,7 +338,7 @@ fn enum_field(key: &str) -> bool {
)
}
/// Parse V5, or explicitly migrate a valid V1/V2/V3/V4 source candidate. Parsing never
/// Parse ordinary V8, or explicitly migrate a supported older source candidate. Parsing never
/// adopts a model and never overwrites an editor draft.
pub fn parse(source: &str) -> Result<Project, Vec<Diagnostic>> {
parse_limited(source, ParseLimits::UNLIMITED)
@@ -480,7 +480,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
cursor: 0,
value_depth: 0,
limits,
#[cfg(test)]
mixed: None,
}
.document()
@@ -916,7 +915,6 @@ pub(crate) fn parse_unit(
cursor: 0,
value_depth: 0,
limits,
#[cfg(test)]
mixed: None,
}
.unit(entry, true)
@@ -1018,6 +1016,28 @@ fn edit_positions_from_tokens(
token: impl Fn(usize) -> Lexeme,
count: usize,
value_ends: Option<&BTreeMap<usize, usize>>,
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
edit_positions_from_tokens_with_text(source, unit, token, count, value_ends, |_, _| Ok(()))
}
fn edit_positions_from_tokens_with_text(
source: &str,
unit: &SourceUnit,
token: impl Fn(usize) -> Lexeme,
count: usize,
value_ends: Option<&BTreeMap<usize, usize>>,
text: impl FnMut(usize, Span) -> Result<(), Box<Diagnostic>>,
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
edit_positions_from_tokens_with_producers(source, unit, token, count, value_ends, None, text)
}
fn edit_positions_from_tokens_with_producers(
source: &str,
unit: &SourceUnit,
token: impl Fn(usize) -> Lexeme,
count: usize,
value_ends: Option<&BTreeMap<usize, usize>>,
mut quota: Option<usize>,
text: impl FnMut(usize, Span) -> Result<(), Box<Diagnostic>>,
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
let mut ends: Vec<_> = (0..count).map(|at| token(at).span.end).collect();
let mut stack = Vec::new();
@@ -1029,12 +1049,32 @@ fn edit_positions_from_tokens(
_ => {}
}
}
struct Positions<'a, F> {
struct Positions<'a, F, H> {
token_at: F,
ends: Vec<usize>,
value_ends: Option<&'a BTreeMap<usize, usize>>,
text: H,
source: &'a str,
quota: &'a mut Option<usize>,
}
impl<F: Fn(usize) -> Lexeme> Positions<'_, F> {
impl<F: Fn(usize) -> Lexeme, H: FnMut(usize, Span) -> Result<(), Box<Diagnostic>>>
Positions<'_, F, H>
{
fn consume(&mut self, slots: usize, offset: usize) -> Result<(), Box<Diagnostic>> {
if let Some(remaining) = self.quota {
*remaining = remaining.checked_sub(slots).ok_or_else(|| {
Box::new(diagnostic(
self.source,
Error {
code: "invalid_source",
offset,
message: "Source8 projection quota underflow.".into(),
},
))
})?;
}
Ok(())
}
fn token(&self, start: usize) -> usize {
let mut low = 0;
let mut high = self.ends.len();
@@ -1048,24 +1088,30 @@ fn edit_positions_from_tokens(
}
low
}
fn fields(&self, fields: &BTreeMap<String, Ast>) -> BTreeMap<String, EditField> {
fn fields(
&mut self,
fields: &BTreeMap<String, Ast>,
) -> Result<BTreeMap<String, EditField>, Box<Diagnostic>> {
fields
.iter()
.map(|(key, ast)| {
let value = self.value(ast);
self.consume(1, ast.offset())?;
let value = self.value(ast)?;
let start = (self.token_at)(self.token(value.span.start) - 1).span.start;
let end = (self.token_at)(self.token(value.span.end)).span.end;
(
(self.text)(key.len(), Span { start, end })?;
Ok((
key.clone(),
EditField {
span: Span { start, end },
value,
},
)
))
})
.collect()
}
fn value(&self, ast: &Ast) -> EditValue {
fn value(&mut self, ast: &Ast) -> Result<EditValue, Box<Diagnostic>> {
self.consume(1, ast.offset())?;
let index = self.token(ast.offset());
let span = Span {
start: ast.offset(),
@@ -1080,23 +1126,33 @@ fn edit_positions_from_tokens(
}),
};
let kind = match ast {
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)),
Ast::Array(items, _) => {
EditKind::List(items.iter().map(|item| self.value(item)).collect())
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)?),
Ast::Array(items, _) => EditKind::List(
items
.iter()
.map(|item| self.value(item))
.collect::<Result<_, _>>()?,
),
Ast::Reference(alias, _) => {
(self.text)(alias.len(), span)?;
EditKind::Reference(alias.clone())
}
Ast::Reference(alias, _) => EditKind::Reference(alias.clone()),
Ast::HistoricalUuid(id, _) => EditKind::HistoricalUuid(*id),
_ => EditKind::Atom,
};
EditValue { span, kind }
Ok(EditValue { span, kind })
}
}
let positions = Positions {
let mut positions = Positions {
token_at: &token,
ends,
value_ends,
text,
source,
quota: &mut quota,
};
unit.declarations
let result = unit
.declarations
.iter()
.map(|declaration| {
if declaration.id_insertion.is_some() {
@@ -1109,6 +1165,7 @@ fn edit_positions_from_tokens(
},
)));
}
positions.consume(7, declaration.span.start)?;
let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) {
&["kind", "alias", "id_keyword", "id"]
} else {
@@ -1132,12 +1189,23 @@ fn edit_positions_from_tokens(
start: declaration.body.start - 1,
end: declaration.body.end + 1,
},
kind: EditKind::Record(positions.fields(&declaration.fields)),
kind: EditKind::Record(positions.fields(&declaration.fields)?),
},
},
))
})
.collect()
.collect::<Result<Vec<_>, _>>()?;
if quota.is_some_and(|remaining| remaining != 0) {
return Err(Box::new(diagnostic(
source,
Error {
code: "invalid_source",
offset: 0,
message: "Source8 projection quota leftover.".into(),
},
)));
}
Ok(result)
}
struct Parser<'a> {
source: &'a str,
@@ -1145,12 +1213,10 @@ struct Parser<'a> {
cursor: usize,
value_depth: usize,
limits: ParseLimits,
#[cfg(test)]
mixed: Option<analysis_source::mixed::State<'a>>,
}
impl Parser<'_> {
fn token(&self, index: usize) -> Option<Lexeme> {
#[cfg(test)]
if let Some(mixed) = &self.mixed {
return mixed.token(index);
}
@@ -1166,7 +1232,6 @@ impl Parser<'_> {
})
}
fn syntax_entry(&mut self) -> Result<(), Error> {
#[cfg(test)]
{
let span = self.current().span;
if let Some(mixed) = &mut self.mixed {
@@ -1196,11 +1261,19 @@ impl Parser<'_> {
if self.current().class != TokenClass::String {
return Err(self.error("Expected a double-quoted string."));
}
let span = self.current().span;
if let Some(mixed) = &mut self.mixed {
let raw = &self.source[span.start..span.end];
let value = mixed.quoted_text(raw, span)?;
self.cursor += 1;
return Ok(value);
}
let value = serde_json::from_str(self.text()).unwrap();
self.cursor += 1;
Ok(value)
}
fn identifier(&mut self) -> Result<String, Error> {
self.own_text(self.text().len(), self.current().span)?;
let value = self.text().to_owned();
if !crate::valid_alias(&value) {
return Err(self.error(std::format!("Invalid or reserved alias '{value}'.")));
@@ -1210,9 +1283,8 @@ impl Parser<'_> {
}
fn optional_id(&mut self) -> Result<Id, Error> {
if self.text() != "id" {
#[cfg(test)]
if self.mixed.is_some() {
return Err(self.error("Private mixed syntax requires explicit IDs."));
return Err(self.error("Mixed source8 requires explicit UUIDs."));
}
return Ok(Id::new_v4());
}
@@ -1230,11 +1302,26 @@ impl Parser<'_> {
message: "Expected UUID in 8-4-4-4-12 hexadecimal form.".into(),
});
}
Id::parse_str(&value).map_err(|_| Error {
let id = Id::parse_str(&value).map_err(|_| Error {
code: "invalid_source",
offset,
message: "Invalid UUID identity.".into(),
})
})?;
let bytes = value.len();
drop(value);
self.release_text(bytes);
Ok(id)
}
fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
if let Some(mixed) = &mut self.mixed {
mixed.own_text(bytes, span)?;
}
Ok(())
}
fn release_text(&mut self, bytes: usize) {
if let Some(mixed) = &mut self.mixed {
mixed.release_text(bytes);
}
}
fn fields(&mut self) -> Result<BTreeMap<String, Ast>, Error> {
self.take("{")?;
@@ -1242,6 +1329,13 @@ impl Parser<'_> {
while self.text() != "}" {
let offset = self.current().span.start;
let (key, value, _) = self.field()?;
self.own_text(
key.len(),
Span {
start: offset,
end: offset,
},
)?;
if fields.insert(key.clone(), value).is_some() {
return Err(Error {
code: "invalid_source",
@@ -1249,12 +1343,16 @@ impl Parser<'_> {
message: std::format!("Duplicate field '{key}'."),
});
}
let bytes = key.len();
drop(key);
self.release_text(bytes);
}
self.take("}")?;
Ok(fields)
}
fn field(&mut self) -> Result<(String, Ast, Span), Error> {
self.syntax_entry()?;
self.own_text(self.text().len(), self.current().span)?;
let key = self.text().to_owned();
if !matches!(
self.current().class,
@@ -1271,6 +1369,9 @@ impl Parser<'_> {
.span
.end;
self.take(";")?;
if let Some(mixed) = &mut self.mixed {
mixed.projection(1, Span { start, end })?;
}
Ok((key, value, Span { start, end }))
}
@@ -1296,7 +1397,6 @@ impl Parser<'_> {
self.value_depth += 1;
let result = self.value_inner();
self.value_depth -= 1;
#[cfg(test)]
if let Ok(ast) = &result {
let end = self
.token(self.cursor - 1)
@@ -1304,7 +1404,12 @@ impl Parser<'_> {
.span
.end;
if let Some(mixed) = &mut self.mixed {
mixed.value_ends.insert(ast.offset(), end);
let span = Span {
start: ast.offset(),
end,
};
mixed.value_end(span)?;
mixed.projection(1, span)?;
}
}
result
@@ -1312,7 +1417,6 @@ impl Parser<'_> {
fn value_inner(&mut self) -> Result<Ast, Error> {
let token = self.current();
let offset = token.span.start;
#[cfg(test)]
if let Some(mixed) = &self.mixed
&& (token.class == TokenClass::Number || matches!(self.text(), "-" | "+"))
{
@@ -1328,6 +1432,9 @@ impl Parser<'_> {
offset,
message: "Expected a UUID after uuid.".into(),
})?;
let bytes = value.len();
drop(value);
self.release_text(bytes);
Ok(Ast::HistoricalUuid(id, offset))
}
"@" => {
@@ -1368,6 +1475,7 @@ impl Parser<'_> {
Ok(Ast::Literal(value, offset))
}
TokenClass::Identifier | TokenClass::Keyword => {
self.own_text(self.text().len(), self.current().span)?;
let value = self.text().to_owned();
self.cursor += 1;
Ok(Ast::Word(value, offset))
@@ -1416,19 +1524,16 @@ impl Parser<'_> {
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
let mut header = if entry {
self.take("arcadia")?;
#[cfg(test)]
let mixed_mode = self.mixed.is_some();
#[cfg(not(test))]
let mixed_mode = false;
if if mixed_mode {
self.text() != "8"
} else {
!matches!(self.text(), "3" | "4" | "5" | "6" | "7")
!matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8")
} {
return Err(self.error(if mixed_mode {
"Private mixed syntax requires arcadia 8."
"Mixed source requires arcadia 8."
} else {
"Unsupported schema version: expected arcadia 3, 4, 5, 6 or 7."
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
}));
}
let schema_version = self.text().parse().expect("checked source version");
@@ -1436,7 +1541,6 @@ impl Parser<'_> {
self.take(";")?;
let start = self.current().span.start;
self.take("project")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.record(false, Span { start, end: start })?;
}
@@ -1481,7 +1585,6 @@ impl Parser<'_> {
let mut open: Vec<usize> = Vec::new();
loop {
let offset = self.current().span.start;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed
&& mixed.typed_header(self.cursor, open.is_empty())
{
@@ -1514,6 +1617,7 @@ impl Parser<'_> {
}
} else {
let (key, value, span) = self.field()?;
self.own_text(key.len(), span)?;
let declaration = &mut declarations[owner];
if declaration.fields.insert(key.clone(), value).is_some() {
return Err(Error {
@@ -1532,7 +1636,6 @@ impl Parser<'_> {
if !includes_allowed {
return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
}
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.include(Span {
start: offset,
@@ -1579,15 +1682,13 @@ impl Parser<'_> {
header = Some(entry_header);
continue;
}
#[cfg(test)]
if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) {
return Err(self.error("Use the short source8 analysis declaration word."));
}
self.syntax_entry()?;
let word = self.text().to_owned();
let kind = declaration_kind(&word)
let word = self.current().text(self.source);
let kind = declaration_kind(word)
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.record(
matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)),
@@ -1624,9 +1725,13 @@ impl Parser<'_> {
let body_start = self.current().span.end;
self.take("{")?;
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.lexical_owners.push(open.last().copied());
let span = Span {
start: offset,
end: body_start,
};
mixed.lexical_owner(open.last().copied(), span)?;
mixed.projection(7, span)?;
}
declarations.push(Declaration {
kind,
@@ -2152,6 +2257,10 @@ pub fn analyze(source: &str) -> Analysis {
#[path = "language_nesting_tests.rs"]
mod nesting_tests;
#[cfg(test)]
#[path = "language_ordinary_source8_tests.rs"]
mod ordinary_source8;
pub(crate) mod legacy {
//! Arcadia text: a readable, lossless representation of a valid OA project.
//!
@@ -1,14 +1,16 @@
//! Unresolved typed source8 syntax, staged for the complete source adapter.
//! This module does not admit a Project or enable public source8.
//! Private typed syntax and complete read-only mixed source8 resolution.
mod budget;
mod cursor;
mod declarations;
#[cfg(test)]
mod index;
mod lex;
mod lower;
pub(super) mod mixed;
mod syntax;
#[cfg(test)]
mod update;
#[cfg(test)]
mod write;
#[cfg(test)]
@@ -18,6 +20,7 @@ use super::Span;
use crate::analysis::{BinaryOp, UnaryOp};
use crate::analysis_numeric::Exact;
use budget::Budget;
pub use mixed::project::{CheckedMixed, check_mixed_bundle};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
@@ -104,6 +107,7 @@ pub enum Binding {
Expression(Expression),
}
#[cfg(test)]
pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
let tokens = lex::tokens(source, budget)?;
let mut cursor = cursor::Cursor::new(source, &tokens, budget);
@@ -114,3 +118,5 @@ pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
#[cfg(test)]
mod tests;
pub(crate) use mixed::discovery::MixedDiscovery;
@@ -38,6 +38,17 @@ pub struct Budget {
pub usage: Usage,
numeric: WorkMeter,
failed: Option<Error>,
text: Option<ProducerText>,
entries: Option<ProducerEntries>,
}
struct ProducerEntries {
carried: usize,
metadata: usize,
}
struct ProducerText {
carried: usize,
owned: usize,
temporary: usize,
}
impl Budget {
pub fn new(limits: Limits) -> Result<Self, Error> {
@@ -62,9 +73,155 @@ impl Budget {
.map_err(|e| error(Span::default(), e.to_string()))?,
),
failed: None,
text: None,
entries: None,
})
}
pub(super) fn enable_entry_producers(
&mut self,
carried: usize,
files: usize,
) -> Result<(), Error> {
assert!(self.entries.is_none());
self.check_producer_entries(carried, files, 0, Span::default())?;
self.entries = Some(ProducerEntries {
carried,
metadata: files,
});
Ok(())
}
fn check_producer_entries(
&mut self,
carried: usize,
metadata: usize,
delta: usize,
span: Span,
) -> Result<(), Error> {
self.active()?;
let total = carried
.checked_add(self.usage.entries)
.and_then(|n| n.checked_add(metadata))
.and_then(|n| n.checked_add(delta));
let message = match total {
None => Some("Source8 producer entry size overflow."),
Some(n) if n > self.limits.entries => Some("Source8 lowering entry limit exceeded."),
_ => None,
};
if let Some(message) = message {
let failure = error(span, message);
self.failed = Some(failure.clone());
return Err(failure);
}
Ok(())
}
pub(super) fn producer_entries(&self) -> Option<usize> {
self.entries.as_ref().map(|e| e.metadata)
}
pub(super) fn reserve_metadata(&mut self, delta: usize, span: Span) -> Result<(), Error> {
if let Some(e) = &self.entries {
self.check_producer_entries(e.carried, e.metadata, delta, span)?;
self.entries.as_mut().expect("enabled entries").metadata += delta;
}
Ok(())
}
pub(super) fn producer_entry_overflow(&mut self, span: Span) -> Error {
if let Some(failure) = &self.failed {
return failure.clone();
}
let failure = error(span, "Source8 producer entry size overflow.");
self.failed = Some(failure.clone());
failure
}
pub(super) fn enable_text_producers(&mut self, carried: usize) -> Result<(), Error> {
self.index_bytes(carried, Span::default())?;
assert!(self.text.is_none());
self.text = Some(ProducerText {
carried,
owned: 0,
temporary: 0,
});
Ok(())
}
pub(super) fn producer_text(&self) -> Option<(usize, usize)> {
self.text.as_ref().map(|t| (t.owned, t.temporary))
}
fn reserve_text(&mut self, bytes: usize, temporary: bool, span: Span) -> Result<(), Error> {
let Some(t) = &self.text else {
return Ok(());
};
let total = t
.carried
.checked_add(t.owned)
.and_then(|n| n.checked_add(t.temporary))
.and_then(|n| n.checked_add(bytes))
.ok_or_else(|| error(span, "Source8 producer text size overflow."))?;
self.index_bytes(total, span)?;
let t = self.text.as_mut().expect("enabled producer text");
if temporary {
t.temporary += bytes;
} else {
t.owned += bytes;
}
Ok(())
}
pub(super) fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
self.reserve_text(bytes, false, span)
}
pub(super) fn temporary_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
self.reserve_text(bytes, true, span)
}
pub(super) fn release_text(&mut self, bytes: usize) {
if let Some(t) = &mut self.text {
t.owned = t.owned.checked_sub(bytes).expect("owned text reservation");
}
}
pub(super) fn release_temporary_text(&mut self, bytes: usize) {
if let Some(t) = &mut self.text {
t.temporary = t
.temporary
.checked_sub(bytes)
.expect("temporary text reservation");
}
}
pub(super) fn quoted_text(&mut self, raw: &str, span: Span) -> Result<String, Error> {
if self.text.is_none() {
return serde_json::from_str(raw).map_err(|_| error(span, "Invalid quoted string."));
}
let payload = raw
.len()
.checked_sub(2)
.ok_or_else(|| error(span, "Invalid quoted text extent."))?;
self.temporary_text(payload, span)?;
let result = serde_json::from_str::<String>(raw);
let value = match result {
Ok(value) => value,
Err(_) => {
self.release_temporary_text(payload);
return Err(error(span, "Invalid quoted string."));
}
};
if value.len() > payload {
drop(value);
self.release_temporary_text(payload);
return Err(error(span, "Decoded text exceeds its payload reservation."));
}
// Keep the reservation continuous while moving decoded bytes into T.
// D <= P and the original checked aggregate bound prove both sums fit.
let t = self.text.as_mut().expect("enabled producer text");
let temporary = t
.temporary
.checked_sub(payload)
.expect("quoted text reservation");
let owned = t
.owned
.checked_add(value.len())
.expect("bounded decoded transfer");
t.temporary = temporary;
t.owned = owned;
Ok(value)
}
/// Immutable exact policy snapshot for private deliberate-snapshot continuation.
#[cfg(test)]
pub(super) fn limits(&self) -> Limits {
self.limits
}
@@ -105,6 +262,9 @@ impl Budget {
span,
"syntax entries",
)?;
if let Some(e) = &self.entries {
self.check_producer_entries(e.carried, e.metadata, 1, span)?;
}
self.usage.entries += 1;
Ok(())
}
@@ -135,6 +295,7 @@ impl Budget {
}
}
}
#[cfg(test)]
pub fn numeric_units(&self) -> u64 {
self.numeric.usage().numeric_units
}
@@ -42,6 +42,7 @@ impl Cursor<'_, '_> {
self.cursor += 1;
Ok(span)
}
#[cfg(test)]
pub(super) fn end(&self) -> Result<(), Error> {
if self.cursor != self.tokens.len() {
Err(error(
@@ -74,6 +75,7 @@ impl Cursor<'_, '_> {
{
return Err(error(span, "Expected a valid reference or parameter name."));
}
self.budget.own_text(self.text().len(), span)?;
let name = self.text().to_owned();
self.cursor += 1;
Ok(Reference { name, span })
@@ -87,8 +89,8 @@ impl Cursor<'_, '_> {
{
return Err(error(span, "Expected a quoted string."));
}
let value =
serde_json::from_str(self.text()).map_err(|_| error(span, "Invalid quoted string."))?;
let raw = &self.source[span.start..span.end];
let value = self.budget.quoted_text(raw, span)?;
self.cursor += 1;
Ok(value)
}
@@ -426,6 +426,9 @@ impl Cursor<'_, '_> {
));
}
let id = Id::parse_str(&spelling).map_err(|_| error(literal, "Invalid UUID identity."))?;
let bytes = spelling.len();
drop(spelling);
self.budget.release_text(bytes);
Ok(IdentitySyntax {
value: Some(id),
keyword: Some(keyword),
@@ -380,7 +380,7 @@ fn missing_id_metadata_is_stable_and_parameter_ids_are_owner_scoped() {
}
#[test]
fn inherited_version_root_and_short_word_contracts_do_not_enable_public8() {
fn inherited_version_root_and_short_word_contracts_do_not_enable_public_typed8() {
let source = "oa_check c \"C\" { predicate unbound; }";
for version in [1, 2, 3, 4, 5, 6, 7, 9] {
let mut budget = budget();
@@ -431,8 +431,12 @@ fn inherited_version_root_and_short_word_contracts_do_not_enable_public8() {
let contextual =
declaration("calculation unit \"C\" { input and : Number; result Number; body $and; }");
assert_eq!(contextual.header.alias.name, "unit");
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
for version in 3..=7 {
let ordinary = crate::language::parse_with_report("arcadia 8; project p \"P\" {}").unwrap();
assert!(ordinary.migration.is_none());
assert_eq!(ordinary.project.schema_version, 8);
assert_eq!(ordinary.project.alias, "p");
assert_eq!(ordinary.project.name, "P");
for version in 3..=8 {
assert!(
crate::language::parse(&format!(
"arcadia {version}; project p \"P\" {{ {source} }}"
@@ -1,5 +1,5 @@
//! Source8 significant-token adapter over the shared raw scanner.
use super::super::raw_tokens::{Class as RawClass, Scanner};
use super::super::raw_tokens::{Class as RawClass, Scanner, StringEvent};
use super::{Budget, Error, Span, error};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -33,7 +33,20 @@ pub fn tokens(source: &str, budget: &mut Budget) -> Result<Vec<Token>, Error> {
start: offset,
end: offset,
})?;
let token = scanner.next().expect("source remains");
let token = if budget.producer_text().is_some() {
scanner.next_with_strings(|event| {
match event {
StringEvent::Reserve { span, payload } => {
budget.temporary_text(payload, span)?
}
StringEvent::Released { payload } => budget.release_temporary_text(payload),
}
Ok::<(), Error>(())
})?
} else {
scanner.next()
}
.expect("source remains");
offset = token.span.end;
let class = match token.class {
RawClass::Word => Class::Word,
@@ -2,12 +2,15 @@
//! single complete-Project semantic admission after merging these additions.
use super::declarations::{self as d, Declaration, DeclarationKind};
use super::*;
#[cfg(test)]
use crate::Project;
use crate::analysis as a;
use crate::analysis_numeric::Dimension;
use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody};
use crate::{Element, ElementBody, Id, Kind, Layer, SharedBody};
use std::collections::{BTreeMap, BTreeSet};
/// Caller-assigned file key; spans are original offsets within that file.
#[cfg(test)]
pub(super) struct Unit {
pub file: usize,
pub declarations: Vec<Declaration>,
@@ -180,6 +183,7 @@ impl Tables {
Ok(())
}
#[cfg(test)]
fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> {
let at = Span::default();
self.insert(project.id, &project.alias, Category::Project, at, budget)?;
@@ -270,6 +274,7 @@ impl Tables {
/// Inputs must come from the same live Budget and stabilization pass. No scan,
/// exact conversion or reference charge occurs here. The ordinary skeleton is
/// not semantically admitted: its typed forward references may need additions.
#[cfg(test)]
pub(super) fn lower(
ordinary: &Project,
units: &[Unit],
@@ -96,7 +96,16 @@ fn staged_oa_and_sa_lowering_match_complete_independent_17_uuid_oracles() {
assert_eq!(actual, expected);
admit(&actual); // the complete model's one semantic admission
}
assert!(crate::language::parse(&ORDINARY_OA.replacen("arcadia 7", "arcadia 8", 1)).is_err());
for (ordinary_source, typed_source) in [(ORDINARY_OA, OA), (ORDINARY_SA, SA)] {
let current = ordinary_source.replacen("arcadia 7", "arcadia 8", 1);
let decoded = crate::language::parse_with_report(&current).unwrap();
assert!(decoded.migration.is_none());
assert_eq!(decoded.project, ordinary(ordinary_source));
assert!(
crate::language::parse(&format!("arcadia 8; project p \"P\" {{ {typed_source} }}"))
.is_err()
);
}
}
#[test]
@@ -13,7 +13,7 @@
//! value-end entries, projected value/field/header/declaration positions, lexical
//! owner slots and the file table. Checks do not recharge syntax or latch errors;
//! every failure must return immediately. Tokens themselves use the token bound.
//! This deliberately bounded staging policy is not calibrated public source8.
//! The public read checker selects both producers; editing remains separately staged.
use super::{
budget::{Budget, Limits},
@@ -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>>,
}
@@ -68,8 +72,11 @@ pub(in crate::language) struct State<'a> {
budget: &'a mut Budget,
model: &'a mut ModelCounts,
typed: Vec<declarations::Declaration>,
pub(in crate::language) value_ends: BTreeMap<usize, usize>,
pub(in crate::language) lexical_owners: Vec<Option<usize>>,
value_ends: BTreeMap<usize, usize>,
lexical_owners: Vec<Option<usize>>,
pending_projection: usize,
reserved_values: usize,
reserved_owners: usize,
}
fn ordinary_error(error: super::Error) -> Error {
@@ -93,6 +100,55 @@ fn lexeme(token: lex::Token) -> Lexeme {
}
impl State<'_> {
pub(in crate::language) fn projection(
&mut self,
slots: usize,
span: Span,
) -> Result<(), Error> {
if self.budget.producer_entries().is_some() {
self.pending_projection = self
.pending_projection
.checked_add(slots)
.ok_or_else(|| ordinary_error(self.budget.producer_entry_overflow(span)))?;
}
Ok(())
}
pub(in crate::language) fn value_end(&mut self, span: Span) -> Result<(), Error> {
let new_key = !self.value_ends.contains_key(&span.start);
self.budget
.reserve_metadata(usize::from(new_key), span)
.map_err(ordinary_error)?;
if new_key {
self.reserved_values += 1;
}
self.value_ends.insert(span.start, span.end);
Ok(())
}
pub(in crate::language) fn lexical_owner(
&mut self,
owner: Option<usize>,
span: Span,
) -> Result<(), Error> {
self.budget
.reserve_metadata(1, span)
.map_err(ordinary_error)?;
self.lexical_owners.push(owner);
self.reserved_owners += 1;
Ok(())
}
pub(in crate::language) fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
self.budget.own_text(bytes, span).map_err(ordinary_error)
}
pub(in crate::language) fn release_text(&mut self, bytes: usize) {
self.budget.release_text(bytes);
}
pub(in crate::language) fn quoted_text(
&mut self,
raw: &str,
span: Span,
) -> Result<String, Error> {
self.budget.quoted_text(raw, span).map_err(ordinary_error)
}
pub(in crate::language) fn token(&self, at: usize) -> Option<Lexeme> {
self.tokens.get(at).copied().map(lexeme)
}
@@ -204,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);
@@ -224,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> {
@@ -245,12 +306,13 @@ fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
}]
}
#[cfg(test)]
fn parse_with_profile(
bundle: &SourceBundle,
limits: ProjectLimits,
profile: Limits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
parse_impl(bundle, limits, profile, None)
parse_impl(bundle, limits, profile, None, false, false)
}
fn parse_impl<'s>(
@@ -258,6 +320,8 @@ fn parse_impl<'s>(
limits: ProjectLimits,
profile: Limits,
carried: Option<carried::CarriedGuard<'s>>,
text_producers: bool,
entry_producers: bool,
) -> Result<ParsedMixedSyntax<'s>, Vec<SourceDiagnostic>> {
let entry_source = bundle.files.get(&bundle.entry).map_or("", String::as_str);
let at_entry = |error| located(&bundle.entry, entry_source, ordinary_error(error));
@@ -279,6 +343,14 @@ fn parse_impl<'s>(
)));
}
let mut budget = Budget::new(profile).map_err(at_entry)?;
if text_producers {
let input = carried
.as_ref()
.expect("text mode requires carried authority");
budget
.enable_text_producers(input.slots().text)
.map_err(at_entry)?;
}
if let Some(input) = &carried {
input.check(&budget, 0, 0).map_err(at_entry)?;
}
@@ -291,7 +363,14 @@ fn parse_impl<'s>(
.index_bytes(bytes, Span::default())
.map_err(at_entry)?;
let mut metadata = bundle.files.len();
if let Some(input) = &carried {
if entry_producers {
let input = carried
.as_ref()
.expect("entry mode requires carried authority");
budget
.enable_entry_producers(input.slots().entries, metadata)
.map_err(at_entry)?;
} else if let Some(input) = &carried {
input.check(&budget, 0, metadata).map_err(at_entry)?;
} else {
budget
@@ -315,30 +394,31 @@ fn parse_impl<'s>(
.expect("traversal checked source");
let failure = |error| located(path, source, ordinary_error(error));
let tokens = lex::tokens(source, &mut budget).map_err(failure)?;
let mut parser = Parser {
let metadata_before = budget.producer_entries();
let (ordinary, state) = parse_unit(
source,
tokens: Vec::new(),
cursor: 0,
value_depth: 0,
limits: limits.model,
mixed: Some(State {
source,
tokens: &tokens,
budget: &mut budget,
model: &mut model,
typed: Vec::new(),
value_ends: BTreeMap::new(),
lexical_owners: Vec::new(),
}),
};
let ordinary = parser
.unit(path == &bundle.entry, true)
.map_err(|e| located(path, source, e))?;
let state = parser.mixed.take().expect("private parser state");
drop(parser);
&tokens,
&mut budget,
&mut model,
limits.model,
path == &bundle.entry,
)
.map_err(|e| located(path, source, e))?;
// Projected ordinary AST nodes/fields are bounded before constructing a
// second position representation. Count references as slots, not syntax.
let projection_slots = ordinary_position_slots(&ordinary);
let projection_slots = if entry_producers {
let slots = tokens
.len()
.checked_add(state.pending_projection)
.ok_or_else(|| failure(state.budget.producer_entry_overflow(Span::default())))?;
state
.budget
.reserve_metadata(slots, Span::default())
.map_err(failure)?;
state.pending_projection
} else {
ordinary_position_slots(&ordinary)
};
metadata = metadata
.checked_add(tokens.len())
.and_then(|n| n.checked_add(state.value_ends.len()))
@@ -355,12 +435,19 @@ fn parse_impl<'s>(
.lowering_entries(metadata, Span::default())
.map_err(failure)?;
}
let positions = language::edit_positions_from_tokens(
let positions = language::edit_positions_from_tokens_with_producers(
source,
&ordinary,
|at| lexeme(tokens[at]),
tokens.len(),
Some(&state.value_ends),
entry_producers.then_some(projection_slots),
|bytes, span| {
state
.budget
.own_text(bytes, span)
.map_err(|e| Box::new(language::diagnostic(source, ordinary_error(e))))
},
)
.map_err(|e| {
vec![SourceDiagnostic {
@@ -369,6 +456,23 @@ fn parse_impl<'s>(
related: Vec::new(),
}]
})?;
if let Some(before) = metadata_before {
let expected = tokens
.len()
.checked_add(state.value_ends.len())
.and_then(|n| n.checked_add(state.lexical_owners.len()))
.and_then(|n| n.checked_add(projection_slots))
.and_then(|n| n.checked_add(before));
if state.reserved_values != state.value_ends.len()
|| state.reserved_owners != state.lexical_owners.len()
|| expected != state.budget.producer_entries()
{
return Err(failure(super::error(
Span::default(),
"Source8 producer entry inventory mismatch.",
)));
}
}
for (raw, span) in &ordinary.includes {
traversal.include(path, raw, *span)?;
}
@@ -393,6 +497,15 @@ fn parse_impl<'s>(
}
traversal.finish()?;
budget.active().map_err(at_entry)?;
if budget.producer_entries().is_some_and(|m| m != metadata) {
return Err(at_entry(super::error(
Span::default(),
"Source8 producer entry inventory mismatch.",
)));
}
if let Some((_, temporary)) = budget.producer_text() {
assert_eq!(temporary, 0, "completed syntax has no temporary text");
}
if let Some(input) = &carried {
input
.check(&budget, budget.usage.entries, metadata)
@@ -436,3 +549,40 @@ fn ordinary_position_slots(unit: &SourceUnit) -> usize {
mod tests;
pub(super) mod project;
/// Shared grammar setup only. Complete parsing still retains/projects its state;
/// discovery drops that state after moving includes without a projection.
fn parse_unit<'a>(
source: &'a str,
tokens: &'a [lex::Token],
budget: &'a mut Budget,
model: &'a mut ModelCounts,
limits: ParseLimits,
entry: bool,
) -> Result<(SourceUnit, State<'a>), Error> {
let mut parser = Parser {
source,
tokens: Vec::new(),
cursor: 0,
value_depth: 0,
limits,
mixed: Some(State {
source,
tokens,
budget,
model,
typed: Vec::new(),
value_ends: BTreeMap::new(),
lexical_owners: Vec::new(),
pending_projection: 0,
reserved_values: 0,
reserved_owners: 0,
}),
};
let ordinary = parser.unit(entry, true)?;
let state = parser.mixed.take().expect("mixed parser state");
drop(parser);
Ok((ordinary, state))
}
pub(crate) mod discovery;
@@ -1,7 +1,9 @@
//! Fixture-constructible carried phase authority, not an emitted-output result.
//! Complete pending-backed authority and fixture-only carried snapshots.
//! Checks existing logical phase totals; does not meter every parser allocation.
use super::*;
use project::accounting::{Ledger, Slots};
#[cfg(test)]
use project::ordinary_index::rename::emission::PendingRename;
struct SnapshotPolicy {
limits: Limits,
@@ -9,16 +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),
}
@@ -27,7 +33,12 @@ pub(super) struct CarriedInput<'s> {
backing: SnapshotBacking<'s>,
}
enum GuardBacking<'s> {
Borrowed,
#[cfg(test)]
Pending(&'s PendingRename<'s>),
Borrowed {
_bundle: &'s SourceBundle,
},
#[cfg(test)]
Retained(&'s RetainedSnapshot),
}
@@ -37,15 +48,47 @@ pub(super) struct CarriedGuard<'s> {
backing: GuardBacking<'s>,
}
impl<'s> CarriedInput<'s> {
fn fresh(bundle: &'s SourceBundle, limits: ProjectLimits) -> Self {
Self {
backing: SnapshotBacking::Borrowed {
bundle,
policy: SnapshotPolicy {
limits: source_profile(limits),
project_limits: limits,
},
},
}
}
#[cfg(test)]
pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self {
Self {
backing: SnapshotBacking::Pending(pending),
}
}
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
match self.backing {
#[cfg(test)]
SnapshotBacking::Pending(pending) => {
let (limits, project_limits) = pending.carried_policy();
(
pending.carried_bundle(),
SnapshotPolicy {
limits,
project_limits,
},
CarriedGuard {
backing: GuardBacking::Pending(pending),
},
)
}
SnapshotBacking::Borrowed { bundle, policy } => (
bundle,
policy,
CarriedGuard {
backing: GuardBacking::Borrowed,
backing: GuardBacking::Borrowed { _bundle: bundle },
},
),
#[cfg(test)]
SnapshotBacking::Retained(owner) => (
&owner.bundle,
SnapshotPolicy {
@@ -62,7 +105,10 @@ impl<'s> CarriedInput<'s> {
impl CarriedGuard<'_> {
pub(super) fn slots(&self) -> Slots {
match &self.backing {
GuardBacking::Borrowed => Slots::default(),
#[cfg(test)]
GuardBacking::Pending(pending) => pending.carried_slots(),
GuardBacking::Borrowed { .. } => Slots::default(),
#[cfg(test)]
GuardBacking::Retained(owner) => owner.retained,
}
}
@@ -83,9 +129,48 @@ 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(bundle, policy.project_limits, policy.limits, Some(guard))
parse_impl(
bundle,
policy.project_limits,
policy.limits,
Some(guard),
false,
false,
)
}
/// Explicit stronger fixture route; the actual owner supplies all authority.
#[cfg(test)]
fn parse_text_producers(
input: CarriedInput<'_>,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let (bundle, policy, guard) = input.into_parts();
parse_impl(
bundle,
policy.project_limits,
policy.limits,
Some(guard),
true,
false,
)
}
/// Combined producer route, selected only by sealed pending or fixture authority.
pub(super) fn parse_entry_producers(
input: CarriedInput<'_>,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let (bundle, policy, guard) = input.into_parts();
parse_impl(
bundle,
policy.project_limits,
policy.limits,
Some(guard),
true,
true,
)
}
#[cfg(test)]
@@ -139,3 +224,18 @@ impl<'s> CarriedInput<'s> {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod text_producer;
#[cfg(test)]
mod entry_producer;
pub(super) fn parse_fresh(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
parse_entry_producers(CarriedInput::fresh(bundle, limits))
}
#[cfg(test)]
mod public_read;
@@ -0,0 +1,445 @@
//! Literal entry oracles, independently authored before the producer implementation.
use super::*;
const LIMIT: &str = "Source8 lowering entry limit exceeded.";
fn doc(body: &str) -> String {
format!(
"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
)
}
fn bundle(body: &str) -> SourceBundle {
SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([("main.arcadia".into(), doc(body))]),
}
}
fn chain() -> SourceBundle {
let mut b = bundle("include \"part.arcadia\";");
b.files.insert(
"part.arcadia".into(),
"include \"last.arcadia\";\r\n".into(),
);
b.files.insert("last.arcadia".into(), "//é\r\n".into());
b
}
fn held(bundle: SourceBundle, entries: usize) -> RetainedSnapshot {
RetainedSnapshot::fixture(
bundle,
SnapshotPolicy {
limits: Limits {
entries,
bytes: 4096,
..Limits::source8(ParseLimits::UNLIMITED)
},
project_limits: ProjectLimits::default(),
},
)
.unwrap()
}
fn failed(result: Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>) -> SourceDiagnostic {
let errors = match result {
Ok(_) => panic!("unexpected syntax success"),
Err(e) => e,
};
assert_eq!(errors.len(), 1);
errors.into_iter().next().unwrap()
}
fn diagnostic(d: &SourceDiagnostic, path: &str, start: usize, end: usize) {
assert_eq!(d.path, path);
assert_eq!(d.diagnostic.code, "invalid_source");
assert_eq!(d.diagnostic.message, LIMIT);
assert_eq!(d.diagnostic.span, Span { start, end });
assert!(d.related.is_empty());
}
#[test]
fn real_owner_syntax_and_resolver_boundaries_are_separate() {
for (b, c, u, m, syntax_cap, resolve_cap, refused_path) in [
(bundle(""), 1, 1, 11, 13, 17, "main.arcadia"),
(chain(), 3, 3, 19, 25, 29, "part.arcadia"),
] {
let owner = held(b.clone(), syntax_cap);
let syntax = parse_entry_producers(owner.input()).unwrap();
assert_eq!(owner.retained.entries, c);
assert_eq!(syntax.budget.usage.entries, u);
assert_eq!(syntax.budget.producer_entries(), Some(m));
assert_eq!(syntax.metadata, m);
assert_eq!(
project::text_producer_inventory(&syntax).unwrap().entries,
c + u + m
);
drop(syntax);
let low = held(b.clone(), syntax_cap - 1);
diagnostic(
&failed(parse_entry_producers(low.input())),
refused_path,
0,
1,
);
for cap in [resolve_cap, resolve_cap - 1] {
let owner = held(b.clone(), cap);
let syntax = parse_entry_producers(owner.input()).unwrap();
let usage = syntax.budget.usage;
let numeric = syntax.budget.numeric_units();
match project::resolve(syntax) {
Ok(result) => {
assert_eq!(cap, resolve_cap);
assert_eq!(result.phase_state().1.entries, c + u + m);
assert_eq!(result.phase_state().0.budget.usage, usage);
assert_eq!(result.phase_state().0.budget.numeric_units(), numeric);
let text =
project::resolve(parse_text_producers(owner.input()).unwrap()).unwrap();
assert_eq!(result.project(), text.project());
assert_eq!(result.warnings(), text.warnings());
assert_eq!(result.phase_state().1, text.phase_state().1);
}
Err(failure) => {
assert_eq!(cap, resolve_cap - 1);
assert_eq!(failure.diagnostics.len(), 1);
diagnostic(&failure.diagnostics[0], "main.arcadia", 11, 73);
}
}
assert_eq!(owner.bundle, b);
}
}
}
#[test]
fn owner_and_value_refuse_at_the_original_producer() {
for (body, cap, offset, complete) in [
(
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { }",
4,
75,
29,
),
(
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"\"; }",
7,
149,
37,
),
] {
let owner = held(bundle(body), cap);
diagnostic(
&failed(parse_entry_producers(owner.input())),
"main.arcadia",
offset,
offset + 1,
);
let owner = held(bundle(body), complete);
let syntax = parse_entry_producers(owner.input()).unwrap();
assert_eq!(
owner.retained.entries + syntax.budget.usage.entries + syntax.metadata,
complete
);
// The independent inventory walker has its own existing scratch peak.
let roomy = held(bundle(body), 4096);
let syntax = parse_entry_producers(roomy.input()).unwrap();
assert_eq!(
project::text_producer_inventory(&syntax).unwrap().entries,
complete
);
}
}
const NESTED: &str = "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\noa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { payload { items [@p, uuid \"00000000-0000-0000-0000-000000000001\", { flag true; }]; };\noa_activity b \"B\" id \"00000000-0000-0000-0000-000000000003\" { }\n}\nnode n id \"00000000-0000-0000-0000-000000000004\" { }\n}\n";
fn nested() -> SourceBundle {
SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([("main.arcadia".into(), NESTED.into())]),
}
}
#[test]
fn nested_inventory_and_prepaid_projection_quota() {
assert_eq!(NESTED.len(), 344);
let owner = held(nested(), 103);
let syntax = parse_entry_producers(owner.input()).unwrap();
assert_eq!(syntax.budget.usage.entries, 13);
assert_eq!(syntax.budget.producer_entries(), Some(89));
assert_eq!(syntax.metadata, 89);
assert_eq!(
owner.retained.entries + syntax.budget.usage.entries + syntax.metadata,
103
);
let roomy = held(nested(), 4096);
let inventory_syntax = parse_entry_producers(roomy.input()).unwrap();
assert_eq!(
project::text_producer_inventory(&inventory_syntax)
.unwrap()
.entries,
103
);
let file = &syntax.files[0];
assert_eq!(file.tokens.len(), 49);
assert_eq!(file.lexical_owners, [None, Some(0), None]);
assert_eq!(file.positions.len(), 3);
assert_eq!(file.positions[0].1.header.len(), 5);
assert_eq!(file.positions[2].1.header.len(), 4);
let language::EditKind::Record(fields) = &file.positions[0].1.fields.kind else {
panic!("body")
};
let payload = &fields["payload"].value;
assert_eq!(
payload.span,
Span {
start: 145,
end: 221
}
);
let language::EditKind::Record(fields) = &payload.kind else {
panic!("payload")
};
let items = &fields["items"].value;
assert_eq!(
items.span,
Span {
start: 153,
end: 218
}
);
let language::EditKind::List(items) = &items.kind else {
panic!("items")
};
assert_eq!(
items.iter().map(|v| v.span).collect::<Vec<_>>(),
[
Span {
start: 154,
end: 156
},
Span {
start: 158,
end: 201
},
Span {
start: 203,
end: 217
}
]
);
let language::EditKind::Record(fields) = &items[2].kind else {
panic!("member")
};
assert_eq!(
fields["flag"].value.span,
Span {
start: 210,
end: 214
}
);
// Injected prepaid quota errors, not valid alternate source inventories.
for (quota, message) in [
(29, "Source8 projection quota underflow."),
(31, "Source8 projection quota leftover."),
] {
let error = match language::edit_positions_from_tokens_with_producers(
file.source,
&file.ordinary,
|at| lexeme(file.tokens[at]),
file.tokens.len(),
None,
Some(quota),
|_, _| Ok(()),
) {
Err(error) => error,
Ok(_) => panic!("injected projection quota must refuse"),
};
assert_eq!(error.message, message);
}
assert_eq!(syntax.budget.producer_entries(), Some(89));
let low = held(nested(), 102);
diagnostic(
&failed(parse_entry_producers(low.input())),
"main.arcadia",
0,
1,
);
}
fn make_state<'a>(budget: &'a mut Budget, model: &'a mut ModelCounts) -> State<'a> {
State {
source: "",
tokens: &[],
budget,
model,
typed: Vec::new(),
value_ends: BTreeMap::new(),
lexical_owners: Vec::new(),
pending_projection: 0,
reserved_values: 0,
reserved_owners: 0,
}
}
fn internal_budget(cap: usize) -> Budget {
Budget::new(Limits {
entries: cap,
..Limits::source8(ParseLimits::UNLIMITED)
})
.unwrap()
}
#[test]
fn internal_reserve_and_mutate_helpers_leave_collections_unchanged() {
// Scalar fault fixtures exercise atomic mutation; these do not invent real owners.
let mut model = ModelCounts {
limits: ProjectLimits::default(),
graph: 0,
records: 0,
includes: 0,
};
let mut budget = internal_budget(3);
budget.enable_entry_producers(1, 1).unwrap();
let mut state = make_state(&mut budget, &mut model);
state.value_end(Span { start: 10, end: 11 }).unwrap();
state.value_end(Span { start: 10, end: 12 }).unwrap();
assert_eq!(state.value_ends, BTreeMap::from([(10, 12)]));
assert_eq!(state.reserved_values, 1);
assert_eq!(state.budget.producer_entries(), Some(2));
let error = state.value_end(Span { start: 20, end: 21 }).unwrap_err();
assert_eq!(error.message, LIMIT);
assert_eq!(error.offset, 20);
let repeated = state.value_end(Span { start: 10, end: 99 }).unwrap_err();
assert_eq!(repeated.offset, 20);
assert_eq!(repeated.message, LIMIT);
assert_eq!(state.value_ends, BTreeMap::from([(10, 12)]));
assert_eq!(state.budget.producer_entries(), Some(2));
let error = state
.lexical_owner(Some(0), Span { start: 30, end: 31 })
.unwrap_err();
assert_eq!(error.offset, 20); // Original latched refusal survives a later producer.
assert!(state.lexical_owners.is_empty());
assert_eq!(state.reserved_owners, 0);
let mut budget = internal_budget(3);
budget.enable_entry_producers(1, 1).unwrap();
let mut state = make_state(&mut budget, &mut model);
state.lexical_owner(None, Span::default()).unwrap();
assert!(
state
.lexical_owner(None, Span { start: 40, end: 41 })
.is_err()
);
assert_eq!(state.lexical_owners, [None]);
assert_eq!(state.reserved_owners, 1);
assert_eq!(state.budget.producer_entries(), Some(2));
}
#[test]
fn internal_overflow_latch_and_syntax_work_precedence() {
let at = Span { start: 9, end: 10 };
let mut budget = internal_budget(2);
budget.enable_entry_producers(1, 1).unwrap();
let e = budget.entry(at).unwrap_err();
assert_eq!(e.message, LIMIT);
assert_eq!(budget.producer_entry_overflow(Span::default()).span, at);
assert_eq!(budget.active().unwrap_err().message, LIMIT);
assert_eq!(budget.usage.entries, 0);
assert_eq!(budget.producer_entries(), Some(1));
assert_eq!(
budget
.reserve_metadata(0, Span::default())
.unwrap_err()
.span,
at
);
let mut budget = internal_budget(0);
budget.enable_entry_producers(0, 0).unwrap();
assert_eq!(
budget.entry(at).unwrap_err().message,
"Source8 syntax entries limit exceeded."
);
let mut budget = internal_budget(100);
budget.enable_entry_producers(1, 1).unwrap();
assert_eq!(
budget.reserve_metadata(usize::MAX, at).unwrap_err().message,
"Source8 producer entry size overflow."
);
assert_eq!(budget.producer_entries(), Some(1));
assert_eq!(budget.active().unwrap_err().span, at);
let mut budget = internal_budget(100);
assert!(budget.enable_entry_producers(usize::MAX, 1).is_err());
assert_eq!(budget.producer_entries(), None);
assert!(budget.active().is_err());
let mut budget = internal_budget(100);
budget.enable_entry_producers(0, 0).unwrap();
let mut model = ModelCounts {
limits: ProjectLimits::default(),
graph: 0,
records: 0,
includes: 0,
};
let mut state = make_state(&mut budget, &mut model);
state.pending_projection = usize::MAX;
assert_eq!(
state.projection(1, at).unwrap_err().message,
"Source8 producer entry size overflow."
);
assert_eq!(state.pending_projection, usize::MAX);
assert!(state.budget.active().is_err());
}
#[test]
fn explicit_zero_and_actual_owner_preserve_complete_oa_sa_and_old_routes() {
for (work, property) in [
("oa_activity", "oa_property"),
("sa_function", "sa_property"),
] {
let body = format!(
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000003\" {{ input x id \"00000000-0000-0000-0000-000000000004\" : Number; result Number; body $x; }}\n{property} v \"V\" id \"00000000-0000-0000-0000-000000000002\" {{ subject @w; type Number; base @c(x: 1); }}\n{work} w \"W\" id \"00000000-0000-0000-0000-000000000005\" {{}}"
);
let original = bundle(&body);
let owner = held(original.clone(), 4096);
for carried in [false, true] {
let seed = parse_text_producers(owner.input()).unwrap();
let input = if carried {
owner.input()
} else {
CarriedInput::borrowed(&owner.bundle, &seed)
};
let syntax = parse_entry_producers(input).unwrap();
let input = if carried {
owner.input()
} else {
CarriedInput::borrowed(&owner.bundle, &seed)
};
let prior = parse_text_producers(input).unwrap();
assert_eq!(syntax.budget.usage, prior.budget.usage);
assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units());
assert_eq!(syntax.metadata, prior.metadata);
assert_eq!(syntax.budget.producer_text(), prior.budget.producer_text());
assert_eq!(syntax.budget.producer_entries(), Some(syntax.metadata));
let result = project::resolve(syntax).unwrap();
let prior = project::resolve(prior).unwrap();
assert_eq!(result.project(), prior.project());
assert_eq!(result.warnings(), prior.warnings());
assert_eq!(result.phase_state().1, prior.phase_state().1);
}
assert_eq!(owner.bundle, original);
}
// Text-only and phase entry acceptance retain their old checkpoints.
for cap in [12, 11] {
let owner = held(bundle(""), cap);
let seed = parse_with_profile(
&owner.bundle,
owner.policy.project_limits,
owner.policy.limits,
)
.unwrap();
assert!(seed.budget.producer_entries().is_none());
let result = parse_entry_producers(CarriedInput::borrowed(&owner.bundle, &seed));
if cap == 12 {
assert_eq!(result.unwrap().metadata, 11);
} else {
diagnostic(&failed(result), "main.arcadia", 0, 1);
}
}
let low = held(
bundle("oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { }"),
4,
);
diagnostic(
&failed(parse_entry_producers(low.input())),
"main.arcadia",
75,
76,
);
diagnostic(
&failed(parse_text_producers(low.input())),
"main.arcadia",
0,
1,
);
diagnostic(&failed(parse_carried(low.input())), "main.arcadia", 0, 1);
}
@@ -0,0 +1,65 @@
//! Real public-constructor wiring; arbitrary fixture profiles are not public policy.
use super::*;
#[test]
fn public_read_fresh_constructor_keeps_zero_carry_both_modes_and_exact_policy() {
let bundle = SourceBundle {
entry: "main.arcadia".into(),
files: [("main.arcadia".into(), "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n".into())].into(),
};
let original = bundle.clone();
for (source_bytes, references, expected_bytes, expected_tokens, expected_references) in [
(4096, 17, 4096, 4096, 17),
(usize::MAX, usize::MAX, 64 * 1024 * 1024, 1_048_576, 500_000),
] {
let limits = ProjectLimits {
max_files: 7,
max_include_depth: 9,
max_include_edges: 11,
model: ParseLimits {
max_source_bytes: source_bytes,
max_graph_objects: 3,
max_records: 1,
max_step_references: references,
},
};
// This is the same production helper the exported checker calls.
let syntax = parse_fresh(&bundle, limits).unwrap();
let guard = syntax.carried.as_ref().expect("explicit carried mode");
assert!(matches!(guard.backing, GuardBacking::Borrowed { .. }));
assert_eq!(guard.slots(), Slots::default());
assert_eq!(syntax.budget.producer_text(), Some((8, 0)));
assert_eq!(syntax.budget.producer_entries(), Some(11));
assert_eq!(syntax.metadata, 11);
let profile = syntax.budget.limits();
assert_eq!(profile.bytes, expected_bytes);
assert_eq!(profile.tokens, expected_tokens);
assert_eq!(profile.entries, 262_144);
assert_eq!(profile.references, expected_references);
assert_eq!(profile.frames, 256);
assert_eq!(profile.numeric_units, 50_000_000);
assert_eq!(syntax.limits.max_files, 7);
assert_eq!(syntax.limits.max_include_depth, 9);
assert_eq!(syntax.limits.max_include_edges, 11);
assert_eq!(syntax.limits.model.max_source_bytes, source_bytes);
assert_eq!(syntax.limits.model.max_graph_objects, 3);
assert_eq!(syntax.limits.model.max_records, 1);
assert_eq!(syntax.limits.model.max_step_references, references);
let prior = parse(&bundle, limits).unwrap();
assert_eq!(syntax.budget.usage, prior.budget.usage);
assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units());
let resolved = project::resolve(syntax).unwrap();
let prior = project::resolve(prior).unwrap();
let checked = project::check_mixed_bundle(&bundle, limits).unwrap();
let mut expected = crate::Project::empty();
expected.id = Id::from_u128(1);
expected.alias = "p".into();
expected.name = "Project".into();
assert_eq!(resolved.project(), &expected);
assert_eq!(prior.project(), &expected);
assert_eq!(checked.project(), &expected);
assert!(checked.warnings().is_empty());
assert_eq!(resolved.warnings(), prior.warnings());
}
assert_eq!(bundle, original);
}
@@ -0,0 +1,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
);
}
}
@@ -0,0 +1,99 @@
//! A separate conservative discovery phase; no model admission or edit positions.
use super::*;
use crate::source_project::{IncludeLocation, SourceLocation, resolve_include};
pub(crate) struct MixedDiscovery {
budget: Budget,
model: ModelCounts,
failed: Option<Vec<SourceDiagnostic>>,
}
impl MixedDiscovery {
pub(crate) fn new(limits: ProjectLimits) -> Result<Self, String> {
let mut budget = Budget::new(source_profile(limits)).map_err(|e| e.message)?;
budget.enable_text_producers(0).map_err(|e| e.message)?;
budget.enable_entry_producers(0, 0).map_err(|e| e.message)?;
Ok(Self {
budget,
model: ModelCounts {
limits,
graph: 0,
records: 0,
includes: 0,
},
failed: None,
})
}
pub(crate) fn inspect(
&mut self,
path: &str,
source: &str,
entry: bool,
) -> Result<Vec<IncludeLocation>, Vec<SourceDiagnostic>> {
if let Some(failed) = &self.failed {
return Err(failed.clone());
}
let result = self.inspect_unit(path, source, entry);
if let Err(failed) = &result {
self.failed = Some(failed.clone());
}
result
}
fn inspect_unit(
&mut self,
path: &str,
source: &str,
entry: bool,
) -> Result<Vec<IncludeLocation>, Vec<SourceDiagnostic>> {
let failure = |error| located(path, source, ordinary_error(error));
// Conservative per-inspected-file reservation, before its scanner/parser.
self.budget
.reserve_metadata(1, Span::default())
.map_err(failure)?;
let tokens = lex::tokens(source, &mut self.budget).map_err(failure)?;
let limits = self.model.limits.model;
let (ordinary, state) = parse_unit(
source,
&tokens,
&mut self.budget,
&mut self.model,
limits,
entry,
)
.map_err(|e| located(path, source, e))?;
// No unit-drop refunds: remaining reservations are cumulative phase work.
// In-parser quoted/UUID/key temporary release behavior is unchanged.
drop(state);
let mut includes = Vec::new();
for (raw, span) in ordinary.includes {
let target = resolve_include(path, &raw).map_err(|message| {
let mut diagnostic = language::diagnostic(
source,
Error {
code: "invalid_path",
offset: span.start,
message,
},
);
diagnostic.span = span;
vec![SourceDiagnostic {
path: path.into(),
diagnostic,
related: Vec::new(),
}]
})?;
includes.push(IncludeLocation {
location: SourceLocation {
path: path.into(),
span,
},
target,
});
}
Ok(includes)
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,144 @@
use super::*;
const MAIN: &str = "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"part.arcadia\";\n}\n";
const PART: &str = "include \"last.arcadia\";\r\n";
const LAST: &str = "//é\r\n";
#[test]
fn mixed_discovery_accumulates_work_and_reservations_after_unit_drop() {
let limits = ProjectLimits {
model: ParseLimits {
max_source_bytes: 4096,
max_records: 1,
max_graph_objects: 3,
max_step_references: 17,
},
max_files: 7,
max_include_depth: 9,
max_include_edges: 11,
};
let mut phase = MixedDiscovery::new(limits).unwrap();
let profile = phase.budget.limits();
assert_eq!(
(
profile.bytes,
profile.tokens,
profile.entries,
profile.references,
profile.frames,
profile.numeric_units
),
(4096, 4096, 262144, 17, 256, 50_000_000)
);
assert_eq!(phase.budget.producer_text(), Some((0, 0)));
assert_eq!(phase.budget.producer_entries(), Some(0));
let includes = phase.inspect("main.arcadia", MAIN, true).unwrap();
assert_eq!(includes.len(), 1);
assert_eq!(includes[0].target, "part.arcadia");
drop(includes);
assert_eq!(phase.budget.producer_text(), Some((20, 0)));
assert_eq!(phase.budget.producer_entries(), Some(1));
assert_eq!(phase.budget.usage.entries, 2);
let includes = phase.inspect("part.arcadia", PART, false).unwrap();
assert_eq!(includes[0].target, "last.arcadia");
drop(includes);
assert_eq!(phase.budget.producer_text(), Some((32, 0)));
assert!(
phase
.inspect("last.arcadia", LAST, false)
.unwrap()
.is_empty()
);
assert_eq!(
(
phase.budget.usage.bytes,
phase.budget.usage.tokens,
phase.budget.usage.entries,
phase.budget.usage.references
),
(132, 31, 3, 0)
);
assert_eq!(phase.budget.producer_entries(), Some(3));
assert_eq!(phase.budget.producer_text(), Some((32, 0)));
assert_eq!(phase.budget.numeric_units(), 0);
assert_eq!(
(phase.model.records, phase.model.graph, phase.model.includes),
(1, 0, 2)
);
assert_eq!(phase.model.limits.max_files, 7);
assert_eq!(phase.model.limits.max_include_depth, 9);
assert_eq!(phase.model.limits.max_include_edges, 11);
assert_eq!(phase.model.limits.model.max_records, 1);
assert_eq!(phase.model.limits.model.max_graph_objects, 3);
}
#[test]
fn mixed_discovery_cumulative_entry_boundary_and_terminal_original_failure() {
// Deliberate private constructor fixture: fresh policy, never a widened owner.
for cap in [6, 5] {
let mut phase = MixedDiscovery::new(ProjectLimits::default()).unwrap();
phase.budget = Budget::new(Limits {
entries: cap,
bytes: 4096,
..source_profile(ProjectLimits::default())
})
.unwrap();
phase.budget.enable_text_producers(0).unwrap();
phase.budget.enable_entry_producers(0, 0).unwrap();
phase.inspect("main.arcadia", MAIN, true).unwrap();
phase.inspect("part.arcadia", PART, false).unwrap();
let last = phase.inspect("last.arcadia", LAST, false);
if cap == 6 {
assert!(last.unwrap().is_empty());
} else {
let errors = last.unwrap_err();
assert_eq!(errors[0].path, "last.arcadia");
assert_eq!(errors[0].diagnostic.span, Span { start: 0, end: 1 });
assert!(errors[0].diagnostic.message.contains("entry limit"));
let usage = phase.budget.usage;
assert_eq!(
phase
.inspect("later.arcadia", "malformed", false)
.unwrap_err(),
errors
);
assert_eq!(phase.budget.usage, usage);
}
}
let mut phase = MixedDiscovery::new(ProjectLimits::default()).unwrap();
let errors = phase
.inspect("broken.arcadia", "arcadia 8; project p \"P\" {}", true)
.unwrap_err();
assert_eq!(
errors[0].diagnostic.message,
"Mixed source8 requires explicit UUIDs."
);
let usage = phase.budget.usage;
assert_eq!(
phase.inspect("later.arcadia", LAST, false).unwrap_err(),
errors
);
assert_eq!(phase.budget.usage, usage);
}
#[test]
fn mixed_discovery_numeric_and_reference_work_is_not_restarted_per_unit() {
let mut phase = MixedDiscovery::new(ProjectLimits::default()).unwrap();
for (alias, number) in [("f", 2), ("g", 3)] {
let source = format!(
"calculation {alias} \"C\" id \"00000000-0000-0000-0000-{number:012}\" {{ input x id \"00000000-0000-0000-0000-000000000001\" : Number; result Number; body $x + 1; }}"
);
let before = phase.budget.numeric_units();
phase
.inspect(&format!("{alias}.arcadia"), &source, false)
.unwrap();
if number == 2 {
assert!(phase.budget.numeric_units() > 0);
} else {
assert_eq!(phase.budget.numeric_units(), 2 * before);
}
assert_eq!(phase.budget.usage.references, number - 1);
assert_eq!(phase.budget.producer_text().unwrap().1, 0);
}
assert_eq!(phase.model.records, 2);
}
@@ -1,5 +1,5 @@
//! Private complete-Project admission from the actual complete syntax witness.
//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy.
//! Complete-Project admission and opaque public read-only mixed source8 witness.
//! The separate typed index/edit chain remains test-only; no replacement policy.
use super::super::{
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
declarations as d, error, lower,
@@ -11,17 +11,119 @@ use crate::{Id, Kind, Project, RelationKind, analysis, language, validation};
use std::collections::BTreeMap;
pub(in crate::language::analysis_source) mod accounting;
use accounting::{Ledger, Slots, add};
#[cfg(test)]
pub(super) fn text_producer_inventory(syntax: &ParsedMixedSyntax<'_>) -> Result<Slots, Failure> {
let mut ledger = Ledger {
live: syntax
.carried
.as_ref()
.map_or_else(Slots::default, |g| g.slots()),
};
accounting::syntax(syntax, &mut ledger)?;
Ok(ledger.live)
}
#[cfg(test)]
pub(in crate::language::analysis_source) mod ordinary_index;
/// An admitted mixed source8 model borrowing its complete original source bundle.
///
/// This non-Clone result grants immutable validation queries only. It is not an
/// editable source index, stabilized output, or permission to format/save a typed
/// model through the ordinary source APIs. All input UUIDs must be explicit.
///
/// The source cannot be mutated while a later query uses the result:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let mut bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// bundle.files.clear();
/// assert_eq!(checked.project().alias, "p");
/// ```
///
/// Nor can the result outlive that source:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let checked = {
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap()
/// };
/// assert_eq!(checked.project().alias, "p");
/// ```
pub struct CheckedMixed<'s> {
resolved: ResolvedMixedProject<'s>,
}
impl CheckedMixed<'_> {
/// The complete admitted model, without mutation or ownership extraction.
pub fn project(&self) -> &Project {
self.resolved.project()
}
/// Actual located non-error validation diagnostics in validation order.
pub fn warnings(&self) -> &[SourceDiagnostic] {
self.resolved.warnings()
}
}
/// Validate a complete mixed ordinary/typed source8 bundle without changing bytes.
///
/// Every project, ordinary/typed declaration and typed parameter requires an
/// explicit UUID. Supplied files must be canonical and reachable from the entry.
/// Errors return no partial admitted model. There is no migration, ID generation,
/// filesystem discovery, formatting or editable-index construction.
///
/// Caller policy is applied across the entire bundle with the existing bounded
/// source8 syntax/storage/numeric policy; this is not an all-heap memory bound.
/// Ordinary parse/compile/edit/save APIs and CLI discovery remain separate.
///
/// ```
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// assert_eq!(checked.project().alias, "p");
/// assert!(checked.warnings().is_empty());
/// ```
pub fn check_mixed_bundle(
bundle: &crate::source_project::SourceBundle,
limits: crate::source_project::ProjectLimits,
) -> Result<CheckedMixed<'_>, Vec<SourceDiagnostic>> {
let syntax = super::carried::parse_fresh(bundle, limits)?;
let resolved = resolve(syntax).map_err(|failure| failure.diagnostics)?;
Ok(CheckedMixed { resolved })
}
/// Read-only authority: every component originates in the consumed syntax.
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
syntax: ParsedMixedSyntax<'s>,
project: Project,
warnings: Vec<SourceDiagnostic>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_limits: analysis::AdmissionLimits,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_usage: analysis::AdmissionUsage,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
expressions: Vec<lower::ExpressionSite>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
occurrences: Vec<lower::Occurrence>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
storage: Ledger,
}
impl ResolvedMixedProject<'_> {
@@ -40,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 {
@@ -61,6 +165,7 @@ impl<'s> ResolvedMixedProject<'s> {
})
}
}
#[cfg(test)]
impl IndexedMixedProject<'_> {
pub fn project(&self) -> &Project {
self.resolved.project()
@@ -97,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,
@@ -129,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| {
@@ -104,9 +104,14 @@ struct Walk<'a, 'b> {
budget: &'b Budget,
work: Vec<Item<'a>>,
lower: LowerStorage,
text_total: Option<usize>,
}
impl<'a> Walk<'a, '_> {
fn text(&mut self, text: &str, at: Span) -> Result<(), TypedError> {
if let Some(total) = &mut self.text_total {
*total = add(*total, text.len(), at)?;
return Ok(());
}
self.ledger
.reserve(self.budget, Slots::text(text.len()), at)
}
@@ -236,6 +241,19 @@ pub(super) fn syntax(
ledger: &mut Ledger,
) -> Result<LowerStorage, Failure> {
let at = Span::default();
let producer_text = syntax.budget.producer_text();
if let Some((text, temporary)) = producer_text {
if temporary != 0 {
return Err(Failure::typed(
syntax,
None,
error(at, "Unreleased syntax text temporary."),
));
}
ledger
.reserve(&syntax.budget, Slots::text(text), at)
.map_err(|e| Failure::typed(syntax, None, e))?;
}
ledger
.reserve(
&syntax.budget,
@@ -251,6 +269,7 @@ pub(super) fn syntax(
budget: &syntax.budget,
work: Vec::new(),
lower: LowerStorage::default(),
text_total: producer_text.map(|_| 0),
};
for (file, unit) in syntax.files.iter().enumerate() {
let mut count = || -> Result<(), TypedError> {
@@ -348,5 +367,12 @@ pub(super) fn syntax(
};
count().map_err(|e| Failure::typed(syntax, Some(file), e))?;
}
if walk.text_total != producer_text.map(|(text, _)| text) {
return Err(Failure::typed(
syntax,
None,
error(at, "Producer text differs from retained syntax inventory."),
));
}
Ok(walk.lower)
}
@@ -1,4 +1,5 @@
//! Owned output only. No parser, extraction or accepted-result conversion.
//! Owned unverified emission; consumed actual-output verification is a private child.
pub(in crate::language::analysis_source) mod acceptance;
use super::*;
use crate::language::analysis_source::budget::Limits;
use crate::source_project::ProjectLimits;
@@ -0,0 +1,175 @@
//! Actual-byte verification and owning result; no installation or mutable extraction.
use super::*;
use crate::language::analysis_source::budget::Usage;
use crate::language::analysis_source::mixed::{carried, project};
use crate::source_project::SourceDiagnostic;
pub(in crate::language::analysis_source) struct AcceptedRename {
bundle: SourceBundle,
project: Project,
warnings: Vec<SourceDiagnostic>,
limits: Limits,
project_limits: ProjectLimits,
retained: Slots,
verification_usage: Usage,
verification_numeric_units: u64,
}
impl AcceptedRename {
pub fn bundle(&self) -> &SourceBundle {
&self.bundle
}
pub fn project(&self) -> &Project {
&self.project
}
pub fn warnings(&self) -> &[SourceDiagnostic] {
&self.warnings
}
pub fn policy(&self) -> (Limits, ProjectLimits) {
(self.limits, self.project_limits)
}
pub fn retained(&self) -> Slots {
self.retained
}
pub fn verification_work(&self) -> (Usage, u64) {
(self.verification_usage, self.verification_numeric_units)
}
}
impl PendingRename<'_> {
pub(in crate::language::analysis_source::mixed) fn carried_bundle(&self) -> &SourceBundle {
&self.bundle
}
pub(in crate::language::analysis_source::mixed) fn carried_policy(
&self,
) -> (Limits, ProjectLimits) {
(self.limits, self.project_limits)
}
pub(in crate::language::analysis_source::mixed) fn carried_slots(&self) -> Slots {
self.retained
}
pub fn verify(self) -> Result<AcceptedRename, Failure> {
let (project, warnings, retained, verification_usage, verification_numeric_units) = {
let syntax = carried::parse_entry_producers(carried::CarriedInput::from_pending(&self))
.map_err(|diagnostics| Failure { diagnostics })?;
let mut resolved = project::resolve(syntax)?;
let equal = equal_target(&mut resolved, self.desired)?;
if !equal {
return Err(Failure::typed(
&resolved.syntax,
None,
error(
Span::default(),
"Verified output differs from the complete rename target.",
),
));
}
let retained = warning_inventory(&resolved.warnings)
.and_then(|warnings| self.retained.add(warnings, Span::default()))
.and_then(|retained| {
check_destination(&resolved.syntax.budget, resolved.storage.live, retained)?;
Ok(retained)
})
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
let usage = resolved.syntax.budget.usage;
let numeric = resolved.syntax.budget.numeric_units();
let project::ResolvedMixedProject {
syntax,
project,
warnings,
expressions,
occurrences,
static_limits: _,
static_usage: _,
storage: _,
} = resolved;
// The full live ledger remains intact through the checks above.
// Only scalar destination accounting escapes these actual drops.
drop(expressions);
drop(occurrences);
drop(syntax);
(project, warnings, retained, usage, numeric)
};
Ok(AcceptedRename {
bundle: self.bundle,
project,
warnings,
limits: self.limits,
project_limits: self.project_limits,
retained,
verification_usage,
verification_numeric_units,
})
}
}
// Every actual Expr root is lowered into this arena. The old pending expression
// count cannot authenticate parsed topology. Keep this boundary independently
// testable without changing the inherited policy or instrumenting comparison.
fn equal_target(
resolved: &mut project::ResolvedMixedProject<'_>,
desired: &Project,
) -> Result<bool, Failure> {
let frontier = Slots::entries(resolved.expressions.len());
resolved
.storage
.reserve(&resolved.syntax.budget, frontier, Span::default())
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
let equal = resolved.project.eq(desired);
resolved.storage.release(frontier);
Ok(equal)
}
fn check_destination(budget: &Budget, live: Slots, retained: Slots) -> Result<(), TypedError> {
let at = Span::default();
if retained.entries > live.entries
|| retained.occurrences > live.occurrences
|| retained.text > live.text
{
return Err(error(
at,
"Accepted inventory exceeds live verification storage.",
));
}
budget.lowering_entries(retained.entries, at)?;
budget.lowering_occurrences(retained.occurrences, at)?;
budget.index_bytes(retained.text, at)
}
fn warning_inventory(warnings: &[SourceDiagnostic]) -> Result<Slots, TypedError> {
let mut inventory = Slots::default();
for SourceDiagnostic {
path,
diagnostic,
related,
} in warnings
{
let language::Diagnostic {
severity,
code,
object_id: _,
field,
line: _,
column: _,
message,
span: _,
} = diagnostic;
if !related.is_empty() || *severity == validation::Severity::Error {
return Err(error(Span::default(), "Unexpected accepted warning shape."));
}
let text = [
path.len(),
code.len(),
field.as_ref().map_or(0, String::len),
message.len(),
]
.into_iter()
.try_fold(0, |n, k| add(n, k, Span::default()))?;
inventory = inventory.add(
Slots {
entries: 1,
occurrences: 0,
text,
},
Span::default(),
)?;
}
Ok(inventory)
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,845 @@
use super::*;
use crate::language::{ParseLimits, analysis_source::mixed::parse_with_profile};
use crate::{Layer, analysis as a};
use project::tests::{id, rich_bundle, rich_model, single};
const MISMATCH: &str = "Verified output differs from the complete rename target.";
const NEXT: &str =
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
const UNNAMED: &str =
"arcadia 8;\nproject next \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
fn profile(entries: usize, bytes: usize) -> Limits {
Limits {
entries,
bytes,
..Limits::source8(ParseLimits::UNLIMITED)
}
}
fn limited(b: &SourceBundle, limits: Limits, policy: ProjectLimits) -> AuthoredMixedProject<'_> {
project::resolve(parse_with_profile(b, policy, limits).unwrap())
.unwrap()
.index()
.unwrap()
.index_ordinary()
.unwrap()
}
fn authored(b: &SourceBundle) -> AuthoredMixedProject<'_> {
limited(b, profile(100_000, 1_000_000), ProjectLimits::default())
}
fn expected(name: &str) -> Project {
Project {
schema_version: 8,
id: id(1),
alias: "next".into(),
name: name.into(),
description: String::new(),
definitions: BTreeMap::new(),
elements: BTreeMap::new(),
relations: BTreeMap::new(),
diagrams: BTreeMap::new(),
artifact_links: BTreeMap::new(),
traces: BTreeMap::new(),
}
}
fn failure<T>(result: Result<T, Failure>) -> Failure {
match result {
Ok(_) => panic!("unexpected verification success"),
Err(e) => e,
}
}
fn diagnostic(failure: &Failure, message: &str, start: usize, end: usize) {
assert_eq!(failure.diagnostics.len(), 1);
let d = &failure.diagnostics[0];
assert_eq!(d.path, "main.arcadia");
assert_eq!(d.diagnostic.code, "invalid_source");
assert_eq!(d.diagnostic.message, message);
assert_eq!(d.diagnostic.span, Span { start, end });
assert!(d.related.is_empty());
}
fn same_policy(accepted: &AcceptedRename, limits: Limits, policy: ProjectLimits) {
let (a, b) = accepted.policy();
assert_eq!(
[a.bytes, a.tokens, a.entries, a.references, a.frames],
[
limits.bytes,
limits.tokens,
limits.entries,
limits.references,
limits.frames
]
);
assert_eq!(a.numeric_units, limits.numeric_units);
assert_eq!(
[
b.max_files,
b.max_include_depth,
b.max_include_edges,
b.model.max_source_bytes,
b.model.max_graph_objects,
b.model.max_records,
b.model.max_step_references
],
[
policy.max_files,
policy.max_include_depth,
policy.max_include_edges,
policy.model.max_source_bytes,
policy.model.max_graph_objects,
policy.model.max_records,
policy.model.max_step_references
]
);
}
#[test]
fn actual_minimal_bytes_full_model_and_late_decode_boundary() {
for bytes in [152, 151] {
let b = single("");
let before = b.clone();
let limits = profile(17, bytes);
let source = limited(&b, limits, ProjectLimits::default());
assert_eq!(source.indexed.resolved.syntax.budget.usage.bytes, 78);
let desired = expected("Project");
let pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
let result = pending.verify();
if bytes == 152 {
let accepted = result.unwrap();
assert_eq!(
accepted.bundle(),
&SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([("main.arcadia".into(), NEXT.into())])
}
);
assert_eq!(accepted.project(), &expected("Project"));
assert!(accepted.warnings().is_empty());
assert_eq!(
accepted.retained(),
Slots {
entries: 1,
occurrences: 0,
text: 105
}
);
assert_eq!(
accepted.verification_work(),
(
Usage {
bytes: 81,
tokens: 19,
entries: 1,
references: 0,
deepest_frame: 0
},
0
)
);
same_policy(&accepted, limits, ProjectLimits::default());
} else {
let e = failure(result);
diagnostic(&e, "Source8 index/output byte limit exceeded.", 37, 38);
assert_eq!(
(
e.diagnostics[0].diagnostic.line,
e.diagnostics[0].diagnostic.column
),
(2, 27)
);
}
assert_eq!(b, before);
assert_eq!(desired, expected("Project"));
}
}
fn warning() -> SourceDiagnostic {
SourceDiagnostic {
path: "main.arcadia".into(),
related: Vec::new(),
diagnostic: language::Diagnostic {
severity: validation::Severity::Warning,
code: "unnamed".into(),
object_id: Some(id(1)),
field: Some("name".into()),
message: "Give the project a descriptive name.".into(),
span: Span { start: 11, end: 69 },
line: 2,
column: 1,
},
}
}
#[test]
fn actual_warning_mapping_boundary_and_final_transfer() {
for bytes in [161, 160] {
let mut b = single("");
b.files.insert(
"main.arcadia".into(),
"arcadia 8;\nproject p \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n"
.into(),
);
assert_eq!(b.files["main.arcadia"].len(), 71);
let limits = profile(18, bytes);
let source = limited(&b, limits, ProjectLimits::default());
assert_eq!(
source.warnings()[0].diagnostic.span,
Span { start: 11, end: 66 }
);
let desired = expected("");
let pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
let result = pending.verify();
if bytes == 161 {
let accepted = result.unwrap();
assert_eq!(accepted.bundle().files["main.arcadia"], UNNAMED);
assert_eq!(accepted.project(), &expected(""));
assert_eq!(accepted.warnings(), [warning()]);
assert_eq!(
accepted.retained(),
Slots {
entries: 2,
occurrences: 0,
text: 157
}
);
assert_eq!(
accepted.verification_work(),
(
Usage {
bytes: 74,
tokens: 19,
entries: 1,
references: 0,
deepest_frame: 0
},
0
)
);
same_policy(&accepted, limits, ProjectLimits::default());
} else {
let e = failure(result);
diagnostic(&e, "Source8 index/output byte limit exceeded.", 11, 69);
assert_eq!(
(
e.diagnostics[0].diagnostic.line,
e.diagnostics[0].diagnostic.column
),
(2, 1)
);
}
}
}
#[test]
fn owning_result_survives_all_inputs_and_preserves_every_policy_field() {
let limits = Limits {
bytes: 2048,
tokens: 101,
entries: 99,
references: 31,
frames: 17,
numeric_units: 8192,
};
let policy = ProjectLimits {
max_files: 7,
max_include_depth: 3,
max_include_edges: 5,
model: ParseLimits {
max_source_bytes: 3000,
max_graph_objects: 67,
max_records: 123,
max_step_references: 37,
},
};
let accepted = {
let b = single("");
let desired = expected("Project");
let pending = limited(&b, limits, policy)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
// A short pending borrow carries a longer independently owned target borrow.
{
let syntax =
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
.unwrap();
assert_eq!(syntax.bundle, &pending.bundle);
drop(syntax);
}
pending.verify().unwrap()
};
assert_eq!(accepted.bundle().files["main.arcadia"], NEXT);
assert_eq!(accepted.project(), &expected("Project"));
assert!(accepted.warnings().is_empty());
same_policy(&accepted, limits, policy);
}
#[test]
fn equal_byte_wrong_models_malformed_and_admission_failure_are_distinct() {
let b = single("");
let desired = expected("Project");
for (start, end, replacement) in [(19, 23, "nope"), (25, 32, "Changed"), (8, 9, "?")] {
let mut pending = authored(&b)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
pending
.bundle
.files
.get_mut("main.arcadia")
.unwrap()
.replace_range(start..end, replacement);
assert_eq!(pending.bundle.files["main.arcadia"].len(), 81);
if start != 8 {
let actual = project::resolve(
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
.unwrap(),
)
.unwrap();
assert_ne!(actual.project(), &desired); // Wrong output really is admissible.
drop(actual);
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
} else {
let e = failure(pending.verify());
assert_eq!(e.diagnostics[0].diagnostic.message, "Invalid source token.");
}
}
let b = single(&format!(
"oa_activity a \"A\" id \"{}\" {{ parent @b; }}\noa_activity b \"B\" id \"{}\" {{}}",
id(2),
id(3)
));
let source = authored(&b);
let mut desired = source.project().clone();
desired.alias = "next".into();
let mut pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
let text = pending.bundle.files.get_mut("main.arcadia").unwrap();
let at = text.find("parent @b").unwrap() + 8;
text.replace_range(at..at + 1, "a");
let syntax =
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap();
drop(syntax); // This corruption is grammar-valid but model-invalid.
let e = failure(pending.verify());
assert!(
e.diagnostics
.iter()
.any(|d| d.diagnostic.code == "containment_cycle")
);
}
#[test]
fn actual_expression_arena_controls_equality_reservation() {
let b = single(&format!(
"calculation c \"C\" id \"{}\" {{ result Number; body 10000; }}",
id(2)
));
let source = limited(&b, profile(4096, 4096), ProjectLimits::default());
let mut desired = source.project().clone();
desired.alias = "next".into();
let mut pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
assert_eq!(pending.expression_nodes, 1);
assert_eq!(
pending.retained,
Slots {
entries: 1,
occurrences: 0,
text: 195
}
);
pending
.bundle
.files
.get_mut("main.arcadia")
.unwrap()
.replace_range(160..165, "1 + 2");
let mut actual = project::resolve(
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap(),
)
.unwrap();
assert_eq!(actual.expressions.len(), 3);
let before = actual.storage.live;
assert!(!equal_target(&mut actual, &desired).unwrap());
assert_eq!(actual.storage.live, before);
// Labeled ledger fault: two free slots would fit old E1, but not actual E3.
// This is not an end-to-end late cap or a policy mutation.
actual.storage.live.entries = 4094;
let e = equal_target(&mut actual, &desired).unwrap_err();
diagnostic(&e, "Source8 lowering entry limit exceeded.", 0, 0);
assert_eq!(actual.storage.live.entries, 4094);
drop(actual);
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
}
#[test]
fn internal_warning_shape_and_destination_inventory_faults() {
assert_eq!(
warning_inventory(&[warning()]).unwrap(),
Slots {
entries: 1,
occurrences: 0,
text: 59
}
);
let mut bad = warning();
bad.related.push(crate::source_project::SourceLocation {
path: "extra".into(),
span: Span::default(),
});
assert_eq!(
warning_inventory(&[bad]).unwrap_err().message,
"Unexpected accepted warning shape."
);
let mut bad = warning();
bad.diagnostic.severity = validation::Severity::Error;
assert!(warning_inventory(&[bad]).is_err());
let budget = Budget::new(profile(100, 4096)).unwrap();
let live = Slots {
entries: 3,
occurrences: 2,
text: 200,
};
for retained in [
Slots { entries: 4, ..live },
Slots {
occurrences: 3,
..live
},
Slots { text: 201, ..live },
] {
assert_eq!(
check_destination(&budget, live, retained)
.unwrap_err()
.message,
"Accepted inventory exceeds live verification storage."
);
}
assert!(
Slots::text(usize::MAX)
.add(Slots::text(1), Span::default())
.is_err()
);
assert!(
Slots::entries(usize::MAX)
.add(Slots::entries(1), Span::default())
.is_err()
);
}
fn rich_desired(layer: Layer) -> Project {
let mut p = rich_model(layer);
p.alias = "w".into();
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
let d = p.diagrams.get_mut(&id(5)).unwrap();
d.alias = "z".into();
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
for (n, name) in [(10, "g"), (11, "f")] {
let def = p.definitions.get_mut(&id(n)).unwrap();
def.alias = name.into();
let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else {
panic!("calculation")
};
c.parameters[0].name = "y".into();
c.parameters[1].name = "x".into();
}
if layer == Layer::Sa {
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
}
p
}
// Independently authored entire expected files: no edit iterator or output text
// contributes to these strings. The LF/CRLF transform applies to whole literals.
const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n";
const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n";
fn rich_expected(layer: Layer) -> SourceBundle {
let (property, work, diagram, extra) = if layer == Layer::Oa {
("oa_property", "oa_activity", "", "")
} else {
(
"sa_property",
"sa_function",
"layer Sa;",
"sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}",
)
};
SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([
("a.arcadia".into(), RICH_A.into()),
("b.arcadia".into(), RICH_B.into()),
(
"main.arcadia".into(),
format!(
"arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n"
),
),
]),
}
}
#[test]
fn rich_oa_sa_swaps_unicode_crlf_complete_actual_bytes_and_models() {
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
for unicode in [false, true] {
let mut b = rich_bundle(layer);
let mut expected = rich_expected(layer);
if unicode {
for text in b.files.values_mut() {
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
}
for text in expected.files.values_mut() {
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
}
}
let before = b.clone();
let desired = rich_desired(layer);
let accepted = authored(&b)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap()
.verify()
.unwrap();
assert_eq!(accepted.bundle(), &expected);
assert_eq!(accepted.project(), &rich_desired(layer));
let (span, line) = if unicode {
(
Span {
start: 351,
end: 416,
},
7,
)
} else {
(
Span {
start: 338,
end: 403,
},
6,
)
};
assert_eq!(
accepted.warnings(),
leaf_work_warnings("main.arcadia", span, line)
);
assert_eq!(
accepted.retained(),
Slots {
entries: 5,
occurrences: 0,
text: if unicode {
unicode_bytes + 42 + 227
} else {
bytes + 42 + 227
}
}
);
assert_eq!(b, before);
assert_eq!(desired, rich_desired(layer));
}
}
}
fn leaf_work_warnings(path: &str, span: Span, line: usize) -> [SourceDiagnostic; 2] {
// Allocation validation precedes advisory coverage. Both locate the complete
// ordinary Work declaration; neither fixture supplies allocation or context.
[
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
].map(|(code, field, message)| SourceDiagnostic {
path: path.into(), related: Vec::new(),
diagnostic: language::Diagnostic {
severity: validation::Severity::Warning, code: code.into(),
object_id: Some(id(2)), field: field.map(str::to_owned),
message: message.into(), span, line, column: 1,
},
})
}
fn calculation(p: &mut Project) -> &mut a::CalculationDefinition {
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(2)).unwrap().body else {
panic!("calculation")
};
c
}
#[test]
fn scoped_small_stack_actual_deep_wide_verification_and_drop() {
for (expression, result, nodes) in [
(
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
"Number",
128,
),
(
format!("[{}]", vec!["1"; 128].join(", ")),
"List(Number)",
129,
),
] {
let body = format!(
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
id(2)
);
let b = single(&body);
let before = b.clone();
let expected_bytes = format!(
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
);
for mismatch in [false, true] {
let source = authored(&b);
let mut desired = source.project().clone();
desired.alias = "next".into();
let mut target = desired.clone();
if result == "List(Number)" {
let a::Binding::Expression(a::Expr::List(items)) =
&mut calculation(&mut target).body
else {
panic!("list")
};
items.pop(); // Actual left remains wide; target-only shape differs.
} else {
target.name = "Different".into();
}
let mut pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
assert_eq!(pending.expression_nodes, nodes);
if mismatch {
pending.desired = &target;
} // Labeled target-only fault after valid emission.
std::thread::scope(|scope| {
let expected = &expected_bytes;
let desired = &desired;
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn_scoped(scope, move || {
let result = pending.verify();
if mismatch {
diagnostic(&failure(result), MISMATCH, 0, 0);
} else {
let accepted = result.unwrap();
assert_eq!(&accepted.bundle().files["main.arcadia"], expected);
assert_eq!(accepted.project(), desired);
drop(accepted);
}
})
.unwrap()
.join()
.unwrap();
});
assert_eq!(b, before);
}
}
// Genuine inherited151 profile reaches a late parser refusal inside worker.
let b = single("");
let desired = expected("Project");
let pending = limited(&b, profile(17, 151), ProjectLimits::default())
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
std::thread::scope(|scope| {
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn_scoped(scope, move || {
diagnostic(
&failure(pending.verify()),
"Source8 index/output byte limit exceeded.",
37,
38,
);
})
.unwrap()
.join()
.unwrap();
});
}
#[test]
fn zero_actual_expression_rejects_large_target_only_variants() {
let b = single(&format!(
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
id(2)
));
let mut deep = a::Expr::Boolean(true);
for _ in 0..2048 {
deep = a::Expr::Unary {
op: a::UnaryOp::Not,
operand: Box::new(deep),
};
}
for expression in [deep, a::Expr::List(vec![a::Expr::Boolean(true); 4096])] {
let source = authored(&b);
let mut desired = source.project().clone();
desired.alias = "next".into();
let mut damaged = desired.clone();
calculation(&mut damaged).body = a::Binding::Expression(expression);
let mut pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap();
assert_eq!(pending.expression_nodes, 0);
pending.desired = &damaged;
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
}
}
#[test]
fn signed_zero_geometry_scalar_and_historical_equality_moves_actual_model() {
let b = rich_bundle(Layer::Oa);
let mut desired = rich_model(Layer::Oa);
desired.alias = "next".into();
desired
.diagrams
.get_mut(&id(5))
.unwrap()
.nodes
.get_mut(&id(4))
.unwrap()
.position
.x = -0.0;
let accepted = authored(&b)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap()
.verify()
.unwrap();
assert_eq!(accepted.project(), &desired);
assert_eq!(
accepted.project().diagrams[&id(5)].nodes[&id(4)]
.position
.x
.to_bits(),
0.0f32.to_bits()
);
let b = single(&format!(
r#"
oa_data_type a "A" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
sa_data_type b "B" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
trace t "T" id "{}" {{ oa @a; sa @b; kind Item; baseline {{
oa {{ name "A"; description ""; }}; sa {{ name "B"; description ""; }};
semantic {{ facets [{{ Data {{ RealMinimum {{ oa {{ Value 0.0; }}; sa {{ Value 0.0; }}; }}; }}; }}]; }};
}}; }}
"#,
id(2),
id(3),
id(4)
));
let source = authored(&b);
let mut desired = source.project().clone();
desired.alias = "next".into();
for e in desired.elements.values_mut() {
let body = match &mut e.body {
crate::ElementBody::Oa(crate::OaBody::Shared(b))
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
_ => panic!("data"),
};
let crate::SharedBody::DataType {
definition,
default,
..
} = body
else {
panic!("data")
};
let crate::DataTypeDefinition::Real { minimum, .. } = definition else {
panic!("real")
};
*minimum = Some(-0.0);
*default = Some(crate::DataValue::Real(-0.0));
}
let facet = &mut desired
.traces
.get_mut(&id(4))
.unwrap()
.baseline
.as_mut()
.unwrap()
.semantic
.as_mut()
.unwrap()
.facets[0];
let crate::semantic::FacetReview::Data(crate::semantic::DataReview::RealMinimum(review)) =
facet
else {
panic!("historical")
};
review.oa = crate::semantic::Observed::Value(Some(-0.0));
review.sa = crate::semantic::Observed::Value(Some(-0.0));
let accepted = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap()
.verify()
.unwrap();
assert_eq!(accepted.project(), &desired);
for e in accepted.project().elements.values() {
let body = match &e.body {
crate::ElementBody::Oa(crate::OaBody::Shared(b))
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
_ => panic!("data"),
};
let crate::SharedBody::DataType {
definition:
crate::DataTypeDefinition::Real {
minimum: Some(minimum),
..
},
default: Some(crate::DataValue::Real(value)),
..
} = body
else {
panic!("real")
};
assert_eq!(minimum.to_bits(), 0.0f64.to_bits());
assert_eq!(value.to_bits(), 0.0f64.to_bits());
}
}
#[test]
fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() {
let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()),
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
])};
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()),
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
])};
let source = authored(&bundle);
let mut desired = source.project().clone();
desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into();
desired.elements.get_mut(&id(2)).unwrap().alias = "f".into();
let pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&bundle)
.unwrap();
let pending = pending.verify().unwrap();
assert_eq!(pending.project(), &desired);
assert_eq!(
pending.warnings(),
leaf_work_warnings("b.arcadia", Span { start: 25, end: 90 }, 2)
);
assert_eq!(pending.bundle, expected);
assert_eq!(
pending.retained,
Slots {
entries: 6,
occurrences: 0,
text: 526 + 221
}
);
}
@@ -522,8 +522,18 @@ fn partial_wide_and_deep_input_cleanup_does_not_poison_a_fresh_parse() {
}
#[test]
fn public_source_headers_and_existing_numeric_token_controls_are_unchanged() {
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
fn public_ordinary_source8_preserves_typed_refusal_and_numeric_controls() {
let ordinary = crate::language::parse_with_report("arcadia 8; project p \"P\" {}").unwrap();
assert!(ordinary.migration.is_none());
assert_eq!(ordinary.project.schema_version, 8);
assert_eq!(ordinary.project.alias, "p");
assert_eq!(ordinary.project.name, "P");
assert!(
crate::language::parse(
"arcadia 8; project p \"P\" { calculation c \"C\" { result Number; body 1; } }"
)
.is_err()
);
for accepted in ["-1.5", "1e-2", "0", "-0"] {
let tokens = crate::language::tokenize(accepted);
assert_eq!(tokens.len(), 1);
@@ -133,6 +133,16 @@ fn typed_oa_sa_canonical_roundtrips_match_independent_projects() {
);
}
let text = canonical(&expected);
let ordinary8 = ordinary_source.replacen("arcadia 7", "arcadia 8", 1);
let ordinary_decoded = crate::language::parse_with_report(&ordinary8).unwrap();
assert!(ordinary_decoded.migration.is_none());
assert_eq!(
ordinary_decoded.project,
crate::language::parse(ordinary_source).unwrap()
);
assert!(
crate::language::parse(&format!("arcadia 8; project p \"P\" {{ {text} }}")).is_err()
);
let actual = parse_typed(crate::language::parse(ordinary_source).unwrap(), &text);
assert_eq!(actual, expected);
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
@@ -147,7 +157,6 @@ fn typed_oa_sa_canonical_roundtrips_match_independent_projects() {
assert!(text.contains(&format!("{prefix}_{word} ")));
}
}
assert!(crate::language::parse("arcadia 8; project p \"P\" {}").is_err());
}
#[test]
+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<'_> {
type Item = Token;
fn next(&mut self) -> Option<Token> {
match self.next_with_strings(|_| Ok::<(), std::convert::Infallible>(())) {
Ok(token) => token,
Err(never) => match never {},
}
}
}
impl Scanner<'_> {
pub(super) fn next_with_strings<E>(
&mut self,
mut hook: impl FnMut(StringEvent) -> Result<(), E>,
) -> Result<Option<Token>, E> {
let source = self.source;
let bytes = source.as_bytes();
let start = self.cursor;
let first = *bytes.get(start)?;
let Some(&first) = bytes.get(start) else {
return Ok(None);
};
let mut legacy_number_part = false;
let class = if matches!(first, b' ' | b'\t' | b'\r' | b'\n') {
self.cursor += 1;
@@ -123,11 +140,24 @@ impl Iterator for Scanner<'_> {
break;
}
}
if closed && serde_json::from_str::<String>(&source[start..self.cursor]).is_ok() {
Class::String
let valid = if closed {
let payload = self.cursor - start - 2;
hook(StringEvent::Reserve {
span: Span {
start,
end: self.cursor,
},
payload,
})?;
// The temporary Result/String drops at this statement's end,
// before the refund hook runs.
let valid = serde_json::from_str::<String>(&source[start..self.cursor]).is_ok();
hook(StringEvent::Released { payload })?;
valid
} else {
Class::Error
}
false
};
if valid { Class::String } else { Class::Error }
} else if b"{}();,[]:=@$+-*/<>!".contains(&first) {
self.cursor += 1;
if matches!(first, b'<' | b'>' | b'=' | b'!') && bytes.get(self.cursor) == Some(&b'=') {
@@ -144,14 +174,14 @@ impl Iterator for Scanner<'_> {
legacy_number_part = first == b'.';
Class::Error
};
Some(Token {
Ok(Some(Token {
class,
span: Span {
start,
end: self.cursor,
},
legacy_number_part,
})
}))
}
}
@@ -194,7 +194,6 @@ pub(super) fn row_key(source: &str, origins: &BTreeMap<Id, Id>) -> Option<String
cursor: 0,
value_depth: 0,
limits: ParseLimits::UNLIMITED,
#[cfg(test)]
mixed: None,
};
let ast = parser.value().ok()?;
@@ -0,0 +1,353 @@
//! Ordinary current-schema compatibility; typed analysis authoring remains private.
use super::*;
use crate::source_project::{self, ProjectLimits, SourceBundle, compile_bundle};
fn example_bundle() -> SourceBundle {
SourceBundle {
entry: "project.arcadia".into(),
files: [
(
"project.arcadia",
include_str!("../../../examples/source-project/project.arcadia"),
),
(
"oa.arcadia",
include_str!("../../../examples/source-project/oa.arcadia"),
),
(
"sa/participants.arcadia",
include_str!("../../../examples/source-project/sa/participants.arcadia"),
),
(
"sa/reporting.arcadia",
include_str!("../../../examples/source-project/sa/reporting.arcadia"),
),
(
"sa/assurance.arcadia",
include_str!("../../../examples/source-project/sa/assurance.arcadia"),
),
(
"views.arcadia",
include_str!("../../../examples/source-project/views.arcadia"),
),
]
.into_iter()
.map(|(path, text)| (path.into(), text.into()))
.collect(),
}
}
fn document(version: &str, body: &str) -> String {
format!(
"arcadia {version};\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
)
}
fn identity(project: &Project, alias: &str) -> Id {
project
.identities()
.into_iter()
.find(|(_, name)| *name == alias)
.unwrap()
.0
}
#[test]
fn current_formatter_roundtrips_complete_ordinary_examples_and_history() {
let input = example_bundle();
let before = input.clone();
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
let mut example = compiled.decoded.project;
assert!(example.elements.values().any(|e| e.layer() == Layer::Oa));
assert!(example.elements.values().any(|e| e.layer() == Layer::Sa));
assert!(!example.diagrams.is_empty());
// The published example has no traces: author an explicit participant link
// rather than assuming that the example already exercises this record class.
let trace_id = Id::from_u128(0x188b_0000_0000_0000_0000_0000_0000_0001);
let trace = Trace {
id: trace_id,
alias: "rescue_participant_trace".into(),
name: "Coordination participant realization".into(),
description: "Relates the operational coordination team to its selected system.".into(),
oa: Id::parse_str("00000000-0000-4000-8000-000000000102").unwrap(),
sa: Id::parse_str("00000000-0000-4000-8000-000000000301").unwrap(),
kind: TraceKind::Participant,
baseline: None,
};
assert_eq!(example.elements[&trace.oa].kind(), Kind::Entity);
assert_eq!(example.elements[&trace.sa].kind(), Kind::System);
assert!(example.traces.insert(trace_id, trace.clone()).is_none());
assert_eq!(example.traces[&trace_id], trace);
let historical =
crate::persistence::decode(include_bytes!("../tests/fixtures/v4-reviewed-prose.json"))
.unwrap();
assert!(historical.traces.values().any(|t| t.baseline.is_some()));
for project in [example, historical] {
assert!(project.definitions.is_empty());
let source = format(&project);
assert!(source.starts_with("arcadia 8;"));
let decoded = parse_with_report(&source).unwrap();
assert_eq!(decoded.project, project);
assert!(decoded.migration.is_none());
let bundle = source_project::format_bundle(&project);
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
assert_eq!(compiled.bundle, bundle);
assert_eq!(compiled.decoded.project, project);
assert!(compiled.decoded.migration.is_none());
let again = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
assert_eq!(again.bundle, compiled.bundle);
assert_eq!(again.index, compiled.index);
assert_eq!(again.decoded.project, project);
assert_eq!(again.diagnostics, compiled.diagnostics);
assert!(again.decoded.migration.is_none());
}
assert_eq!(input, before);
}
#[test]
fn versions_three_through_seven_migrate_to_idempotent_ordinary_eight() {
for version in ["3", "4", "5", "6", "7"] {
let source = document(version, "");
let decoded = parse_with_report(&source).unwrap();
assert_eq!(decoded.project.schema_version, 8);
assert!(decoded.migration.is_some());
let input = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), source.clone())]),
};
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, decoded.project);
assert!(compiled.decoded.migration.is_some());
assert!(compiled.bundle.files["project.arcadia"].starts_with("arcadia 8;"));
let again = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
assert!(again.decoded.migration.is_none());
assert_eq!(again.decoded.project, decoded.project);
assert_eq!(again.bundle, compiled.bundle);
assert_eq!(again.index, compiled.index);
assert_eq!(input.files["project.arcadia"], source);
}
}
#[test]
fn included_nested_omitted_ids_keep_actual_index_and_unicode_crlf() {
let entry =
"// 海岸\r\narcadia 8;\r\nproject coast \"Coast\" { include \"team.arcadia\"; }\r\n";
let fragment = "// Keep nested ownership é\r\noa_entity team \"Team\" {\r\n oa_actor sailor \"Sailor\" { description \"海\"; }\r\n}\r\n";
let input = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
("project.arcadia".into(), entry.into()),
("team.arcadia".into(), fragment.into()),
]),
};
let before = input.clone();
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
assert!(compiled.decoded.migration.is_none());
let p = &compiled.decoded.project;
let team = identity(p, "team");
let sailor = identity(p, "sailor");
assert_eq!(p.elements[&sailor].parent, Some(team));
assert_eq!(p.elements[&sailor].description, "海");
assert_eq!(compiled.index.lexical_owners[&sailor], team);
for (id, prefix) in [
(team, "oa_entity team \"Team\""),
(sailor, "oa_actor sailor \"Sailor\""),
] {
let location = &compiled.index.declarations[&id];
assert_eq!(location.path, "team.arcadia");
let actual = &compiled.bundle.files[&location.path][location.span.start..location.span.end];
assert!(actual.starts_with(prefix));
assert!(actual.contains(&format!("id \"{id}\"")));
assert!(actual.ends_with('}'));
}
assert!(compiled.bundle.files["project.arcadia"].starts_with("// 海岸\r\narcadia 8;\r\n"));
assert!(compiled.bundle.files["team.arcadia"].starts_with("// Keep nested ownership é\r\n"));
for text in compiled.bundle.files.values() {
assert_eq!(text.matches('\n').count(), text.matches("\r\n").count());
}
assert!(compiled.diagnostics.is_empty());
let again = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
assert_eq!(again.bundle, compiled.bundle);
assert_eq!(again.index, compiled.index);
assert_eq!(again.decoded.project, *p);
let mut stale = again.clone();
stale.decoded.project.name = "stale".into();
let error = source_project::update_bundle(&stale, p, ProjectLimits::default()).unwrap_err();
assert_eq!(error[0].diagnostic.code, "stale_source_index");
assert_eq!(input, before);
}
#[test]
fn typed_analysis_future_headers_and_limits_still_refuse() {
for word in [
"quantity_kind",
"unit",
"calculation",
"oa_property",
"sa_property",
"oa_check",
"sa_check",
"oa_analysis",
"oa_case",
"sa_analysis",
"sa_case",
"oa_engineering_property",
"sa_engineering_property",
"oa_analysis_check",
"sa_analysis_check",
"oa_analysis_case",
"sa_analysis_case",
] {
let fragment = format!("{word} x \"X\" id \"00000000-0000-0000-0000-000000000002\" {{}}\n");
let source = document("8", &fragment);
assert!(parse(&source).is_err(), "{word}");
let input = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
(
"project.arcadia".into(),
document("8", "include \"part.arcadia\";"),
),
("part.arcadia".into(), fragment.clone()),
]),
};
let error = compile_bundle(&input, ProjectLimits::default()).unwrap_err();
assert_eq!(error[0].path, "part.arcadia", "{word}");
assert_eq!(error[0].diagnostic.code, "invalid_source");
assert_eq!(error[0].diagnostic.span.start, 0);
if word.contains("engineering_property")
|| word.contains("analysis_check")
|| word.contains("analysis_case")
{
assert_eq!(
error[0].diagnostic.message,
"Analysis records require the source8 authoring implementation."
);
}
assert_eq!(input.files["part.arcadia"], fragment);
}
for version in ["0", "9", "99", "8.0"] {
let error = parse(&document(version, "")).unwrap_err();
assert_eq!(
error[0].message,
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
);
assert_eq!(error[0].span.start, 8);
}
let source = document("8", "");
let error = parse_limited(
&source,
ParseLimits {
max_source_bytes: source.len() - 1,
..ParseLimits::UNLIMITED
},
)
.unwrap_err();
assert_eq!(error[0].code, "source_limit");
let error = parse_limited(
&source,
ParseLimits {
max_records: 0,
..ParseLimits::UNLIMITED
},
)
.unwrap_err();
assert_eq!(error[0].code, "model_limit");
assert!(parse("arcadia 8; project").is_err());
}
#[test]
fn public_checked_save_reopens_ordinary_source_and_json_but_refuses_lost_definition() {
let project = parse_with_report(include_str!("../../../examples/text-first/project.arcadia"))
.unwrap()
.project;
let dir = tempfile::tempdir().unwrap();
for name in ["ordinary.arcadia", "ordinary.json"] {
let path = dir.path().join(name);
let bytes = crate::persistence::save_checked(&path, &project, None).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), bytes);
let decoded = crate::persistence::decode_with_report(&bytes).unwrap();
assert_eq!(decoded.project, project);
assert!(decoded.migration.is_none());
assert_eq!(
crate::persistence::save_checked(&path, &project, Some(&bytes)).unwrap(),
bytes
);
}
let mut with_definition = Project::empty();
let id = Id::parse_str("188b911c-5f8b-431c-9eea-2d41f3953767").unwrap();
with_definition.definitions.insert(
id,
crate::analysis::Definition {
id,
alias: "duration".into(),
name: "Duration".into(),
description: String::new(),
body: crate::analysis::DefinitionBody::QuantityKind(
crate::analysis::QuantityKindDefinition {
dimension: crate::analysis_numeric::Dimension::new([0, 0, 1, 0, 0, 0, 0])
.unwrap(),
algebra: crate::analysis::QuantityAlgebra::Linear,
},
),
},
);
let path = dir.path().join("protected.arcadia");
let sentinel = b"external unfinished draft\r\n";
std::fs::write(&path, sentinel).unwrap();
let count = std::fs::read_dir(dir.path()).unwrap().count();
let error =
crate::persistence::save_checked(&path, &with_definition, Some(sentinel)).unwrap_err();
assert!(
matches!(error,crate::persistence::FileError::Invalid(message) if message=="Generated source does not reproduce the complete requested project.")
);
assert_eq!(std::fs::read(&path).unwrap(), sentinel);
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), count);
}
#[test]
fn cli_preview_literals_compile_to_independent_complete_projects() {
let entry = "// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"participants.arcadia\"; include \"children.arcadia\";\r\n}\r\n";
let before_team = "// @team remains documentation\r\noa_entity team \"Team @team\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n";
let after_team = "// @team remains documentation\r\noa_entity rescue_team \"Team @team\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n";
let before_child = "oa_entity child \"Child\" id \"00000000-0000-0000-0000-000000000003\" { parent @team; }\r\n";
let after_child = "oa_entity child \"Child\" id \"00000000-0000-0000-0000-000000000003\" { parent @rescue_team; }\r\n";
let project_id = Id::from_u128(1);
let team_id = Id::from_u128(2);
let child_id = Id::from_u128(3);
let mut expected = Project::empty();
expected.id = project_id;
expected.alias = "p".into();
expected.name = "Project".into();
for (id, alias, name, parent) in [
(team_id, "team", "Team @team", None),
(child_id, "child", "Child", Some(team_id)),
] {
expected.elements.insert(
id,
Element {
id,
alias: alias.into(),
name: name.into(),
description: String::new(),
parent,
body: ElementBody::new(Kind::Entity, Layer::Oa).unwrap(),
},
);
}
for (team, child, alias) in [
(before_team, before_child, "team"),
(after_team, after_child, "rescue_team"),
] {
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: [
("project.arcadia", entry),
("participants.arcadia", team),
("children.arcadia", child),
]
.into_iter()
.map(|(path, source)| (path.into(), source.into()))
.collect(),
};
expected.elements.get_mut(&team_id).unwrap().alias = alias.into();
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, expected);
assert!(compiled.decoded.migration.is_none());
assert!(compiled.diagnostics.is_empty());
assert_eq!(compiled.bundle, bundle);
}
}
+82 -8
View File
@@ -379,6 +379,59 @@ pub fn expect_new_file(
Ok(())
}
/// Captured mixed source bytes and discovery diagnostics, without save authority.
/// Handle discovery errors before checking the complete bundle. This is not an
/// atomic snapshot against another process and never performs journal recovery.
pub struct MixedSourceSnapshot {
bundle: SourceBundle,
diagnostics: Vec<SourceDiagnostic>,
}
impl MixedSourceSnapshot {
pub fn bundle(&self) -> &SourceBundle {
&self.bundle
}
pub fn diagnostics(&self) -> &[SourceDiagnostic] {
&self.diagnostics
}
}
/// Discover explicit-ID mixed source8 through the shared grammar without writes.
/// Complete semantic checking remains `language::check_mixed_bundle`; discovery
/// alone does not admit a model. Source bytes are capped before disk reads.
pub fn load_mixed_source(
entry: &Path,
mut limits: ProjectLimits,
) -> Result<MixedSourceSnapshot, SourceFileError> {
limits.model.max_source_bytes = limits.model.max_source_bytes.min(64 * 1024 * 1024);
let (root, entry) = root_entry(entry)?;
let snapshot = DiskProject {
root,
bundle: SourceBundle {
entry,
files: BTreeMap::new(),
},
expectations: BTreeMap::new(),
reachable: BTreeSet::new(),
diagnostics: Vec::new(),
};
let mut discovery = language::MixedDiscovery::new(limits).map_err(SourceFileError::Limit)?;
let captured = discover(snapshot, limits, DiscoveryMode::Mixed(&mut discovery))?;
drop(discovery);
let DiskProject {
bundle,
diagnostics,
..
} = captured;
Ok(MixedSourceSnapshot {
bundle,
diagnostics,
})
}
enum DiscoveryMode<'a> {
Ordinary,
Mixed(&'a mut language::MixedDiscovery),
}
/// Merge editor overlays and discover only files absent from retained buffers.
/// Existing drafts (including detached ones) and their disk baselines survive.
pub fn discover_missing(
@@ -390,6 +443,14 @@ pub fn discover_missing(
result.bundle.files.extend(overlays.clone());
result.reachable.clear();
result.diagnostics.clear();
discover(result, limits, DiscoveryMode::Ordinary)
}
fn discover(
mut result: DiskProject,
limits: ProjectLimits,
mut mode: DiscoveryMode<'_>,
) -> Result<DiskProject, SourceFileError> {
check_snapshot(&result, limits)?;
let mut stack = vec![(result.bundle.entry.clone(), 0usize, None::<SourceLocation>)];
let mut edges = 0usize;
@@ -492,12 +553,20 @@ pub fn discover_missing(
}
}
}
match inspect_includes(
&path,
&result.bundle.files[&path],
path == result.bundle.entry,
limits,
) {
let includes = match &mut mode {
DiscoveryMode::Ordinary => inspect_includes(
&path,
&result.bundle.files[&path],
path == result.bundle.entry,
limits,
),
DiscoveryMode::Mixed(discovery) => discovery.inspect(
&path,
&result.bundle.files[&path],
path == result.bundle.entry,
),
};
match includes {
Ok(includes) => {
edges = edges.saturating_add(includes.len());
if edges > limits.max_include_edges {
@@ -517,7 +586,12 @@ pub fn discover_missing(
.map(|include| (include.target, depth + 1, Some(include.location))),
);
}
Err(diagnostics) => result.diagnostics.extend(diagnostics),
Err(diagnostics) => {
result.diagnostics.extend(diagnostics);
if matches!(mode, DiscoveryMode::Mixed(_)) {
break;
}
}
}
}
check_snapshot(&result, limits)?;
@@ -525,7 +599,7 @@ pub fn discover_missing(
&result.root,
result.expectations.keys().chain(result.bundle.files.keys()),
)?;
if result.diagnostics.is_empty() {
if result.diagnostics.is_empty() && matches!(mode, DiscoveryMode::Ordinary) {
match compile_bundle(&result.reachable_bundle(), limits) {
Ok(compiled) => result.diagnostics = compiled.diagnostics,
Err(diagnostics) => result.diagnostics = diagnostics,
+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);
}
+318
View File
@@ -0,0 +1,318 @@
//! Actual filesystem discovery followed by the public borrowed complete checker.
use oa_core::{analysis as a, language, source_files::*, source_project::*, *};
use std::{collections::BTreeMap, fs};
fn id(n: u128) -> Id {
Id::from_u128(n)
}
fn document(body: &str) -> String {
format!(
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
id(1)
)
}
fn write(files: &[(&str, String)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
for (path, text) in files {
fs::write(dir.path().join(path), text).unwrap();
}
dir
}
fn unchanged(dir: &tempfile::TempDir, files: &[(&str, String)]) {
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), files.len());
for (path, text) in files {
assert_eq!(fs::read(dir.path().join(path)).unwrap(), text.as_bytes());
}
}
fn calculation(alias: &str, n: u128, body: a::Expr) -> a::Definition {
a::Definition {
id: id(n),
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),
}),
}
}
fn calc(alias: &str, n: u128, body: &str) -> String {
format!(
"calculation {alias} \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body {body}; }}",
id(n),
id(1)
)
}
#[test]
fn mixed_files_complete_model_nested_ordinary_scoped_cross_file_and_original_bytes() {
let files = [
(
"main.arcadia",
document(
"// include \"ignored.arcadia\"; 海\ninclude \"f.arcadia\"; include \"g.arcadia\";",
),
),
(
"f.arcadia",
format!(
"{}\noa_entity team \"Team\" id \"{}\" {{ oa_actor crew \"Crew\" id \"{}\" {{}} }}\r\n",
calc("f", 2, "@g(x: $x)"),
id(4),
id(5)
),
),
("g.arcadia", calc("g", 3, "$x")),
];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert!(
snapshot.diagnostics().is_empty(),
"{:?}",
snapshot.diagnostics()
);
assert_eq!(snapshot.bundle().entry, "main.arcadia");
assert_eq!(
snapshot.bundle().files,
files
.iter()
.map(|(p, t)| (p.to_string(), t.clone()))
.collect()
);
let mut expected = Project::empty();
expected.id = id(1);
expected.alias = "p".into();
expected.name = "Project".into();
expected.definitions.insert(
id(2),
calculation(
"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("g", 3, a::Expr::Parameter(a::ParameterId(id(1)))),
);
for (n, alias, name, kind, parent) in [
(4, "team", "Team", Kind::Entity, None),
(5, "crew", "Crew", Kind::Actor, Some(id(4))),
] {
expected.elements.insert(
id(n),
Element {
id: id(n),
alias: alias.into(),
name: name.into(),
description: String::new(),
parent,
body: ElementBody::new(kind, Layer::Oa).unwrap(),
},
);
}
let checked =
language::check_mixed_bundle(snapshot.bundle(), ProjectLimits::default()).unwrap();
assert_eq!(checked.project(), &expected);
assert!(checked.warnings().is_empty());
drop(checked);
drop(snapshot);
unchanged(&dir, &files);
}
#[test]
fn mixed_files_terminal_syntax_preserves_first_path_and_still_reports_journal() {
let files = [
(
"main.arcadia",
document("include \"bad.arcadia\"; include \"later.arcadia\";"),
),
("bad.arcadia", "oa_entity missing \"Missing\" {}".into()),
("later.arcadia", "must not parse".into()),
];
let dir = write(&files);
fs::write(
dir.path().join(".arcadia-save-main.arcadia.journal"),
b"unrecovered",
)
.unwrap();
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert_eq!(snapshot.diagnostics().len(), 2);
assert_eq!(snapshot.diagnostics()[0].path, "bad.arcadia");
assert_eq!(
snapshot.diagnostics()[0].diagnostic.message,
"Mixed source8 requires explicit UUIDs."
);
assert_eq!(snapshot.diagnostics()[1].diagnostic.code, "pending_save");
assert!(!snapshot.bundle().files.contains_key("later.arcadia"));
assert_eq!(
fs::read(dir.path().join("later.arcadia")).unwrap(),
b"must not parse"
);
assert_eq!(
fs::read(dir.path().join(".arcadia-save-main.arcadia.journal")).unwrap(),
b"unrecovered"
);
for (path, text) in &files {
assert_eq!(fs::read(dir.path().join(path)).unwrap(), text.as_bytes());
}
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 4);
}
#[test]
fn mixed_files_identity_classes_and_malformed_units_are_discovery_errors() {
let valid = calc("f", 2, "$x");
let cases = [
document("").replace(&format!(" id \"{}\"", id(1)), ""),
document("oa_entity e \"E\" {}"),
document(&valid.replace(&format!("id \"{}\"", id(2)), "")),
document(&valid.replace(&format!("input x id \"{}\"", id(1)), "input x")),
document("calculation malformed"),
];
for (i, text) in cases.into_iter().enumerate() {
let files = [("main.arcadia", text)];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert_eq!(snapshot.diagnostics().len(), 1);
assert_eq!(snapshot.diagnostics()[0].path, "main.arcadia");
if i < 4 {
assert_eq!(
snapshot.diagnostics()[0].diagnostic.message,
"Mixed source8 requires explicit UUIDs."
);
} else {
assert_eq!(snapshot.diagnostics()[0].diagnostic.code, "invalid_source");
}
unchanged(&dir, &files);
}
}
#[test]
fn mixed_files_include_failures_and_cycles_do_not_become_partial_admission() {
for (include, code) in [
("missing.arcadia", "missing_include"),
("../escape.arcadia", "invalid_path"),
] {
let files = [("main.arcadia", document(&format!("include \"{include}\";")))];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics()
.iter()
.any(|d| d.diagnostic.code == code)
);
unchanged(&dir, &files);
}
let files = [
("main.arcadia", document("include \"part.arcadia\";")),
("part.arcadia", "include \"part.arcadia\";".into()),
];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert!(snapshot.diagnostics().is_empty());
let errors = match language::check_mixed_bundle(snapshot.bundle(), ProjectLimits::default()) {
Ok(_) => panic!("cycle admitted"),
Err(errors) => errors,
};
assert!(errors.iter().any(|d| d.diagnostic.code == "include_cycle"));
unchanged(&dir, &files);
}
#[test]
fn mixed_files_limits_encoding_nonregular_and_physical_aliases() {
let files = [
("main.arcadia", document("include \"part.arcadia\";")),
("part.arcadia", "include \"last.arcadia\";\r\n".into()),
("last.arcadia", "//é\r\n".into()),
];
let dir = write(&files);
let entry = dir.path().join("main.arcadia");
for (kind, cap) in [("files", 3), ("depth", 2), ("edges", 2), ("bytes", 132)] {
for allowed in [cap, cap - 1] {
let mut limits = ProjectLimits::default();
match kind {
"files" => limits.max_files = allowed,
"depth" => limits.max_include_depth = allowed,
"edges" => limits.max_include_edges = allowed,
_ => limits.model.max_source_bytes = allowed,
};
let result = load_mixed_source(&entry, limits);
if allowed == cap {
assert!(result.unwrap().diagnostics().is_empty(), "{kind}");
} else {
match result {
Err(_) => {}
Ok(snapshot) => assert!(!snapshot.diagnostics().is_empty(), "{kind}"),
}
}
unchanged(&dir, &files);
}
}
fs::write(dir.path().join("last.arcadia"), [0xff]).unwrap();
let snapshot = load_mixed_source(&entry, ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics()
.iter()
.any(|d| d.diagnostic.code == "source_encoding")
);
fs::remove_file(dir.path().join("last.arcadia")).unwrap();
fs::create_dir(dir.path().join("last.arcadia")).unwrap();
let snapshot = load_mixed_source(&entry, ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics()
.iter()
.any(|d| d.diagnostic.code == "invalid_path")
);
let collision = write(&[
(
"main.arcadia",
document("include \"part.arcadia\"; include \"PART.arcadia\";"),
),
("part.arcadia", "// case alias\n".into()),
]);
assert!(matches!(
load_mixed_source(
&collision.path().join("main.arcadia"),
ProjectLimits::default()
),
Err(SourceFileError::Path(_))
));
let aliases = write(&[
(
"main.arcadia",
document("include \"a.arcadia\"; include \"b.arcadia\";"),
),
("a.arcadia", "// same physical file\n".into()),
]);
fs::hard_link(
aliases.path().join("a.arcadia"),
aliases.path().join("b.arcadia"),
)
.unwrap();
assert!(matches!(
load_mixed_source(
&aliases.path().join("main.arcadia"),
ProjectLimits::default()
),
Err(SourceFileError::Path(_))
));
}
+3
View File
@@ -25,7 +25,10 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti
assert_eq!(baseline.sa.name, "Approved SA name");
assert_ne!(baseline.oa.name, decoded.project.named(trace.oa));
let mut expected: Value = serde_json::from_slice(JSON4).unwrap();
assert!(expected.as_object().unwrap().get("definitions").is_none());
assert!(decoded.project.definitions.is_empty());
expected["schema_version"] = oa_core::SCHEMA_VERSION.into();
expected["definitions"] = serde_json::json!({});
assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected);
let reopened =
persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap())
+5 -2
View File
@@ -1036,16 +1036,19 @@ fn source7_source_bundle_migration_is_token_only_and_new_link_stays_with_caller(
let f = chain(Layer::Oa);
let mut bundle = source_project::format_bundle(&f.p);
let entry = bundle.files.get_mut(&bundle.entry).unwrap();
assert!(entry.starts_with("arcadia 8;"));
*entry = entry
.replacen("arcadia 7;", "arcadia // preserve this header\r\n 6;", 1)
.replacen("arcadia 8;", "arcadia // preserve this header\r\n 6;", 1)
.replace('\n', "\r\n");
assert!(entry.starts_with("arcadia // preserve this header"));
assert_eq!(entry.matches(" 6;").count(), 1);
let old = bundle.clone();
let compiled = source_project::compile_bundle(&bundle, Default::default()).unwrap();
assert_eq!(compiled.decoded.project, f.p);
assert_eq!(compiled.decoded.migration.as_ref().unwrap().from_schema, 6);
for (path, source) in &old.files {
let expected = if path == &old.entry {
source.replacen(" 6;", " 7;", 1)
source.replacen(" 6;", " 8;", 1)
} else {
source.clone()
};
+25
View File
@@ -633,6 +633,31 @@ 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>
<p>For filesystem input, <span class="docutils literal">arcadia <span class="pre">check-mixed</span> ROOT <span class="pre">[--json]</span></span> discovers included
files with the same mixed grammar and the existing disk/path/journal safeguards,
then checks the complete captured bundle. It requires explicit UUIDs and schema8;
<span class="docutils literal"><span class="pre">--write</span></span> is rejected. The discovery pass shares a conservative cumulative
resource budget across files and ends before a fresh complete-validation pass.
It does not claim one combined numeric-work allowance or an atomic snapshot
against concurrent external edits. Ordinary check/lint/edit commands retain their
existing contracts. See <a class="reference external" href="source-cli.md">Source CLI</a> for command use and limits.</p>
</section>
<section id="scope-and-conformance">
<h2>1. Scope and conformance</h2>
<p>This specification defines the shared Operational Analysis (OA) and System
+29
View File
@@ -6,6 +6,35 @@ 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.
For filesystem input, ``arcadia check-mixed ROOT [--json]`` discovers included
files with the same mixed grammar and the existing disk/path/journal safeguards,
then checks the complete captured bundle. It requires explicit UUIDs and schema8;
``--write`` is rejected. The discovery pass shares a conservative cumulative
resource budget across files and ends before a fresh complete-validation pass.
It does not claim one combined numeric-work allowance or an atomic snapshot
against concurrent external edits. Ordinary check/lint/edit commands retain their
existing contracts. See `Source CLI <source-cli.md>`_ for command use and limits.
1. Scope and conformance
------------------------
+40
View File
@@ -64,3 +64,43 @@ are not a claim of interactive performance at the maximum project size.
For the notation itself, start with [the text-first examples](../examples/text-first/README.md)
and the [language contract](language.rst).
## Read-only mixed source8 checking
```sh
arcadia check-mixed path/to/main.arcadia --json
```
Use `check-mixed` for source8 containing ordinary declarations and typed analysis
(quantity kinds, units, calculations, properties, checks and cases). Every
project, ordinary declaration, typed definition and scoped parameter must have
an explicit UUID. This command does not migrate older headers, allocate UUIDs,
format, preview edits or save. `--write` is a usage error. Existing `check`,
`lint`, `stabilize` and `rename-alias` retain their ordinary-source behavior.
The loader discovers actual includes with the source grammar and existing
portable-path, regular-file, bounded-read and pending-journal checks. It captures
original bytes, runs a bounded discovery syntax pass, then checks the complete
borrowed bundle in a separate bounded verification pass. A discovery diagnostic
prevents complete verification; the first mixed syntax/work failure stops
further discovery parsing. Both passes derive limits from the same CLI policy;
numeric work is bounded per pass, not once across the whole command. Logical
producer limits are not a bound on all allocations or process memory.
The format-1 JSON envelope uses operation `check-mixed`, status `checked` or
`invalid` for completed source checking, and existing `rejected` for direct disk
errors. `changes` is empty, `written` is false, `identities_to_stabilize` is zero,
`migration` is null and `notes` is empty. Located diagnostics refer to original
`input` bytes, with half-open UTF-8 byte spans and one-based Unicode-scalar
line/column coordinates. Direct filesystem failures use the existing operation
coordinate space. Engineering warnings permit exit 0; invalid source/semantics
exit 1, usage exits 2, and disk/path/missing-file/pending-journal failures exit 3.
Invalid UTF-8 discovery diagnostics retain the existing source-error exit 1.
Human output goes to stderr and ends with `checked: mixed source8 check; no files
changed` on success, after any warnings. No source bytes or directory entries
are written, and pending saves are never recovered implicitly. Checking captured
bytes does not establish an atomic multi-file snapshot against concurrent writers.
This command provides validation only; mixed editing, adoption, GUI and history
integration remain outside this interface.
@@ -0,0 +1,56 @@
# Private retained-entry producer prerequisite
Base `97a8703adc75467a7f8e75f5c6cb00ca542b707c`; implements the accepted
`harness/analysis-entry-producer-contract-20260925.md` in the parent source tree.
This is fixture-only phase/producer evidence, not emitted-output admission.
The explicit entry route also enables text producers and derives C and exact
policy from the existing carried guard. Budget adds only optional scalar C/M.
Existing legacy, carried-phase and text-only entry paths retain their checkpoints.
U keeps its original event sites and work-limit precedence; prospective aggregate
checks latch without changing totals. F is installed before traversal. Private
State helpers reserve unique value-end keys and lexical owner occurrences before
mutation. Successful grammar events count projection slots with checked addition.
The new route reserves token-end slots and projection together, without the old
borrowed-work counting Vec. A scalar quota checks each projected envelope, field
and actual value before growth. Synthetic bodies remain included in seven.
Per-file actual map/owner lengths and final independent metadata reconcile M;
resolver C then U+M transfer and text reconciliation are unchanged.
Six tests in `mixed::carried::entry_producer` cover independent literal E and H
syntax caps 13/12 and 25/24 separately from resolver 17/16 and 29/28, owner/value
prefix refusals 4/7, and the 344-byte nested syntax-only fixture (U13, M89, P30,
C1; 103/102). They check node/element envelopes, nested positions and owner order,
quota underflow/leftover, direct unchanged collections on refusal and repeated
map-key replacement. Labeled arithmetic controls exercise overflow, sticky-first
failure, prospective U and work-limit precedence. Whole OA/SA Project, warnings,
Usage, numeric work and retained totals are compared with text-only under actual
and explicit zero guards. Old-route diagnostics and original bytes are checked.
All byte policies are roomy 4096 so earlier text limits do not mask entry tests.
Source review identified a possible internal overflow call overwriting an already
latched error. The helper now returns the first failure; a direct fault control
checks that behavior. Repeated map keys now perform a zero-delta reservation
before replacement, preserving a latched refusal without modifying the map;
a direct control checks that branch too. These were corrected before compilation,
not a runtime bug
observation. Test review also caught that the independent inventory walker adds
its own existing scratch slots: exact-cap ordinary syntax controls remain at
29/37/103, while a separate roomy owner of the same literal runs that walk.
No live policy is widened and no exact-cap resolver success is implied.
No prior tests, stack sizes, policies, inherited failure dispositions
or unrelated formatting are changed. Only touched Rust files were directly
formatted with skip_children. Parent owns compiler, full disposition, Clippy,
format and prologue/stack acceptance; no runtime result is claimed here.
Allocator overhead, token storage under its existing token ceiling, delimiter and
other work stacks, numeric/decoder internals and model/validator heap remain
outside this retained metric. The extra shared projector wrapper and scalar
Budget/State fields require measured parent stack checks; no heap indirection or
larger stack is introduced. Final output acceptance remains a separate task.
Initial a629ae01 focused compilation failed before any test ran: the quota
control's unwrap_err required Debug on its successful EditDeclaration vector.
The test now matches Err explicitly and panics on unexpected Ok, preserving
the same assertion without changing production types. Original compiler output
and source-bound receipt remain with parent gate evidence.
@@ -0,0 +1,90 @@
# Private actual-output rename acceptance
Base `29417130c71f70c4eaefe8ed4c879efba78eb72c`. Implements the parent-selected
`harness/analysis-mixed-acceptance-contract-20260925.md` (root b86c8dbc) within the
private experimental mixed source path. No public source8, save, history,
installation or repeated carried-source emission is enabled.
A consumed PendingRename now verifies its actual complete bundle through the
combined text/entry producer parser and existing complete resolver/admission.
CarriedInput and its slim guard borrow that entire pending owner; covariance
shortens the desired borrow to the verification scope without another parser
lifetime, policy copy in guards, arbitrary tuple constructor or heap wrapper.
Narrow immutable views supply the exact bundle, policy and retained O together.
The existing fixture constructors and old parser modes remain available unchanged.
The actual resolved expression arena supplies E for equality reservation against
its full live storage. Complete actual Project PartialEq compares every field
with desired; old name-exempt equality and pending's original E are not used.
A static entry/default-span diagnostic reports mismatch. Warning inventory uses
an exhaustive borrowed diagnostic-field match, checks empty related/non-Error
shapes and sums checked entries/text. O+W is checked as a componentwise live
subset and against inherited ceilings without resetting or double-charging the
full ledger. Actual model/warnings move after equality; expression/occurrence
arenas, syntax and complete pending guard drop before the bundle moves into
AcceptedRename. Its private fields have read-only accessors, exact policies,
final retained slots and separate verification Usage/numeric work; no input or
desired borrow remains.
Eleven new tests use the acceptance module filter. Independent minimal literal
and whole model verify 152/151 through one unchanged profile; warning literals
and full diagnostics verify 161/160 and final O+W=(2,0,157). Exact policy fields
and a lexical scope dropping all inputs exercise returned ownership. Previously
authored OA/SA/scoped-global swap and UTF-8/CRLF/all-file literals are reused,
including an untouched fourth file. Equal-byte wrong alias/name, malformed
source and parseable containment-cycle corruption separate equality, syntax and
model-admission refusal. Actual E1->3 corruption plus a labeled internal ledger
fault distinguishes reserving3 from trusting old1, without a fabricated late
end-to-end equality cap. Other internal controls cover warning shapes, final
subset and checked inventory overflow.
Scoped 128 KiB workers perform actual verification/assertions/drop for deep128
and wide129 expression nodes (success and target-only mismatch), plus the genuine
151-byte late refusal. All original prerequisites are prepared outside those
workers. Separate E0 target-only deep/wide and signed-zero geometry/scalar/history
cases exercise complete equality. Positive-zero bits in actual returned model
help distinguish moving the parsed model from substituting desired intent.
Before execution, source review prompted correcting the carried module summary;
parent readback caught an unnamed-source test slice, replaced with the exact
independent 71-byte original literal. Local type inspection corrected the f32
geometry bit assertion. These are pre-execution authoring corrections, not
runtime failures. No prior tests or stack sizes are edited. Only touched Rust
files were directly formatted with skip_children; compiler, full prior outcome
comparison, Clippy, format and measured prologue/stack gates belong to the parent.
No runtime result was claimed at that initial handoff; see the correction below.
The selected retained/decode/equality domain remains logical, not universal heap
or before-every-allocation accounting. Existing traversal/path-copy, delimiter,
ordinary-open, Pratt and duplicate-name scratch keep their independent controls
and stated exclusions; model/validator/numeric/decoder internals and native stack
remain separate domains. File counts do not tightly bound traversal path bytes.
Actual lifetime coercion and stack headroom still require the parent gates.
## Initial focused failure and test-oracle correction
Parent's initial focused run of `4872141c036c16825f1c098bd7cf44709897e814`
compiled successfully and completed 9 passes / 2 failures. The failing assertions
incorrectly required empty warnings in the rich OA/SA and four-file fixtures.
The original run, log and artifact remain preserved under the parent's
`analysis-output-acceptance-gates-20260925/acceptance-focused-4872141-1` records;
artifact cba18de5 prefix, 43,060,224 bytes. Those failures are not erased or called
production failures. The other nine controls, including both late byte caps and
scoped 128 KiB verification/drop, passed in that run.
Source-derived correction: validation.rs emits unallocated_leaf first, with
field allocation and message "Allocate leaf 'Work' to a performer."; its later
coverage.validate call emits work_without_behavior_context, field None, message
"Include this leaf activity/function in a process/chain or scenario through a
work action or exchange occurrence." Both are Warning, object ID2, empty related,
and map to the full Work declaration. The first local derivation missed the
coverage advisory; independent review caught it before the correction commit.
The pre-existing project expected_leaf_work_warnings oracle confirms both/order.
New tests now require both complete mapped diagnostics, with independent literal
spans: rich OA/SA main338..403 line6 column1, or Unicode/CRLF351..416 line7 column1;
four-file b.arcadia25..90 line2 column1. Warning storage is (2,0,227) for main,
(2,0,221) for b.arcadia. Final rich entries5/text1169,1212,1243,1286 for OA LF,
OA transformed, SA LF, SA transformed; four-file entries6/text747. Production
acceptance and all original test bodies remain unchanged. This is a test-only
oracle repair plus this note; corrected runtime acceptance awaits parent gates.
@@ -0,0 +1,48 @@
# Mixed CLI core discovery implementation
Base bab6f96a; selected root contract f810fb40. This contribution owns core only.
source_files::load_mixed_source returns opaque MixedSourceSnapshot with immutable
bundle/diagnostics views. It consumes a fresh private DiskProject after final
checks and drops root/expectations/reachability; no reachable-bundle clone, model,
index, save or recovery authority escapes. The byte allowance is clamped to the
existing64MiB ceiling before disk reads; other caller fields are preserved.
The existing discovery loop has a private borrowed ordinary/mixed selector.
Ordinary overlays, aggregation, limits, final compilation and journal behavior
are preserved. Mixed selects a shared Budget/ModelCounts phase and skips ordinary
compilation. Syntax/budget failure records the original file diagnostic and stops
parsing later siblings, while final disk/physical/journal checks still execute.
Disk/encoding/path failures retain existing classifications and safeguards.
Both producer modes start once at zero. Each inspected file reserves its phase
slot before scanning. Bytes/tokens/references/numeric/syntax/model work accumulate
across units. Remaining text/value/owner reservations remain conservative after
unit destruction; in-parser temporary/UUID/key releases are unchanged. No edit
positions or complete model are constructed during discovery. Final public
semantic checking is a separate pass with the original caller policy.
A narrow shared parse_unit helper centralizes exactly the existing Parser setup,
unit(entry,true) call, state extraction and Parser drop. Complete mixed parsing
keeps all subsequent metadata/projection/traversal/accounting operations. Source
review finds no semantic blocker; helper-frame effects still require parent's
actual existing stack gates and prologue comparison, not a source-only claim.
Three internal discovery tests cover exact132-byte/31-token/3-event chain totals,
conservative text32/metadata3 after unit drops, all temporary balances, original
policy capture, cumulative6/5 entry boundary with no later-file remapping, syntax
failure terminality and positive cumulative numeric/reference work. The tight
entry profile is explicitly a fresh internal fixture, not a public policy knob.
Five external mixed_source_files tests exercise real filesystem capture and
independently authored complete model equality (nested ordinary participants,
cross-file calculations and scoped parameters), original bytes, four missing-ID
classes, malformed syntax, unparsed later sibling plus preserved pending journal,
missing/escaping/cyclic includes, file/byte/depth/edge boundaries, invalid UTF-8,
nonregular files, case collisions and hard-link aliases. CLI worker separately
owns OA/SA rich command fixtures/report/actual executable tests and documentation.
No old test body or expected outcome was changed.
Only owned Rust files were formatted directly with skip_children=true. No Cargo,
Rust compiler, rustdoc, CLI execution or remote operation ran in this contribution.
Parent owns all runtime, production ordinary-regression, stack, lint and combined
source acceptance. Existing depth/platform/accounting limitations remain intact.
@@ -0,0 +1,65 @@
# Mixed CLI read interface
Implements the selected `harness/analysis-mixed-cli-contract-20260925.md` contract
in the integration owner's root records. Local implementation checkpoint;
initial execution evidence and remaining acceptance boundaries are recorded below.
`oa_cli::check_mixed(&Path) -> Report` loads `MixedSourceSnapshot`, checks its
borrowed original bundle only when discovery diagnostics are empty, and copies
only returned diagnostics into the existing format-1 report. It never constructs
`Prepared`, compiles ordinary source, stabilizes identities, migrates or saves.
The closed `Dispatch` enum separates `CheckMixed` from `Editable { operation,
write }`; `Operation` and `prepare` retain their existing ordinary semantics.
Reports preserve input locations and explicit object IDs. Successful warnings
are retained. Source errors use `invalid`/1, loader missing/path/journal diagnostic
refusals use `invalid`/3, and direct `SourceFileError` retains the existing
`rejected`/3 mapping. Fixed fields are empty changes/notes, false written, zero
identity stabilization, and null migration. Human output names a mixed source8
check, with no edit-candidate summary. `--write`, unknown/repeated options and
wrong arity cannot enter either loader or preparation through mixed dispatch.
Six external `mixed_cli` tests execute the real Cargo-provided binary. The rich
OA/SA fixtures reuse previously authored ordinary/typed literals and independently
constructed public model values, with an explicit SA System, reordered call
labels, escaped include paths, decoy include comments, Unicode and LF/CRLF.
Their complete returned models and four ordered leaf warnings are checked before
complete CLI report comparison. The four warnings represent two leaf declarations
(allocation then coverage); they do not replace the earlier one-leaf oracle.
Other cases cover all four missing-UUID classes, schema refusal, cross-file
semantic failure, missing/cyclic/escaping includes, invalid UTF-8, pending journal,
reserved root, human output and option refusal. Each subprocess compares complete
file bytes and directory inventory before/after, including unrelated files.
Platform-specific OS missing-file text is obtained from an independent filesystem
metadata error; no parser, formatter or CLI output generates expected reports.
The parent owns compilation/runtime acceptance. This source note does not imply
Linux coverage, full mixed editing, GUI adoption or an atomic disk snapshot.
## Initial execution and narrow test corrections
Parent-run source `f2324822936374c5b9092518b986375233dc7882` passed all five
`mixed_source_files` external tests and all six `mixed_cli` executable tests.
The mixed test import nevertheless produced `hidden_glob_reexports` because
`std::process::Command` shadowed the public core `Command` re-export used by the
independent model helper. The local name is now `ProcessCommand`; behavior and
expectations are unchanged.
The initial existing `source_cli` suite completed 10 passes and two failures:
`complete_alarm_example_preview_then_write_and_fixed_point` compared the whole
ID-stripped output against an unchanged schema7 header, and
`rename_shows_missing_identity_and_separate_source_migration` expected target
schema7. Ordinary source has already selected schema8. The correction preserves
the original alarm fixture, explicitly asserts its schema7 prefix and compares
all stripped bytes against exactly one header-token replacement to schema8.
It also asserts the preview's 7→8 migration report. The 40 identities, unchanged
preview, complete saved text and repeated-save fixed-point assertions remain.
The rename fixture still originates at schema5; only its expected migration
target and added diff line change to schema8. No production changes accompany
these evaluator corrections.
Original logs/receipts remain under the integration owner's
`.local/analysis-mixed-cli-gates-20260925/`, including
`mixed-cli-integration-suite-source_cli-f232482-1.*`. This records actual initial
failures rather than claiming they passed; corrected-source reruns and final
acceptance remain the parent's responsibility.
@@ -0,0 +1,63 @@
# Public mixed read implementation
Base7b06b8ea, selected root contract bcddaba3. Public
language::check_mixed_bundle returns opaque non-Clone CheckedMixed borrowing the
complete original SourceBundle, with only immutable project/warnings accessors.
Its private resolved field retains syntax, expression/occurrence vectors, ledger
and admission evidence; there is no index, output, save or extraction authority.
The shared parser mixed hooks now compile in production. Ordinary callers still
select None. The fresh constructor binds the actual bundle and exact ProjectLimits,
derives existing internal policy once, selects Some(Borrowed) with zero carry and
both producers, then consumes syntax through complete resolution/admission.
The borrowed guard retains the actual source reference even when test-only
Pending/Retained variants are absent. No duplicate live policy/model is added.
Public schema/UUID requirement wording replaces private-staging wording only;
all limiter phase/category/span transport remains inherited.
Private legacy index/update/write, ordinary-index/rename/emission/acceptance and
fixture/pending constructors remain test-only. Shared parse/lower code remains
private. Necessary retained fields have individual documented non-test dead-code
allowances; no module-wide suppression, fake read, or new public lint-only API.
Exact production compiler lint findings remain a parent gate, not inferred clean
from source inspection. Existing298 tests are not edited.
One internal public_read test invokes the actual production parse helper and
observes borrowedC0, both producer inventories, every inherited/derived policy
field (including262144 entries independent of model records1), full empty-Project
and warning parity, Usage/numeric parity and unchanged input. Three rustdoc blocks
provide a compiling real usage example plus source-drop and source-mutation
compile-fail cases with later result use. Independent worker owns the non-test
mixed_source8_read integration target and its separate complete-model oracles.
Established docs/language.rst gains a bounded staged-read note without changing
its broader conformance/version obligations. Existing docutils HTML5 generation
with halt/report level2 regenerated language.html successfully. An initial local
RST generation refused the new section formatting; the section separator and
blank line were corrected before successful regeneration. No Rust compiler,
Cargo test, CLI, debugger or publication was run by this worker.
Parent owns pinned compilation, all old/new runtime tests, doctests, strict
Clippy, formatting disposition and production Parser stack regression checks.
DEPTH_128_VALIDATED=false, existing depth policies, logical-accounting exclusions,
full-pipeline limitation and deferred Linux qualification remain unchanged.
## First non-test compile correction
Parent's integration-list build at ddf823a stopped with E0063: the semantic review
row-key reader's Parser initializer still gated mixed:None under cfg(test).
That initializer now supplies None in production too, preserving its ordinary
facet-reading behavior. Source inventory found the other shared Parser initializers
in language.rs and mixed.rs already coherent; language::legacy uses its own
separate Parser type. No API, test or runtime policy changed. The failed build
record remains preserved; no compiler was run for this repair by the worker.
## Non-test integration result and warning cleanup
Parent's actual non-test integration suite at 8555e417 passed all ten tests with
the independent complete-model and diagnostic assertions unchanged. Compilation
reported an unused Budget::limits observer and a redundant private language self
import shadowing the integration helper's public glob export. The observer is
now cfg(test), matching all real callers; the integration import drops only self.
No semantic implementation, policy, fixture or expected result changed. Parent
explicitly authorized that one test import-line edit after its independent worker
finished. This is warning cleanup, not a claim that later lint/full/doctest gates
already passed. No compiler was run by this worker.
@@ -0,0 +1,32 @@
# Private retained-syntax text producer prerequisite
2026-09-25. Candidate on source baseline `8d52dab97c92cc31baae9f20d8c324b55818aa5d`, selected root contract `harness/analysis-text-producer-contract-20260925.md` at local-only `17836587`. Root publication is parent-owned and server capacity is not reported recovered. This note records authored work, not compiler/runtime acceptance.
An explicit fixture-only `parse_text_producers` route derives bundle, guard and exact policies from CarriedInput. Existing legacy and carried-phase routes pass disabled mode. Budget adds only optional scalar carried/owned/temporary text counts; source/token/entry/reference/numeric Usage remains separate. Named storage checks use the inherited byte ceiling before copy/decode and preserve failed-reservation totals. Quoted text reserves raw payload P, reconciles decoded D<=P, and transfers the existing reservation continuously into owned text without a second fallible reservation. The conservative escaped-fit refusal is intentional only for this new route.
One shared raw scanner body has a bounded before-validation/refund hook. Its original Iterator API uses a no-op hook. A completed quoted token reserves before the existing serde validation; the temporary result String drops at the statement end before refund. Unclosed tokens skip decoding. New-mode storage refusal may precede malformed escape classification; old routes retain that classification. There is no new decoder or numeric conversion.
Typed and ordinary retained names, quoted fields, AST words/references/keys and projected key/reference copies reserve before creation. All three consumed UUID spelling sites drop then refund: ordinary optional IDs, typed identities, and ordinary HistoricalUuid. Nested field originals provisionally count as owned text alongside retained clones, then explicitly drop/refund; top-level fields and field_spans retain both copies. The declaration-kind spelling is borrowed from the original source. Existing duplicate description checks precede replacement/second grammar decoding.
Successful syntax has no outstanding decoder/scanner temporary reservation. Resolver seeds C once, transfers producer T once, and changes the existing complete text walk into an independent checked sum/equality check for this mode. Existing routes keep progressive text reservation. A cfg(test), read-only project inventory helper invokes that same bounded walk on syntax-only fixtures; it cannot replace bundle/policy/baseline authority.
Scope remains logical retained String lengths and named result temporaries. Decoder internal scratch/capacity, existing duplicate-name coordination copies, traversal/entry/metadata producer timing, model conversion/validation, Exact internals, allocator overhead and native stack retain their explicit separate domains. No PendingRename extraction, emitted-output parser transition, accepted result, public source8, persistence or history operation is added.
## Six focused authored tests
All new names are under `mixed::carried::text_producer`, selected by filter `text_producer`. Independent literal/count expectations came from the source-only text-producer oracle, not application outputs.
- Empty real owner E complete cap146/145, finalT8; escaped project cap491/490, finalT41; independent syntax inventory, complete expected Project and Usage parity, and unchanged carried-phase112 control.
- Three-file escaped include cap358/357, C253/T45, final resolved298 and entry25. A separately labeled scanner-only state replay distinguishes scanner reservation from the identical later grammar threshold.
- Projection-dominant syntax cap304/303, finalT83 and independent inventory304; nested/reference/all-three-UUID syntax fixture cap453/452, finalT49/inventory432. The arbitrary payload fixture is explicitly not asserted to admit a Project.
- Raw/escaped Unicode, simple escapes, surrogate pairs and empty strings: rawP cap/refund and scanner hook order; malformed escapes/surrogates, unclosed tokens and explicit legacy error precedence. Arithmetic baselines are labeled separately from real-owner provenance.
- Explicit zero-C stronger route OA/SA complete model, warnings, final retained storage, Usage and numeric-work parity; checked overflow/refund fault injections.
- Project and typed duplicate descriptions with enough room for the first grammar decode but not a second, asserting the existing duplicate errors rather than a false successful overwrite.
No old test or worker-stack allowance changed. Implementer ran no Cargo, test executable or rustfmt; parent owns formatting, compiler, prologue/headroom and runtime gates. Independent production review found no initial semantic blocker; final review binds parent-formatted source and authored tests before commit. Preserve all prior recorded failures and compare actual full-suite dispositions rather than claiming they are unchanged from source inspection.
Parent gates at initial source6b0ef99 passed all six focused tests and completed
the full library with259 passed /16 inherited failures. Strict Clippy rejected
the ordinary quoted hook's is_some/expect pattern. The successor uses direct
if-let matching with the same token span and decoder; no tests, limits or
accounting operations changed. Final source gates remain separately required.
@@ -0,0 +1,102 @@
# Ordinary source8 compatibility implementation
Base `edf096cd33350865ecfc3c65ec266a6c0d3c4000`, selected parent contract
`harness/ordinary-source8-compatibility-contract-20260925.md` at root fb9b0c5c.
The public writer already emits8. The ordinary Parser::unit version list now
accepts8 and its unsupported-version diagnostic names the accurate range.
Current ordinary resolution already checks model limits and returns no migration;
3–7 retain their existing migration paths. No new converter or dispatch adapter
was introduced. Comments distinguish this ordinary repair from private mixed
analysis authoring.
Production change is intentionally confined to that version boundary. Long
analysis constructors retain their explicit conversion refusal, and short typed
declarations remain outside the ordinary grammar. Mixed cfg gates, formatter,
public SourceIndex/update semantics, ID stabilization, resource/depth policies
and codec switches are unchanged. Ordinary8 is not complete analysis/source8.
Six new tests use the ordinary_source8 module filter. They reuse the substantial
six-file source-project example and historical reviewed-prose fixture for complete
model/current formatter and bundle/index idempotence. Other groups cover3–7
migration to stable8; nested included declarations with omitted IDs, UTF-8/CRLF,
comments and actual returned index/identity spans; stale compiled input; short
and long analysis rejection, future/malformed headers and caller limits. Public
checked-save exercises both source and JSON, actual returned bytes/reopen/repeat
save, and rejection of the real formatter's omitted definition catalog while
preserving existing destination bytes. These are authored evaluators, not passing
execution claims. The three-file CLI preview literals additionally compile to an independently constructed complete Project before and after only the team alias changes; actual CLI execution remains a separate parent gate.
The prior test exception below repairs fixture construction without weakening
its semantics; all other prior test bodies/expectations remain unchanged. The16 formerly header-masked
library workflows must run their intended nested/update/migration/transaction
assertions; new downstream failures must be investigated on their own merits.
Parent owns focused/full/integration/CLI/non-test consumer/Clippy/format gates,
including previously unreached ordinary JSON branches. DEPTH_128_VALIDATED stays
false, and ordinary8 does not qualify the private depth candidate, user-deferred
Linux or the separately recorded full-pipeline setup limitation. Source/public
save equality and no-staging-before-validation safeguards remain mandatory.
Only changed Rust files were directly formatted with skip_children; source review
found no production blocker. Compiler/runtime acceptance remains parent-owned.
## Authorized pre-execution evaluator repair
Independent integration inventory found source7_contract's
source7_source_bundle_migration_is_token_only_and_new_link_stays_with_caller
attempted to replace formatted header7 with commented6, although the formatter
already emits8. That replacement was a no-op; the later expected7 was stale too.
Parent authorized this one existing evaluator correction: first assert actual
header8, replace8 with the original commented6 fixture, assert the comment and
single6 token, and expect only6->8 during migration. All exact file/comment/module,
whole-model, caller ownership and recompilation assertions remain, as does the
original test name. This is a static finding before execution, not a reported
runtime failure; no other legacy evaluator or formatter is changed.
## Focused fixture repair after initial execution
Parent's focused run at 33b86a4d completed five tests and failed one: the example
bundle has no trace records, contrary to the new fixture's nonempty assertion.
The repair explicitly authors a Participant trace from fixed OA rescue-team UUID
102 to fixed SA system UUID301, checks endpoint kinds and the complete trace,
and includes it in the unchanged whole-model source/bundle roundtrip assertions.
Historical reviewed-prose trace/baseline coverage remains. Independent source
review also identified the actual short case spellings oa_analysis/sa_analysis;
both now appear in the ordinary refusal table alongside the invalid case aliases.
Production and the other evaluators are unchanged. These corrections are not a
runtime pass claim; the initial five-pass/one-failure record remains preserved.
## Obsolete private header assertions after full execution
Parent's full run at 520e50ce completed 298 tests: 294 passed and four failed.
All 16 previously header-masked ordinary workflows passed. The four failures
were private analysis tests that still expected an empty ordinary schema8
project to be rejected. The scoped repair accepts ordinary8 with no migration,
compares complete ordinary OA/SA projects where the existing fixed-ID fixtures
provide that oracle, and explicitly embeds actual typed source or canonical
text in schema8 to preserve public typed refusal. Declaration refusal coverage
now includes versions3 through8. Existing numeric/lowering/model assertions stay.
Only these evaluator names change:
- inherited_version_root_and_short_word_contracts_do_not_enable_public8 ->
inherited_version_root_and_short_word_contracts_do_not_enable_public_typed8
- public_source_headers_and_existing_numeric_token_controls_are_unchanged ->
public_ordinary_source8_preserves_typed_refusal_and_numeric_controls
Production is unchanged. The initial 294-pass/four-failure evidence remains;
repaired outcomes require the parent's next runtime gate.
## Historical schema4 JSON oracle reconciliation
Parent's r3_migration integration run at 0c07f53a completed four passes and one
failure. Its whole-JSON expected value updated only schema_version, omitting the
current definitions catalog. Project4::into_current already creates an empty
catalog; Project serialization requires that schema8 field. The test now asserts
that the original fixture has no definitions field and the decoded catalog is
empty, then adds exactly definitions:{} to expected JSON. Complete JSON equality,
historical baseline/prose checks, migration and reopen checks remain unchanged.
No production behavior or frozen historical fixture changes.
The parent reports completed core, six other selected integration gates and both
actual CLI operations at 0c07f53a; that evidence remains reusable because this
repair changes only the affected integration evaluator and this note. The initial
r3 four-pass/one-failure record remains preserved. Parent owns the affected r3
inventory/rerun and all-target Clippy/format checks on the repaired pin; this note
does not claim their outcome or repeat unrelated runtime acceptance.