Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f10490b13b | ||
|
|
f232482293 | ||
|
|
3fd87bd8be | ||
|
|
f83f3bf45b | ||
|
|
bab6f96a5b | ||
|
|
8555e417ee | ||
|
|
ddf823a3d3 | ||
|
|
042e25efc2 | ||
|
|
7b06b8ea1b | ||
|
|
0c07f53ab5 | ||
|
|
520e50ce62 | ||
|
|
33b86a4d6b | ||
|
|
edf096cd33 | ||
|
|
4872141c03 | ||
|
|
29417130c7 | ||
|
|
a629ae01d4 | ||
|
|
97a8703adc | ||
|
|
6b0ef99eca |
+66
-11
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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":[]})
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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(¤t).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)
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
//! Owned output only. No parser, extraction or accepted-result conversion.
|
||||
//! Owned unverified emission; consumed actual-output verification is a private child.
|
||||
pub(in crate::language::analysis_source) mod acceptance;
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::source_project::ProjectLimits;
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
//! Actual-byte verification and owning result; no installation or mutable extraction.
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Usage;
|
||||
use crate::language::analysis_source::mixed::{carried, project};
|
||||
use crate::source_project::SourceDiagnostic;
|
||||
|
||||
pub(in crate::language::analysis_source) struct AcceptedRename {
|
||||
bundle: SourceBundle,
|
||||
project: Project,
|
||||
warnings: Vec<SourceDiagnostic>,
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
retained: Slots,
|
||||
verification_usage: Usage,
|
||||
verification_numeric_units: u64,
|
||||
}
|
||||
impl AcceptedRename {
|
||||
pub fn bundle(&self) -> &SourceBundle {
|
||||
&self.bundle
|
||||
}
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
pub fn policy(&self) -> (Limits, ProjectLimits) {
|
||||
(self.limits, self.project_limits)
|
||||
}
|
||||
pub fn retained(&self) -> Slots {
|
||||
self.retained
|
||||
}
|
||||
pub fn verification_work(&self) -> (Usage, u64) {
|
||||
(self.verification_usage, self.verification_numeric_units)
|
||||
}
|
||||
}
|
||||
impl PendingRename<'_> {
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_bundle(&self) -> &SourceBundle {
|
||||
&self.bundle
|
||||
}
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_policy(
|
||||
&self,
|
||||
) -> (Limits, ProjectLimits) {
|
||||
(self.limits, self.project_limits)
|
||||
}
|
||||
pub(in crate::language::analysis_source::mixed) fn carried_slots(&self) -> Slots {
|
||||
self.retained
|
||||
}
|
||||
pub fn verify(self) -> Result<AcceptedRename, Failure> {
|
||||
let (project, warnings, retained, verification_usage, verification_numeric_units) = {
|
||||
let syntax = carried::parse_entry_producers(carried::CarriedInput::from_pending(&self))
|
||||
.map_err(|diagnostics| Failure { diagnostics })?;
|
||||
let mut resolved = project::resolve(syntax)?;
|
||||
let equal = equal_target(&mut resolved, self.desired)?;
|
||||
if !equal {
|
||||
return Err(Failure::typed(
|
||||
&resolved.syntax,
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Verified output differs from the complete rename target.",
|
||||
),
|
||||
));
|
||||
}
|
||||
let retained = warning_inventory(&resolved.warnings)
|
||||
.and_then(|warnings| self.retained.add(warnings, Span::default()))
|
||||
.and_then(|retained| {
|
||||
check_destination(&resolved.syntax.budget, resolved.storage.live, retained)?;
|
||||
Ok(retained)
|
||||
})
|
||||
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
|
||||
let usage = resolved.syntax.budget.usage;
|
||||
let numeric = resolved.syntax.budget.numeric_units();
|
||||
let project::ResolvedMixedProject {
|
||||
syntax,
|
||||
project,
|
||||
warnings,
|
||||
expressions,
|
||||
occurrences,
|
||||
static_limits: _,
|
||||
static_usage: _,
|
||||
storage: _,
|
||||
} = resolved;
|
||||
// The full live ledger remains intact through the checks above.
|
||||
// Only scalar destination accounting escapes these actual drops.
|
||||
drop(expressions);
|
||||
drop(occurrences);
|
||||
drop(syntax);
|
||||
(project, warnings, retained, usage, numeric)
|
||||
};
|
||||
Ok(AcceptedRename {
|
||||
bundle: self.bundle,
|
||||
project,
|
||||
warnings,
|
||||
limits: self.limits,
|
||||
project_limits: self.project_limits,
|
||||
retained,
|
||||
verification_usage,
|
||||
verification_numeric_units,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Every actual Expr root is lowered into this arena. The old pending expression
|
||||
// count cannot authenticate parsed topology. Keep this boundary independently
|
||||
// testable without changing the inherited policy or instrumenting comparison.
|
||||
fn equal_target(
|
||||
resolved: &mut project::ResolvedMixedProject<'_>,
|
||||
desired: &Project,
|
||||
) -> Result<bool, Failure> {
|
||||
let frontier = Slots::entries(resolved.expressions.len());
|
||||
resolved
|
||||
.storage
|
||||
.reserve(&resolved.syntax.budget, frontier, Span::default())
|
||||
.map_err(|e| Failure::typed(&resolved.syntax, None, e))?;
|
||||
let equal = resolved.project.eq(desired);
|
||||
resolved.storage.release(frontier);
|
||||
Ok(equal)
|
||||
}
|
||||
fn check_destination(budget: &Budget, live: Slots, retained: Slots) -> Result<(), TypedError> {
|
||||
let at = Span::default();
|
||||
if retained.entries > live.entries
|
||||
|| retained.occurrences > live.occurrences
|
||||
|| retained.text > live.text
|
||||
{
|
||||
return Err(error(
|
||||
at,
|
||||
"Accepted inventory exceeds live verification storage.",
|
||||
));
|
||||
}
|
||||
budget.lowering_entries(retained.entries, at)?;
|
||||
budget.lowering_occurrences(retained.occurrences, at)?;
|
||||
budget.index_bytes(retained.text, at)
|
||||
}
|
||||
fn warning_inventory(warnings: &[SourceDiagnostic]) -> Result<Slots, TypedError> {
|
||||
let mut inventory = Slots::default();
|
||||
for SourceDiagnostic {
|
||||
path,
|
||||
diagnostic,
|
||||
related,
|
||||
} in warnings
|
||||
{
|
||||
let language::Diagnostic {
|
||||
severity,
|
||||
code,
|
||||
object_id: _,
|
||||
field,
|
||||
line: _,
|
||||
column: _,
|
||||
message,
|
||||
span: _,
|
||||
} = diagnostic;
|
||||
if !related.is_empty() || *severity == validation::Severity::Error {
|
||||
return Err(error(Span::default(), "Unexpected accepted warning shape."));
|
||||
}
|
||||
let text = [
|
||||
path.len(),
|
||||
code.len(),
|
||||
field.as_ref().map_or(0, String::len),
|
||||
message.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.try_fold(0, |n, k| add(n, k, Span::default()))?;
|
||||
inventory = inventory.add(
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text,
|
||||
},
|
||||
Span::default(),
|
||||
)?;
|
||||
}
|
||||
Ok(inventory)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+845
@@ -0,0 +1,845 @@
|
||||
use super::*;
|
||||
use crate::language::{ParseLimits, analysis_source::mixed::parse_with_profile};
|
||||
use crate::{Layer, analysis as a};
|
||||
use project::tests::{id, rich_bundle, rich_model, single};
|
||||
const MISMATCH: &str = "Verified output differs from the complete rename target.";
|
||||
const NEXT: &str =
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
const UNNAMED: &str =
|
||||
"arcadia 8;\nproject next \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
fn limited(b: &SourceBundle, limits: Limits, policy: ProjectLimits) -> AuthoredMixedProject<'_> {
|
||||
project::resolve(parse_with_profile(b, policy, limits).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
}
|
||||
fn authored(b: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
limited(b, profile(100_000, 1_000_000), ProjectLimits::default())
|
||||
}
|
||||
fn expected(name: &str) -> Project {
|
||||
Project {
|
||||
schema_version: 8,
|
||||
id: id(1),
|
||||
alias: "next".into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
fn failure<T>(result: Result<T, Failure>) -> Failure {
|
||||
match result {
|
||||
Ok(_) => panic!("unexpected verification success"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
fn diagnostic(failure: &Failure, message: &str, start: usize, end: usize) {
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
let d = &failure.diagnostics[0];
|
||||
assert_eq!(d.path, "main.arcadia");
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.message, message);
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
fn same_policy(accepted: &AcceptedRename, limits: Limits, policy: ProjectLimits) {
|
||||
let (a, b) = accepted.policy();
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[
|
||||
limits.bytes,
|
||||
limits.tokens,
|
||||
limits.entries,
|
||||
limits.references,
|
||||
limits.frames
|
||||
]
|
||||
);
|
||||
assert_eq!(a.numeric_units, limits.numeric_units);
|
||||
assert_eq!(
|
||||
[
|
||||
b.max_files,
|
||||
b.max_include_depth,
|
||||
b.max_include_edges,
|
||||
b.model.max_source_bytes,
|
||||
b.model.max_graph_objects,
|
||||
b.model.max_records,
|
||||
b.model.max_step_references
|
||||
],
|
||||
[
|
||||
policy.max_files,
|
||||
policy.max_include_depth,
|
||||
policy.max_include_edges,
|
||||
policy.model.max_source_bytes,
|
||||
policy.model.max_graph_objects,
|
||||
policy.model.max_records,
|
||||
policy.model.max_step_references
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn actual_minimal_bytes_full_model_and_late_decode_boundary() {
|
||||
for bytes in [152, 151] {
|
||||
let b = single("");
|
||||
let before = b.clone();
|
||||
let limits = profile(17, bytes);
|
||||
let source = limited(&b, limits, ProjectLimits::default());
|
||||
assert_eq!(source.indexed.resolved.syntax.budget.usage.bytes, 78);
|
||||
let desired = expected("Project");
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
let result = pending.verify();
|
||||
if bytes == 152 {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(
|
||||
accepted.bundle(),
|
||||
&SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), NEXT.into())])
|
||||
}
|
||||
);
|
||||
assert_eq!(accepted.project(), &expected("Project"));
|
||||
assert!(accepted.warnings().is_empty());
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 105
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.verification_work(),
|
||||
(
|
||||
Usage {
|
||||
bytes: 81,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
same_policy(&accepted, limits, ProjectLimits::default());
|
||||
} else {
|
||||
let e = failure(result);
|
||||
diagnostic(&e, "Source8 index/output byte limit exceeded.", 37, 38);
|
||||
assert_eq!(
|
||||
(
|
||||
e.diagnostics[0].diagnostic.line,
|
||||
e.diagnostics[0].diagnostic.column
|
||||
),
|
||||
(2, 27)
|
||||
);
|
||||
}
|
||||
assert_eq!(b, before);
|
||||
assert_eq!(desired, expected("Project"));
|
||||
}
|
||||
}
|
||||
fn warning() -> SourceDiagnostic {
|
||||
SourceDiagnostic {
|
||||
path: "main.arcadia".into(),
|
||||
related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning,
|
||||
code: "unnamed".into(),
|
||||
object_id: Some(id(1)),
|
||||
field: Some("name".into()),
|
||||
message: "Give the project a descriptive name.".into(),
|
||||
span: Span { start: 11, end: 69 },
|
||||
line: 2,
|
||||
column: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn actual_warning_mapping_boundary_and_final_transfer() {
|
||||
for bytes in [161, 160] {
|
||||
let mut b = single("");
|
||||
b.files.insert(
|
||||
"main.arcadia".into(),
|
||||
"arcadia 8;\nproject p \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n"
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(b.files["main.arcadia"].len(), 71);
|
||||
let limits = profile(18, bytes);
|
||||
let source = limited(&b, limits, ProjectLimits::default());
|
||||
assert_eq!(
|
||||
source.warnings()[0].diagnostic.span,
|
||||
Span { start: 11, end: 66 }
|
||||
);
|
||||
let desired = expected("");
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
let result = pending.verify();
|
||||
if bytes == 161 {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(accepted.bundle().files["main.arcadia"], UNNAMED);
|
||||
assert_eq!(accepted.project(), &expected(""));
|
||||
assert_eq!(accepted.warnings(), [warning()]);
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 2,
|
||||
occurrences: 0,
|
||||
text: 157
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.verification_work(),
|
||||
(
|
||||
Usage {
|
||||
bytes: 74,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
same_policy(&accepted, limits, ProjectLimits::default());
|
||||
} else {
|
||||
let e = failure(result);
|
||||
diagnostic(&e, "Source8 index/output byte limit exceeded.", 11, 69);
|
||||
assert_eq!(
|
||||
(
|
||||
e.diagnostics[0].diagnostic.line,
|
||||
e.diagnostics[0].diagnostic.column
|
||||
),
|
||||
(2, 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn owning_result_survives_all_inputs_and_preserves_every_policy_field() {
|
||||
let limits = Limits {
|
||||
bytes: 2048,
|
||||
tokens: 101,
|
||||
entries: 99,
|
||||
references: 31,
|
||||
frames: 17,
|
||||
numeric_units: 8192,
|
||||
};
|
||||
let policy = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 3,
|
||||
max_include_edges: 5,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 3000,
|
||||
max_graph_objects: 67,
|
||||
max_records: 123,
|
||||
max_step_references: 37,
|
||||
},
|
||||
};
|
||||
let accepted = {
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
let pending = limited(&b, limits, policy)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
// A short pending borrow carries a longer independently owned target borrow.
|
||||
{
|
||||
let syntax =
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
|
||||
.unwrap();
|
||||
assert_eq!(syntax.bundle, &pending.bundle);
|
||||
drop(syntax);
|
||||
}
|
||||
pending.verify().unwrap()
|
||||
};
|
||||
assert_eq!(accepted.bundle().files["main.arcadia"], NEXT);
|
||||
assert_eq!(accepted.project(), &expected("Project"));
|
||||
assert!(accepted.warnings().is_empty());
|
||||
same_policy(&accepted, limits, policy);
|
||||
}
|
||||
#[test]
|
||||
fn equal_byte_wrong_models_malformed_and_admission_failure_are_distinct() {
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
for (start, end, replacement) in [(19, 23, "nope"), (25, 32, "Changed"), (8, 9, "?")] {
|
||||
let mut pending = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
pending
|
||||
.bundle
|
||||
.files
|
||||
.get_mut("main.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(start..end, replacement);
|
||||
assert_eq!(pending.bundle.files["main.arcadia"].len(), 81);
|
||||
if start != 8 {
|
||||
let actual = project::resolve(
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(actual.project(), &desired); // Wrong output really is admissible.
|
||||
drop(actual);
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
} else {
|
||||
let e = failure(pending.verify());
|
||||
assert_eq!(e.diagnostics[0].diagnostic.message, "Invalid source token.");
|
||||
}
|
||||
}
|
||||
let b = single(&format!(
|
||||
"oa_activity a \"A\" id \"{}\" {{ parent @b; }}\noa_activity b \"B\" id \"{}\" {{}}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
let text = pending.bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let at = text.find("parent @b").unwrap() + 8;
|
||||
text.replace_range(at..at + 1, "a");
|
||||
let syntax =
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap();
|
||||
drop(syntax); // This corruption is grammar-valid but model-invalid.
|
||||
let e = failure(pending.verify());
|
||||
assert!(
|
||||
e.diagnostics
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.code == "containment_cycle")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn actual_expression_arena_controls_equality_reservation() {
|
||||
let b = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body 10000; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = limited(&b, profile(4096, 4096), ProjectLimits::default());
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, 1);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 195
|
||||
}
|
||||
);
|
||||
pending
|
||||
.bundle
|
||||
.files
|
||||
.get_mut("main.arcadia")
|
||||
.unwrap()
|
||||
.replace_range(160..165, "1 + 2");
|
||||
let mut actual = project::resolve(
|
||||
carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual.expressions.len(), 3);
|
||||
let before = actual.storage.live;
|
||||
assert!(!equal_target(&mut actual, &desired).unwrap());
|
||||
assert_eq!(actual.storage.live, before);
|
||||
// Labeled ledger fault: two free slots would fit old E1, but not actual E3.
|
||||
// This is not an end-to-end late cap or a policy mutation.
|
||||
actual.storage.live.entries = 4094;
|
||||
let e = equal_target(&mut actual, &desired).unwrap_err();
|
||||
diagnostic(&e, "Source8 lowering entry limit exceeded.", 0, 0);
|
||||
assert_eq!(actual.storage.live.entries, 4094);
|
||||
drop(actual);
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
}
|
||||
#[test]
|
||||
fn internal_warning_shape_and_destination_inventory_faults() {
|
||||
assert_eq!(
|
||||
warning_inventory(&[warning()]).unwrap(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 59
|
||||
}
|
||||
);
|
||||
let mut bad = warning();
|
||||
bad.related.push(crate::source_project::SourceLocation {
|
||||
path: "extra".into(),
|
||||
span: Span::default(),
|
||||
});
|
||||
assert_eq!(
|
||||
warning_inventory(&[bad]).unwrap_err().message,
|
||||
"Unexpected accepted warning shape."
|
||||
);
|
||||
let mut bad = warning();
|
||||
bad.diagnostic.severity = validation::Severity::Error;
|
||||
assert!(warning_inventory(&[bad]).is_err());
|
||||
let budget = Budget::new(profile(100, 4096)).unwrap();
|
||||
let live = Slots {
|
||||
entries: 3,
|
||||
occurrences: 2,
|
||||
text: 200,
|
||||
};
|
||||
for retained in [
|
||||
Slots { entries: 4, ..live },
|
||||
Slots {
|
||||
occurrences: 3,
|
||||
..live
|
||||
},
|
||||
Slots { text: 201, ..live },
|
||||
] {
|
||||
assert_eq!(
|
||||
check_destination(&budget, live, retained)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Accepted inventory exceeds live verification storage."
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
Slots::text(usize::MAX)
|
||||
.add(Slots::text(1), Span::default())
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
Slots::entries(usize::MAX)
|
||||
.add(Slots::entries(1), Span::default())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
fn rich_desired(layer: Layer) -> Project {
|
||||
let mut p = rich_model(layer);
|
||||
p.alias = "w".into();
|
||||
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
|
||||
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
|
||||
let d = p.diagrams.get_mut(&id(5)).unwrap();
|
||||
d.alias = "z".into();
|
||||
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
|
||||
for (n, name) in [(10, "g"), (11, "f")] {
|
||||
let def = p.definitions.get_mut(&id(n)).unwrap();
|
||||
def.alias = name.into();
|
||||
let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c.parameters[0].name = "y".into();
|
||||
c.parameters[1].name = "x".into();
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
|
||||
}
|
||||
p
|
||||
}
|
||||
// Independently authored entire expected files: no edit iterator or output text
|
||||
// contributes to these strings. The LF/CRLF transform applies to whole literals.
|
||||
const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n";
|
||||
const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n";
|
||||
fn rich_expected(layer: Layer) -> SourceBundle {
|
||||
let (property, work, diagram, extra) = if layer == Layer::Oa {
|
||||
("oa_property", "oa_activity", "", "")
|
||||
} else {
|
||||
(
|
||||
"sa_property",
|
||||
"sa_function",
|
||||
"layer Sa;",
|
||||
"sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}",
|
||||
)
|
||||
};
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("a.arcadia".into(), RICH_A.into()),
|
||||
("b.arcadia".into(), RICH_B.into()),
|
||||
(
|
||||
"main.arcadia".into(),
|
||||
format!(
|
||||
"arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n"
|
||||
),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn rich_oa_sa_swaps_unicode_crlf_complete_actual_bytes_and_models() {
|
||||
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
|
||||
for unicode in [false, true] {
|
||||
let mut b = rich_bundle(layer);
|
||||
let mut expected = rich_expected(layer);
|
||||
if unicode {
|
||||
for text in b.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
for text in expected.files.values_mut() {
|
||||
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let before = b.clone();
|
||||
let desired = rich_desired(layer);
|
||||
let accepted = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.bundle(), &expected);
|
||||
assert_eq!(accepted.project(), &rich_desired(layer));
|
||||
let (span, line) = if unicode {
|
||||
(
|
||||
Span {
|
||||
start: 351,
|
||||
end: 416,
|
||||
},
|
||||
7,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Span {
|
||||
start: 338,
|
||||
end: 403,
|
||||
},
|
||||
6,
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
accepted.warnings(),
|
||||
leaf_work_warnings("main.arcadia", span, line)
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.retained(),
|
||||
Slots {
|
||||
entries: 5,
|
||||
occurrences: 0,
|
||||
text: if unicode {
|
||||
unicode_bytes + 42 + 227
|
||||
} else {
|
||||
bytes + 42 + 227
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(b, before);
|
||||
assert_eq!(desired, rich_desired(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn leaf_work_warnings(path: &str, span: Span, line: usize) -> [SourceDiagnostic; 2] {
|
||||
// Allocation validation precedes advisory coverage. Both locate the complete
|
||||
// ordinary Work declaration; neither fixture supplies allocation or context.
|
||||
[
|
||||
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
|
||||
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
|
||||
].map(|(code, field, message)| SourceDiagnostic {
|
||||
path: path.into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning, code: code.into(),
|
||||
object_id: Some(id(2)), field: field.map(str::to_owned),
|
||||
message: message.into(), span, line, column: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
fn calculation(p: &mut Project) -> &mut a::CalculationDefinition {
|
||||
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(2)).unwrap().body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c
|
||||
}
|
||||
#[test]
|
||||
fn scoped_small_stack_actual_deep_wide_verification_and_drop() {
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let body = format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
|
||||
id(2)
|
||||
);
|
||||
let b = single(&body);
|
||||
let before = b.clone();
|
||||
let expected_bytes = format!(
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
);
|
||||
for mismatch in [false, true] {
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut target = desired.clone();
|
||||
if result == "List(Number)" {
|
||||
let a::Binding::Expression(a::Expr::List(items)) =
|
||||
&mut calculation(&mut target).body
|
||||
else {
|
||||
panic!("list")
|
||||
};
|
||||
items.pop(); // Actual left remains wide; target-only shape differs.
|
||||
} else {
|
||||
target.name = "Different".into();
|
||||
}
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, nodes);
|
||||
if mismatch {
|
||||
pending.desired = ⌖
|
||||
} // Labeled target-only fault after valid emission.
|
||||
std::thread::scope(|scope| {
|
||||
let expected = &expected_bytes;
|
||||
let desired = &desired;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
let result = pending.verify();
|
||||
if mismatch {
|
||||
diagnostic(&failure(result), MISMATCH, 0, 0);
|
||||
} else {
|
||||
let accepted = result.unwrap();
|
||||
assert_eq!(&accepted.bundle().files["main.arcadia"], expected);
|
||||
assert_eq!(accepted.project(), desired);
|
||||
drop(accepted);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(b, before);
|
||||
}
|
||||
}
|
||||
// Genuine inherited151 profile reaches a late parser refusal inside worker.
|
||||
let b = single("");
|
||||
let desired = expected("Project");
|
||||
let pending = limited(&b, profile(17, 151), ProjectLimits::default())
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
std::thread::scope(|scope| {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
diagnostic(
|
||||
&failure(pending.verify()),
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
37,
|
||||
38,
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn zero_actual_expression_rejects_large_target_only_variants() {
|
||||
let b = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
|
||||
id(2)
|
||||
));
|
||||
let mut deep = a::Expr::Boolean(true);
|
||||
for _ in 0..2048 {
|
||||
deep = a::Expr::Unary {
|
||||
op: a::UnaryOp::Not,
|
||||
operand: Box::new(deep),
|
||||
};
|
||||
}
|
||||
for expression in [deep, a::Expr::List(vec![a::Expr::Boolean(true); 4096])] {
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let mut damaged = desired.clone();
|
||||
calculation(&mut damaged).body = a::Binding::Expression(expression);
|
||||
let mut pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap();
|
||||
assert_eq!(pending.expression_nodes, 0);
|
||||
pending.desired = &damaged;
|
||||
diagnostic(&failure(pending.verify()), MISMATCH, 0, 0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn signed_zero_geometry_scalar_and_historical_equality_moves_actual_model() {
|
||||
let b = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "next".into();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.0;
|
||||
let accepted = authored(&b)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.project(), &desired);
|
||||
assert_eq!(
|
||||
accepted.project().diagrams[&id(5)].nodes[&id(4)]
|
||||
.position
|
||||
.x
|
||||
.to_bits(),
|
||||
0.0f32.to_bits()
|
||||
);
|
||||
let b = single(&format!(
|
||||
r#"
|
||||
oa_data_type a "A" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
sa_data_type b "B" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }}
|
||||
trace t "T" id "{}" {{ oa @a; sa @b; kind Item; baseline {{
|
||||
oa {{ name "A"; description ""; }}; sa {{ name "B"; description ""; }};
|
||||
semantic {{ facets [{{ Data {{ RealMinimum {{ oa {{ Value 0.0; }}; sa {{ Value 0.0; }}; }}; }}; }}]; }};
|
||||
}}; }}
|
||||
"#,
|
||||
id(2),
|
||||
id(3),
|
||||
id(4)
|
||||
));
|
||||
let source = authored(&b);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
for e in desired.elements.values_mut() {
|
||||
let body = match &mut e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("data")
|
||||
};
|
||||
let crate::DataTypeDefinition::Real { minimum, .. } = definition else {
|
||||
panic!("real")
|
||||
};
|
||||
*minimum = Some(-0.0);
|
||||
*default = Some(crate::DataValue::Real(-0.0));
|
||||
}
|
||||
let facet = &mut desired
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets[0];
|
||||
let crate::semantic::FacetReview::Data(crate::semantic::DataReview::RealMinimum(review)) =
|
||||
facet
|
||||
else {
|
||||
panic!("historical")
|
||||
};
|
||||
review.oa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
review.sa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
let accepted = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.project(), &desired);
|
||||
for e in accepted.project().elements.values() {
|
||||
let body = match &e.body {
|
||||
crate::ElementBody::Oa(crate::OaBody::Shared(b))
|
||||
| crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b,
|
||||
_ => panic!("data"),
|
||||
};
|
||||
let crate::SharedBody::DataType {
|
||||
definition:
|
||||
crate::DataTypeDefinition::Real {
|
||||
minimum: Some(minimum),
|
||||
..
|
||||
},
|
||||
default: Some(crate::DataValue::Real(value)),
|
||||
..
|
||||
} = body
|
||||
else {
|
||||
panic!("real")
|
||||
};
|
||||
assert_eq!(minimum.to_bits(), 0.0f64.to_bits());
|
||||
assert_eq!(value.to_bits(), 0.0f64.to_bits());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() {
|
||||
let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
|
||||
("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
|
||||
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()),
|
||||
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
|
||||
])};
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into();
|
||||
desired.elements.get_mut(&id(2)).unwrap().alias = "f".into();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
let pending = pending.verify().unwrap();
|
||||
assert_eq!(pending.project(), &desired);
|
||||
assert_eq!(
|
||||
pending.warnings(),
|
||||
leaf_work_warnings("b.arcadia", Span { start: 25, end: 90 }, 2)
|
||||
);
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 6,
|
||||
occurrences: 0,
|
||||
text: 526 + 221
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
¶meter,
|
||||
&fails(¶meter, 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);
|
||||
}
|
||||
@@ -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(_))
|
||||
));
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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(&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
|
||||
|
||||
@@ -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
|
||||
------------------------
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user