Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d52dab97c | ||
|
|
19a4082117 |
@@ -660,3 +660,70 @@ fn ordinary_position_key_copy_is_counted_at_existing_postparse_text_phase() {
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emission_refuses_changed_carried_even_zero_and_preserves_noop_guards() {
|
||||
for retained in [false, true] {
|
||||
let held = owner(empty(), profile(100, 4096));
|
||||
let seed =
|
||||
parse_with_profile(&held.bundle, ProjectLimits::default(), profile(100, 4096)).unwrap();
|
||||
for changed in [false, true] {
|
||||
let input = if retained {
|
||||
held.input()
|
||||
} else {
|
||||
CarriedInput::borrowed(&held.bundle, &seed)
|
||||
};
|
||||
let parsed = parse_carried(input).unwrap();
|
||||
let original_guard = parsed.carried.as_ref().unwrap().slots();
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
let source = resolved.index().unwrap().index_ordinary().unwrap();
|
||||
let mut desired = source.project().clone();
|
||||
if changed {
|
||||
desired.alias = "next".into();
|
||||
}
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
if changed {
|
||||
let error = match plan.emit_changed_unverified(&held.bundle) {
|
||||
Ok(_) => panic!("carried changed emission must refuse"),
|
||||
Err(e) => e,
|
||||
};
|
||||
diagnostic(
|
||||
&error.diagnostics[0],
|
||||
"main.arcadia",
|
||||
0,
|
||||
0,
|
||||
"Changed emission requires original legacy syntax.",
|
||||
);
|
||||
} else {
|
||||
let source = plan.into_unchanged(&held.bundle).unwrap();
|
||||
assert!(std::ptr::eq(source.bundle(), &held.bundle));
|
||||
// The intact owner can be consumed through another no-op plan;
|
||||
// no production getter exposes or replaces its guard.
|
||||
let source = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.into_unchanged(&held.bundle)
|
||||
.unwrap();
|
||||
assert!(std::ptr::eq(source.bundle(), &held.bundle));
|
||||
let mut changed_desired = desired.clone();
|
||||
changed_desired.alias = "next".into();
|
||||
assert!(
|
||||
source
|
||||
.plan_rename(&changed_desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&held.bundle)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
original_guard,
|
||||
if retained {
|
||||
held.retained
|
||||
} else {
|
||||
Slots::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(held.bundle.files["main.arcadia"], doc(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Complete-target rename planning only; no output emission or admission.
|
||||
//! Complete-target rename planning and private unverified emission; no admission.
|
||||
pub(in crate::language::analysis_source) mod emission;
|
||||
use super::*;
|
||||
use crate::source_project::SourceBundle;
|
||||
use lower::Target;
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
//! Owned output only. No parser, extraction or accepted-result conversion.
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::source_project::ProjectLimits;
|
||||
|
||||
pub(in crate::language::analysis_source) struct PendingRename<'t> {
|
||||
bundle: SourceBundle,
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
retained: Slots,
|
||||
expression_nodes: usize,
|
||||
desired: &'t Project,
|
||||
}
|
||||
|
||||
impl<'s, 't> RenamePlan<'s, 't> {
|
||||
pub fn into_unchanged(
|
||||
self,
|
||||
current: &SourceBundle,
|
||||
) -> Result<AuthoredMixedProject<'s>, Failure> {
|
||||
self.validate_current(current)?;
|
||||
self.validate()?;
|
||||
if !self.names.is_empty() {
|
||||
return Err(self.failure(None, error(Span::default(), "Rename plan changes names.")));
|
||||
}
|
||||
Ok(self.source)
|
||||
}
|
||||
|
||||
pub fn emit_changed_unverified(
|
||||
mut self,
|
||||
current: &SourceBundle,
|
||||
) -> Result<PendingRename<'t>, Failure> {
|
||||
self.validate_current(current)?;
|
||||
self.validate()?;
|
||||
if self.names.is_empty() {
|
||||
return Err(self.failure(None, error(Span::default(), "Rename plan is unchanged.")));
|
||||
}
|
||||
if self.source.indexed.resolved.syntax.carried.is_some() {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Changed emission requires original legacy syntax.",
|
||||
),
|
||||
));
|
||||
}
|
||||
let total = self.output_bytes()?;
|
||||
let retained =
|
||||
output_slots(self.source.bundle(), total).map_err(|e| self.failure(None, e))?;
|
||||
let resolved = &mut self.source.indexed.resolved;
|
||||
resolved
|
||||
.storage
|
||||
.reserve(&resolved.syntax.budget, retained, Span::default())
|
||||
.map_err(|e| self.failure(None, e))?;
|
||||
|
||||
// The complete output reservation precedes every owned byte and entry.
|
||||
let mut files = BTreeMap::new();
|
||||
let mut produced = 0;
|
||||
for (file, path, original) in self.source.files() {
|
||||
let result = (|| {
|
||||
let edits = || {
|
||||
self.edits
|
||||
.range((file, 0, 0)..=(file, usize::MAX, usize::MAX))
|
||||
};
|
||||
let mut removed = 0;
|
||||
let mut replacement = 0;
|
||||
for (&(_, start, end), target) in edits() {
|
||||
let at = Span { start, end };
|
||||
removed = add(
|
||||
removed,
|
||||
end.checked_sub(start)
|
||||
.ok_or_else(|| error(at, "Invalid rename token size."))?,
|
||||
at,
|
||||
)?;
|
||||
replacement = add(replacement, self.names[target].len(), at)?;
|
||||
}
|
||||
let size = add(
|
||||
original.len().checked_sub(removed).ok_or_else(|| {
|
||||
error(Span::default(), "Rename removals exceed original file.")
|
||||
})?,
|
||||
replacement,
|
||||
Span::default(),
|
||||
)?;
|
||||
let mut output = String::with_capacity(size);
|
||||
let mut cursor = 0;
|
||||
for (&(_, start, end), target) in edits() {
|
||||
let at = Span { start, end };
|
||||
output.push_str(
|
||||
original
|
||||
.get(cursor..start)
|
||||
.ok_or_else(|| error(at, "Invalid rename output range."))?,
|
||||
);
|
||||
output.push_str(self.names[target]);
|
||||
cursor = end;
|
||||
}
|
||||
output.push_str(
|
||||
original
|
||||
.get(cursor..)
|
||||
.ok_or_else(|| error(Span::default(), "Invalid rename output tail."))?,
|
||||
);
|
||||
if output.len() != size {
|
||||
return Err(error(Span::default(), "Rename output length mismatch."));
|
||||
}
|
||||
produced = add(produced, size, Span::default())?;
|
||||
Ok(output)
|
||||
})();
|
||||
let output = result.map_err(|e| self.failure(Some(file), e))?;
|
||||
files.insert(path.to_owned(), output);
|
||||
}
|
||||
if files.len() != retained.entries || produced != total {
|
||||
return Err(self.failure(None, error(Span::default(), "Incomplete rename output.")));
|
||||
}
|
||||
let bundle = SourceBundle {
|
||||
entry: self.source.bundle().entry.clone(),
|
||||
files,
|
||||
};
|
||||
let limits = self.source.indexed.resolved.syntax.budget.limits();
|
||||
let project_limits = self.source.indexed.resolved.syntax.limits;
|
||||
let expression_nodes = self.source.indexed.resolved.expressions.len();
|
||||
let Self {
|
||||
source,
|
||||
desired,
|
||||
names,
|
||||
edits,
|
||||
} = self;
|
||||
drop(names);
|
||||
drop(edits);
|
||||
drop(source);
|
||||
Ok(PendingRename {
|
||||
bundle,
|
||||
limits,
|
||||
project_limits,
|
||||
retained,
|
||||
expression_nodes,
|
||||
desired,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn output_slots(bundle: &SourceBundle, total: usize) -> Result<Slots, TypedError> {
|
||||
let mut text = add(total, bundle.entry.len(), Span::default())?;
|
||||
for path in bundle.files.keys() {
|
||||
text = add(text, path.len(), Span::default())?;
|
||||
}
|
||||
Ok(Slots {
|
||||
entries: bundle.files.len(),
|
||||
occurrences: 0,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+646
@@ -0,0 +1,646 @@
|
||||
use super::*;
|
||||
use crate::Layer;
|
||||
use crate::language::ParseLimits;
|
||||
use crate::language::analysis_source::mixed::{parse_with_profile, project};
|
||||
use project::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
|
||||
fn authored(bundle: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap().index_ordinary().unwrap()
|
||||
}
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
fn limited(
|
||||
bundle: &SourceBundle,
|
||||
limits: Limits,
|
||||
policy: ProjectLimits,
|
||||
) -> AuthoredMixedProject<'_> {
|
||||
project::resolve(parse_with_profile(bundle, policy, limits).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
}
|
||||
fn chain() -> SourceBundle {
|
||||
let mut b = single("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 failure<T>(result: Result<T, Failure>, text: &str) -> Failure {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("unexpected success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics.len(), 1);
|
||||
assert!(e.diagnostics[0].diagnostic.message.contains(text), "{e:?}");
|
||||
e
|
||||
}
|
||||
fn same_policy(output: &PendingRename<'_>, limits: Limits, policy: ProjectLimits) {
|
||||
let a = output.limits;
|
||||
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);
|
||||
let a = output.project_limits;
|
||||
assert_eq!(
|
||||
[
|
||||
a.max_files,
|
||||
a.max_include_depth,
|
||||
a.max_include_edges,
|
||||
a.model.max_source_bytes,
|
||||
a.model.max_graph_objects,
|
||||
a.model.max_records,
|
||||
a.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
|
||||
]
|
||||
);
|
||||
}
|
||||
const NEXT: &str =
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
|
||||
const NEXT_CHAIN: &str = "arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"part.arcadia\";\n}\n";
|
||||
#[test]
|
||||
fn exact_real_single_and_chain_overlap_caps_and_complete_owned_bytes() {
|
||||
for (bundle, entries, bytes, before, output_entries, output_text, expected) in [
|
||||
(
|
||||
single(""),
|
||||
17,
|
||||
114,
|
||||
Slots {
|
||||
entries: 16,
|
||||
occurrences: 0,
|
||||
text: 9,
|
||||
},
|
||||
1,
|
||||
105,
|
||||
NEXT,
|
||||
),
|
||||
(
|
||||
chain(),
|
||||
29,
|
||||
216,
|
||||
Slots {
|
||||
entries: 26,
|
||||
occurrences: 0,
|
||||
text: 33,
|
||||
},
|
||||
3,
|
||||
183,
|
||||
NEXT_CHAIN,
|
||||
),
|
||||
] {
|
||||
for (entry_cap, byte_cap, success) in [
|
||||
(entries, bytes, true),
|
||||
(entries - 1, bytes, false),
|
||||
(entries, bytes - 1, false),
|
||||
] {
|
||||
let limits = profile(entry_cap, byte_cap);
|
||||
let policy = ProjectLimits::default();
|
||||
let source = limited(&bundle, limits, policy);
|
||||
let mut desired = source.project().clone();
|
||||
desired.alias = "next".into();
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.source.indexed.resolved.storage.live, before);
|
||||
let result = plan.emit_changed_unverified(&bundle);
|
||||
if success {
|
||||
let output = result.unwrap();
|
||||
assert_eq!(
|
||||
output.retained,
|
||||
Slots {
|
||||
entries: output_entries,
|
||||
occurrences: 0,
|
||||
text: output_text
|
||||
}
|
||||
);
|
||||
assert_eq!(output.bundle.entry, "main.arcadia");
|
||||
let mut expected_files = BTreeMap::from([("main.arcadia".into(), expected.into())]);
|
||||
if output_entries == 3 {
|
||||
expected_files.insert(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
);
|
||||
expected_files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
}
|
||||
assert_eq!(output.bundle.files, expected_files);
|
||||
assert_eq!(output.expression_nodes, 0);
|
||||
assert!(std::ptr::eq(output.desired, &desired));
|
||||
same_policy(&output, limits, policy);
|
||||
} else {
|
||||
let e = failure(result, "limit exceeded");
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(e.diagnostics[0].diagnostic.span, Span::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn noop_returns_identical_owner_below_output_overlap_and_releases_desired_borrow() {
|
||||
for (bundle, entries, bytes) in [(single(""), 16, 78), (chain(), 26, 132)] {
|
||||
let source = limited(&bundle, profile(entries, bytes), ProjectLimits::default());
|
||||
let storage = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let numeric = source.indexed.resolved.syntax.budget.numeric_units();
|
||||
let names = source.indexed.data.names.values().next().unwrap().as_ptr();
|
||||
let warnings = source.warnings().to_vec();
|
||||
let result = {
|
||||
let desired = source.project().clone();
|
||||
source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.into_unchanged(&bundle)
|
||||
.unwrap()
|
||||
};
|
||||
assert!(std::ptr::eq(result.bundle(), &bundle));
|
||||
assert_eq!(
|
||||
result.indexed.data.names.values().next().unwrap().as_ptr(),
|
||||
names
|
||||
);
|
||||
assert_eq!(result.indexed.resolved.storage.live, storage);
|
||||
assert_eq!(result.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
result.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(result.warnings(), warnings);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn wrong_branches_and_complete_stale_current_refuse_before_output() {
|
||||
let bundle = chain();
|
||||
let original = authored(&bundle).project().clone();
|
||||
let mut desired = original.clone();
|
||||
desired.alias = "next".into();
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&original)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle),
|
||||
"unchanged",
|
||||
);
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.into_unchanged(&bundle),
|
||||
"changes names",
|
||||
);
|
||||
for case in 0..6 {
|
||||
let mut current = bundle.clone();
|
||||
match case {
|
||||
0 => current.entry = "last.arcadia".into(),
|
||||
1 => current.files.get_mut("last.arcadia").unwrap().push(' '),
|
||||
2 => current.files.get_mut("main.arcadia").unwrap().push(' '),
|
||||
3 => {
|
||||
current.files.remove("last.arcadia");
|
||||
}
|
||||
4 => {
|
||||
current.files.insert("extra.arcadia".into(), String::new());
|
||||
}
|
||||
_ => {
|
||||
let text = current.files.remove("last.arcadia").unwrap();
|
||||
current.files.insert("renamed.arcadia".into(), text);
|
||||
}
|
||||
}
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&original)
|
||||
.unwrap()
|
||||
.into_unchanged(¤t),
|
||||
"Current complete bundle",
|
||||
);
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(¤t),
|
||||
"Current complete bundle",
|
||||
);
|
||||
}
|
||||
let current = bundle.clone();
|
||||
assert_eq!(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(¤t)
|
||||
.unwrap()
|
||||
.bundle
|
||||
.files["main.arcadia"],
|
||||
NEXT_CHAIN
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn existing_edit_validation_runs_before_both_transitions() {
|
||||
let bundle = single("");
|
||||
let original = authored(&bundle).project().clone();
|
||||
let mut desired = original.clone();
|
||||
desired.alias = "next".into();
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.clear();
|
||||
failure(
|
||||
plan.emit_changed_unverified(&bundle),
|
||||
"missing a required target token",
|
||||
);
|
||||
let mut plan = authored(&bundle).plan_rename(&original).unwrap();
|
||||
plan.edits.insert((0, 19, 20), Target::Global(id(1)));
|
||||
failure(plan.into_unchanged(&bundle), "unchanged name");
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.clear();
|
||||
plan.edits
|
||||
.insert((0, usize::MAX, usize::MAX), Target::Global(id(1)));
|
||||
failure(plan.emit_changed_unverified(&bundle), "nonempty");
|
||||
// Arithmetic-only fault injection, not a real bundle large enough to overflow.
|
||||
assert!(output_slots(&bundle, usize::MAX).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn exact_all_policy_fields_and_pending_outlives_original_bundle() {
|
||||
let desired = {
|
||||
let b = single("");
|
||||
let mut p = authored(&b).project().clone();
|
||||
p.alias = "next".into();
|
||||
p
|
||||
};
|
||||
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 pending = {
|
||||
let b = single("");
|
||||
limited(&b, limits, policy)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&b)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
same_policy(&pending, limits, policy);
|
||||
}
|
||||
#[test]
|
||||
fn scoped_small_stack_changed_unchanged_and_failure_drop() {
|
||||
let bundle = single("");
|
||||
let original = authored(&bundle).project().clone();
|
||||
let mut changed = original.clone();
|
||||
changed.alias = "next".into();
|
||||
std::thread::scope(|scope| {
|
||||
for mode in 0..3 {
|
||||
let plan = limited(
|
||||
&bundle,
|
||||
profile(17, if mode == 2 { 113 } else { 114 }),
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.plan_rename(if mode == 1 { &original } else { &changed })
|
||||
.unwrap();
|
||||
let b = &bundle;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
if mode == 1 {
|
||||
let owner = plan.into_unchanged(b).unwrap();
|
||||
assert!(std::ptr::eq(owner.bundle(), b));
|
||||
drop(owner);
|
||||
} else if mode == 0 {
|
||||
let pending = plan.emit_changed_unverified(b).unwrap();
|
||||
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
|
||||
drop(pending);
|
||||
} else {
|
||||
drop(failure(plan.emit_changed_unverified(b), "limit exceeded"));
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
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 \"00000000-0000-0000-0000-000000000002\" {{ result {result}; body {expression}; }}"
|
||||
);
|
||||
let bundle = single(&body);
|
||||
let before = bundle.clone();
|
||||
let expected = format!(
|
||||
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
);
|
||||
for changed in [false, true] {
|
||||
// Complete prerequisites stay outside the bounded emitter worker.
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
|
||||
let mut desired = source.project().clone();
|
||||
if changed {
|
||||
desired.alias = "next".into();
|
||||
}
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
std::thread::scope(|scope| {
|
||||
let bundle = &bundle;
|
||||
let expected = &expected;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
if changed {
|
||||
let pending = plan.emit_changed_unverified(bundle).unwrap();
|
||||
assert_eq!(pending.expression_nodes, nodes);
|
||||
assert_eq!(&pending.bundle.files["main.arcadia"], expected);
|
||||
drop(pending);
|
||||
} else {
|
||||
let owner = plan.into_unchanged(bundle).unwrap();
|
||||
assert!(std::ptr::eq(owner.bundle(), bundle));
|
||||
assert_eq!(owner.indexed.resolved.expressions.len(), nodes);
|
||||
drop(owner);
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(bundle, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 whole_rich_oa_sa_scoped_global_swaps_and_unicode_crlf_bytes() {
|
||||
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
|
||||
for unicode in [false, true] {
|
||||
let mut bundle = rich_bundle(layer);
|
||||
let mut expected = rich_expected(layer);
|
||||
if unicode {
|
||||
for text in bundle.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 desired = rich_desired(layer);
|
||||
let source = authored(&bundle);
|
||||
let original_project = source.project().clone();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(pending.expression_nodes, 15);
|
||||
let count = if unicode { unicode_bytes } else { bytes };
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: count + 42
|
||||
}
|
||||
);
|
||||
assert_eq!(authored(&bundle).project(), &original_project);
|
||||
assert_eq!(desired, rich_desired(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[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();
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 4,
|
||||
occurrences: 0,
|
||||
text: 526
|
||||
}
|
||||
);
|
||||
assert_eq!(pending.expression_nodes, 2);
|
||||
}
|
||||
const CATEGORY_EXPECTED: &str = r#"arcadia 8;
|
||||
project a "Authored @p child" id "00000000-0000-0000-0000-000000000001" {
|
||||
description "Do not index @child, @link or uuid in prose 雨";
|
||||
oa_activity p "Parent child" id "00000000-0000-0000-0000-000000000002" {
|
||||
oa_activity implicit "Explicit child" id "00000000-0000-0000-0000-000000000003" { parent @ // @noise child
|
||||
p; }
|
||||
oa_activity child "Implicit child" id "00000000-0000-0000-0000-000000000004" { description "child @a"; }
|
||||
}
|
||||
sa_function link "Selected" id "00000000-0000-0000-0000-000000000005" {}
|
||||
interaction s "Link" id "00000000-0000-0000-0000-000000000006" { source @implicit; target @child; }
|
||||
diagram note "View" id "00000000-0000-0000-0000-000000000007" {
|
||||
routes [{ edge @s; }];
|
||||
view_options { hidden_edges [@s]; };
|
||||
}
|
||||
artifact d "Evidence" id "00000000-0000-0000-0000-000000000008" { owner @s; uri "docs/@child.md"; }
|
||||
trace q "Review" id "00000000-0000-0000-0000-000000000009" {
|
||||
oa @implicit; sa @link; kind Manual;
|
||||
baseline {
|
||||
oa { name "child"; description "@child"; };
|
||||
sa { name "s"; description "@s"; };
|
||||
semantic { facets [{ Field { Type { oa { Value { Named uuid "00000000-0000-0000-0000-000000000003"; }; }; sa { Value Boolean; }; }; }; }]; };
|
||||
};
|
||||
}
|
||||
oa_process t "Caller" id "00000000-0000-0000-0000-000000000010" { steps [@sub, @left]; }
|
||||
oa_process use "Callee" id "00000000-0000-0000-0000-000000000011" { steps [@right]; }
|
||||
oa_occurrence sub "Call" id "00000000-0000-0000-0000-000000000012" { parent @t; body { SequenceReference { sequence @use; }; }; }
|
||||
oa_occurrence right "Left" id "00000000-0000-0000-0000-000000000013" { parent @use; body { Action { action @implicit; lifeline none; }; }; }
|
||||
oa_occurrence left "Right" id "00000000-0000-0000-0000-000000000014" { parent @t; body { Action { action @child; lifeline none; }; }; }
|
||||
contextual_link ctx "Context" id "00000000-0000-0000-0000-000000000015" { owner @t; source { references [@sub]; occurrence @right; }; target { references []; occurrence @left; }; purpose { Precedence { guard "@child"; }; }; }
|
||||
}
|
||||
"#;
|
||||
#[test]
|
||||
fn complete_categories_preserve_trivia_implicit_parent_and_historical_uuid_bytes() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
include_str!("../../tests/fixtures/categories.arcadia").into(),
|
||||
)]),
|
||||
};
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
let replacements = [
|
||||
"a", "p", "implicit", "child", "link", "s", "note", "d", "q", "t", "use", "sub", "right",
|
||||
"left", "ctx",
|
||||
];
|
||||
desired.alias = "a".into();
|
||||
for e in desired.elements.values_mut() {
|
||||
e.alias = replacements[(1..=15).find(|n| id(*n) == e.id).unwrap() - 1].into();
|
||||
}
|
||||
for r in desired.relations.values_mut() {
|
||||
r.alias = replacements[(1..=15).find(|n| id(*n) == r.id).unwrap() - 1].into();
|
||||
}
|
||||
desired.diagrams.get_mut(&id(7)).unwrap().alias = "note".into();
|
||||
desired.artifact_links.get_mut(&id(8)).unwrap().alias = "d".into();
|
||||
desired.traces.get_mut(&id(9)).unwrap().alias = "q".into();
|
||||
let pending = source
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(&bundle)
|
||||
.unwrap();
|
||||
let expected = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
format!(
|
||||
"{}\r\n",
|
||||
CATEGORY_EXPECTED
|
||||
.trim_end_matches('\n')
|
||||
.replace("@noise child\n", "@noise child\r\n")
|
||||
),
|
||||
)]),
|
||||
};
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 2169
|
||||
}
|
||||
);
|
||||
assert_eq!(pending.expression_nodes, 0);
|
||||
}
|
||||
#[test]
|
||||
fn combined_growth_shrinkage_emits_original_coordinates_and_complete_untouched_file() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("main.arcadia").unwrap() =
|
||||
bundle.files["main.arcadia"].replace("node n ", "node nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn ");
|
||||
let source = authored(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.alias = "n".into();
|
||||
desired.definitions.get_mut(&id(10)).unwrap().alias = "renamed_f".into();
|
||||
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
|
||||
("a.arcadia".into(),"calculation renamed_f \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput x id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput y id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $x + $y;\n}\n".into()),
|
||||
("b.arcadia".into(),"calculation g \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput x id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput y id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $x - $y;\n}\n".into()),
|
||||
("main.arcadia".into(),"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode n id \"00000000-0000-0000-0000-000000000004\" { diagram @d; element @v; }\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @renamed_f(y: 2, x: @g(x: 3, y: 4)) + @renamed_f(x: 5, y: 6); }\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\ndiagram d \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" { }\n\n}\n".into()),
|
||||
])};
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.output_bytes().unwrap(), 924);
|
||||
let pending = plan.emit_changed_unverified(&bundle).unwrap();
|
||||
assert_eq!(pending.bundle, expected);
|
||||
assert_eq!(
|
||||
pending.retained,
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: 966
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -40,266 +40,282 @@ pub(super) fn binding(p: &mut Cursor<'_, '_>) -> Result<Binding, Error> {
|
||||
while let Some(task) = tasks.pop() {
|
||||
p.budget.active()?;
|
||||
match task {
|
||||
Task::Expression(minimum) => {
|
||||
let start = p.span().start;
|
||||
if p.text() != "(" {
|
||||
p.budget.entry(p.span())?;
|
||||
}
|
||||
tasks.push(Task::Continue(minimum));
|
||||
match p.text() {
|
||||
"true" | "false" => {
|
||||
let value = p.text() == "true";
|
||||
let span = p.span();
|
||||
p.cursor += 1;
|
||||
values.push(p.node(ExpressionKind::Boolean(value), span)?);
|
||||
}
|
||||
"-" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Unary(start, UnaryOp::Negate));
|
||||
tasks.push(Task::Expression(7));
|
||||
}
|
||||
"(" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Group(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"[" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(SequenceKind::List, start)));
|
||||
}
|
||||
"@" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
if p.text() == "(" {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(
|
||||
SequenceKind::Call(reference),
|
||||
start,
|
||||
)));
|
||||
} else {
|
||||
let end = reference.span.end;
|
||||
values.push(
|
||||
p.node(ExpressionKind::Property(reference), Span { start, end })?,
|
||||
);
|
||||
}
|
||||
}
|
||||
"$" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
let end = reference.span.end;
|
||||
values.push(
|
||||
p.node(ExpressionKind::Parameter(reference), Span { start, end })?,
|
||||
);
|
||||
}
|
||||
"ratio" => {
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
"abs" | "not" | "if" | "sum" => {
|
||||
let kind = match p.text() {
|
||||
"abs" => SequenceKind::Unary(UnaryOp::Absolute),
|
||||
"not" => SequenceKind::Unary(UnaryOp::Not),
|
||||
"if" => SequenceKind::If,
|
||||
_ => SequenceKind::Sum,
|
||||
};
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Next(Sequence::new(kind, start)));
|
||||
}
|
||||
"power" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Power(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"as_kind" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
p.take("@")?;
|
||||
let kind = p.reference()?;
|
||||
p.take(",")?;
|
||||
tasks.push(Task::AsKind(start, kind));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
_ if p
|
||||
.tokens
|
||||
.get(p.cursor)
|
||||
.is_some_and(|token| token.class == Class::Number) =>
|
||||
{
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
_ => return Err(error(p.span(), "Expected an analysis expression.")),
|
||||
}
|
||||
}
|
||||
Task::Continue(minimum) => {
|
||||
if let Some((op, precedence)) =
|
||||
operator(p.text()).filter(|(_, prec)| *prec >= minimum)
|
||||
{
|
||||
let left = values.pop().expect("completed Pratt operand");
|
||||
if comparison(op) && ungrouped_comparison(&left) {
|
||||
return Err(error(p.span(), "Comparison children require parentheses."));
|
||||
}
|
||||
p.budget.entry(p.span())?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Continue(minimum));
|
||||
tasks.push(Task::Binary(left, op));
|
||||
tasks.push(Task::Expression(precedence + 1));
|
||||
}
|
||||
}
|
||||
Task::Binary(left, op) => {
|
||||
let right = values.pop().expect("completed right operand");
|
||||
if comparison(op) && ungrouped_comparison(&right) {
|
||||
return Err(error(
|
||||
right.span,
|
||||
"Comparison children require parentheses.",
|
||||
));
|
||||
}
|
||||
let span = Span {
|
||||
start: left.span.start,
|
||||
end: right.span.end,
|
||||
};
|
||||
values.push(p.node(
|
||||
ExpressionKind::Binary {
|
||||
op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
},
|
||||
span,
|
||||
)?);
|
||||
}
|
||||
Task::Unary(start, op) => {
|
||||
let operand = values.pop().expect("completed unary operand");
|
||||
let end = operand.span.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Unary {
|
||||
op,
|
||||
operand: Box::new(operand),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::Group(start) => {
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
let value = values.last_mut().expect("completed grouped expression");
|
||||
value.grouped = true;
|
||||
value.span = Span { start, end };
|
||||
}
|
||||
Task::Next(mut sequence) => {
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() == closing {
|
||||
let end = p.take(closing)?.end;
|
||||
p.close();
|
||||
values.push(p.sequence(sequence, end)?);
|
||||
} else {
|
||||
let count = sequence.values.len();
|
||||
let maximum = match sequence.kind {
|
||||
SequenceKind::List => crate::analysis::MAX_LIST_LENGTH,
|
||||
SequenceKind::Unary(_) => 1,
|
||||
SequenceKind::If => 3,
|
||||
SequenceKind::Sum => 2,
|
||||
SequenceKind::Call(_) => usize::MAX,
|
||||
};
|
||||
if count >= maximum {
|
||||
return Err(error(
|
||||
p.span(),
|
||||
"Too many list members or constructor arguments.",
|
||||
));
|
||||
}
|
||||
let key = if matches!(sequence.kind, SequenceKind::Call(_)) {
|
||||
p.budget.entry(p.span())?;
|
||||
let key = p.reference()?;
|
||||
if !sequence.keys.insert(key.name.clone()) {
|
||||
return Err(error(key.span, "Duplicate call argument name."));
|
||||
}
|
||||
p.take(":")?;
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tasks.push(Task::Collected(sequence, key));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
}
|
||||
Task::Collected(mut sequence, key) => {
|
||||
sequence
|
||||
.values
|
||||
.push((key, values.pop().expect("completed sequence value")));
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() != closing {
|
||||
p.take(",")?;
|
||||
if p.text() == closing {
|
||||
return Err(error(p.span(), "Trailing comma is not an expression."));
|
||||
}
|
||||
}
|
||||
tasks.push(Task::Next(sequence));
|
||||
}
|
||||
Task::Power(start) => {
|
||||
let base = values.pop().expect("completed power base");
|
||||
p.take(",")?;
|
||||
let negative = if p.text() == "-" {
|
||||
p.cursor += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let span = p.integer()?;
|
||||
let magnitude = p.source[span.start..span.end]
|
||||
.parse::<u8>()
|
||||
.map_err(|_| error(span, "Power exponent is outside -16..16."))?;
|
||||
if magnitude > 16 {
|
||||
return Err(error(span, "Power exponent is outside -16..16."));
|
||||
}
|
||||
let exponent = if negative {
|
||||
-(magnitude as i8)
|
||||
} else {
|
||||
magnitude as i8
|
||||
};
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Power {
|
||||
base: Box::new(base),
|
||||
exponent,
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::AsKind(start, kind) => {
|
||||
let value = values.pop().expect("completed kind assertion value");
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::AsKind {
|
||||
kind,
|
||||
value: Box::new(value),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::Expression(minimum) => prefix(p, minimum, &mut tasks, &mut values)?,
|
||||
task => continue_task(p, task, &mut tasks, &mut values)?,
|
||||
}
|
||||
}
|
||||
Ok(Binding::Expression(
|
||||
values.pop().expect("one complete expression"),
|
||||
))
|
||||
}
|
||||
// Keep mutually exclusive construction temporaries out of the numeric prefix
|
||||
// call chain. Both steps borrow the driver's original iterative stacks.
|
||||
fn prefix(
|
||||
p: &mut Cursor<'_, '_>,
|
||||
minimum: u8,
|
||||
tasks: &mut Vec<Task>,
|
||||
values: &mut Vec<Expression>,
|
||||
) -> Result<(), Error> {
|
||||
let start = p.span().start;
|
||||
if p.text() != "(" {
|
||||
p.budget.entry(p.span())?;
|
||||
}
|
||||
tasks.push(Task::Continue(minimum));
|
||||
match p.text() {
|
||||
"true" | "false" => {
|
||||
let value = p.text() == "true";
|
||||
let span = p.span();
|
||||
p.cursor += 1;
|
||||
values.push(p.node(ExpressionKind::Boolean(value), span)?);
|
||||
}
|
||||
"-" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Unary(start, UnaryOp::Negate));
|
||||
tasks.push(Task::Expression(7));
|
||||
}
|
||||
"(" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Group(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"[" => {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(SequenceKind::List, start)));
|
||||
}
|
||||
"@" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
if p.text() == "(" {
|
||||
p.open()?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Next(Sequence::new(
|
||||
SequenceKind::Call(reference),
|
||||
start,
|
||||
)));
|
||||
} else {
|
||||
let end = reference.span.end;
|
||||
values.push(p.node(ExpressionKind::Property(reference), Span { start, end })?);
|
||||
}
|
||||
}
|
||||
"$" => {
|
||||
p.cursor += 1;
|
||||
let reference = p.reference()?;
|
||||
let end = reference.span.end;
|
||||
values.push(p.node(ExpressionKind::Parameter(reference), Span { start, end })?);
|
||||
}
|
||||
"ratio" => {
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
"abs" | "not" | "if" | "sum" => {
|
||||
let kind = match p.text() {
|
||||
"abs" => SequenceKind::Unary(UnaryOp::Absolute),
|
||||
"not" => SequenceKind::Unary(UnaryOp::Not),
|
||||
"if" => SequenceKind::If,
|
||||
_ => SequenceKind::Sum,
|
||||
};
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Next(Sequence::new(kind, start)));
|
||||
}
|
||||
"power" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
tasks.push(Task::Power(start));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
"as_kind" => {
|
||||
p.cursor += 1;
|
||||
p.open()?;
|
||||
p.take("(")?;
|
||||
p.take("@")?;
|
||||
let kind = p.reference()?;
|
||||
p.take(",")?;
|
||||
tasks.push(Task::AsKind(start, kind));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
_ if p
|
||||
.tokens
|
||||
.get(p.cursor)
|
||||
.is_some_and(|token| token.class == Class::Number) =>
|
||||
{
|
||||
let (value, literal) = p.exact_literal()?;
|
||||
values.push(p.number(value, literal)?);
|
||||
}
|
||||
_ => return Err(error(p.span(), "Expected an analysis expression.")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn continue_task(
|
||||
p: &mut Cursor<'_, '_>,
|
||||
task: Task,
|
||||
tasks: &mut Vec<Task>,
|
||||
values: &mut Vec<Expression>,
|
||||
) -> Result<(), Error> {
|
||||
match task {
|
||||
Task::Expression(_) => unreachable!("prefix tasks are dispatched by the driver"),
|
||||
Task::Continue(minimum) => {
|
||||
if let Some((op, precedence)) = operator(p.text()).filter(|(_, prec)| *prec >= minimum)
|
||||
{
|
||||
let left = values.pop().expect("completed Pratt operand");
|
||||
if comparison(op) && ungrouped_comparison(&left) {
|
||||
return Err(error(p.span(), "Comparison children require parentheses."));
|
||||
}
|
||||
p.budget.entry(p.span())?;
|
||||
p.cursor += 1;
|
||||
tasks.push(Task::Continue(minimum));
|
||||
tasks.push(Task::Binary(left, op));
|
||||
tasks.push(Task::Expression(precedence + 1));
|
||||
}
|
||||
}
|
||||
Task::Binary(left, op) => {
|
||||
let right = values.pop().expect("completed right operand");
|
||||
if comparison(op) && ungrouped_comparison(&right) {
|
||||
return Err(error(
|
||||
right.span,
|
||||
"Comparison children require parentheses.",
|
||||
));
|
||||
}
|
||||
let span = Span {
|
||||
start: left.span.start,
|
||||
end: right.span.end,
|
||||
};
|
||||
values.push(p.node(
|
||||
ExpressionKind::Binary {
|
||||
op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
},
|
||||
span,
|
||||
)?);
|
||||
}
|
||||
Task::Unary(start, op) => {
|
||||
let operand = values.pop().expect("completed unary operand");
|
||||
let end = operand.span.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Unary {
|
||||
op,
|
||||
operand: Box::new(operand),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::Group(start) => {
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
let value = values.last_mut().expect("completed grouped expression");
|
||||
value.grouped = true;
|
||||
value.span = Span { start, end };
|
||||
}
|
||||
Task::Next(mut sequence) => {
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() == closing {
|
||||
let end = p.take(closing)?.end;
|
||||
p.close();
|
||||
values.push(p.sequence(sequence, end)?);
|
||||
} else {
|
||||
let count = sequence.values.len();
|
||||
let maximum = match sequence.kind {
|
||||
SequenceKind::List => crate::analysis::MAX_LIST_LENGTH,
|
||||
SequenceKind::Unary(_) => 1,
|
||||
SequenceKind::If => 3,
|
||||
SequenceKind::Sum => 2,
|
||||
SequenceKind::Call(_) => usize::MAX,
|
||||
};
|
||||
if count >= maximum {
|
||||
return Err(error(
|
||||
p.span(),
|
||||
"Too many list members or constructor arguments.",
|
||||
));
|
||||
}
|
||||
let key = if matches!(sequence.kind, SequenceKind::Call(_)) {
|
||||
p.budget.entry(p.span())?;
|
||||
let key = p.reference()?;
|
||||
if !sequence.keys.insert(key.name.clone()) {
|
||||
return Err(error(key.span, "Duplicate call argument name."));
|
||||
}
|
||||
p.take(":")?;
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tasks.push(Task::Collected(sequence, key));
|
||||
tasks.push(Task::Expression(0));
|
||||
}
|
||||
}
|
||||
Task::Collected(mut sequence, key) => {
|
||||
sequence
|
||||
.values
|
||||
.push((key, values.pop().expect("completed sequence value")));
|
||||
let closing = if matches!(sequence.kind, SequenceKind::List) {
|
||||
"]"
|
||||
} else {
|
||||
")"
|
||||
};
|
||||
if p.text() != closing {
|
||||
p.take(",")?;
|
||||
if p.text() == closing {
|
||||
return Err(error(p.span(), "Trailing comma is not an expression."));
|
||||
}
|
||||
}
|
||||
tasks.push(Task::Next(sequence));
|
||||
}
|
||||
Task::Power(start) => {
|
||||
let base = values.pop().expect("completed power base");
|
||||
p.take(",")?;
|
||||
let negative = if p.text() == "-" {
|
||||
p.cursor += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let span = p.integer()?;
|
||||
let magnitude = p.source[span.start..span.end]
|
||||
.parse::<u8>()
|
||||
.map_err(|_| error(span, "Power exponent is outside -16..16."))?;
|
||||
if magnitude > 16 {
|
||||
return Err(error(span, "Power exponent is outside -16..16."));
|
||||
}
|
||||
let exponent = if negative {
|
||||
-(magnitude as i8)
|
||||
} else {
|
||||
magnitude as i8
|
||||
};
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::Power {
|
||||
base: Box::new(base),
|
||||
exponent,
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
Task::AsKind(start, kind) => {
|
||||
let value = values.pop().expect("completed kind assertion value");
|
||||
let end = p.take(")")?.end;
|
||||
p.close();
|
||||
values.push(p.node(
|
||||
ExpressionKind::AsKind {
|
||||
kind,
|
||||
value: Box::new(value),
|
||||
},
|
||||
Span { start, end },
|
||||
)?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
impl Sequence {
|
||||
fn new(kind: SequenceKind, start: usize) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Private mixed emitted-unverified transition
|
||||
|
||||
2026-09-25. Implementation candidate from `fa7a980418ef99d772e67c66c28dfa7b5181f875`, under the accepted root contract `harness/analysis-mixed-emission-contract-20260925.md` published at `6c0d9e80`. This note reports source work; parent owns compilation and runtime acceptance.
|
||||
|
||||
`RenamePlan::into_unchanged` and `emit_changed_unverified` consume the plan. Both recheck the entire current bundle and reuse the complete original-coordinate edit validator; wrong-branch calls refuse. Unchanged returns the original admitted owner. Changed refuses every explicit carried guard, including zero-baseline mode, because dropping that guard cannot release its externally owned retained allocation.
|
||||
|
||||
Changed emission computes O as one entry per output file and text equal to all final source bytes plus all owned keys plus entry bytes. It reserves O once against the existing live plan ledger B (which already contains R and M) before output copies. Each canonical file has one final String; scalar sizing walks borrow existing maps. Original-coordinate slices and replacement names are appended without parsing, decoding, normalization or sequential textual substitution. Length and complete-file checks precede the transition. The old names map, edits map and entire source witness explicitly drop before the opaque PendingRename is created. Caller-owned original source and desired Project remain external. Pending owns the bundle, exact policies, O, original expression count E and desired borrow; it has no extraction, parser/adoption or accepted-result API. E is provenance, not a proven equality bound on arbitrary future faulty output.
|
||||
|
||||
The split API avoids a common large enum, callback framework, heap indirection or lint suppression. No producer hooks, carried constructor, semantic verification, public source8 dispatch, persistence or history behavior changed. Original stack controls and inherited failure/formatting dispositions are not edited or claimed repaired.
|
||||
|
||||
## Source checks and authored tests
|
||||
|
||||
Independent source reviewer inspected production before tests completed. Direct targeted rustfmt only; no Cargo, executable tests or compiler run by the implementer. Eleven new tests are selected by `emission`: ten in the new emission child and one appended to carried tests.
|
||||
|
||||
- Actual predecessor-compatible E entry/text caps 17/114 versus 16/114 and 17/113, and H caps 29/216 versus 28/216 and 29/215; expected pending O values (1,0,105) and (3,0,183), complete literal bytes and source/entry/key ownership.
|
||||
- No-op identity, usage, numeric work, warnings and retained ledger below copy-overlap byte ceilings; desired lifetime ends while unchanged owner survives.
|
||||
- Both wrong branches, touched/untouched stale bytes, entry/path/file-set changes, equal independently owned current input, and selected reused edit-validator failures. Checked-add overflow is labeled arithmetic fault injection.
|
||||
- All exact Limits/ProjectLimits fields and a pending result escaping the original source-bundle scope.
|
||||
- Rich OA/SA global/scoped swaps with repeated/reordered labels, independently authored whole files in LF and Unicode/CRLF variants; four-file cross-family swap with an untouched comment file; full ordinary category output preserving prose, commented references and historical UUID; combined growth/shrinkage with an unchanged file.
|
||||
- Actual retained and borrowed-zero carried sources: changed refuses; no-op preserves the original guard through a subsequent changed refusal.
|
||||
- Scoped 128 KiB worker success/no-op/refusal and local drops, with parser prerequisites constructed normally. The same test also moves complete deep 128-node and wide 129-node owners into workers for changed emission and unchanged extraction/drop, asserting E and whole output bytes; the isolated storage refusal uses the minimal fixture. Existing full-pipeline stack tests remain unchanged.
|
||||
|
||||
Oracle source: independent emission and rename-plan reports published with the contract/evidence, not output obtained from this emitter. Pre-execution review corrected a policy fixture that exceeded its caller byte/reference ceiling. Raw-byte inspection corrected the category expected literal to preserve its two specific CRLF line endings among LF lines rather than converting the whole file. These were authoring corrections, not runtime failures. Parent gates must still establish compile, actual boundary behavior, whole bytes, unchanged full-suite dispositions, Clippy and formatting.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Private Pratt parser stack-frame repair candidate
|
||||
|
||||
2026-09-25. Separate correction after emission source `19a4082117b88caf332198e4c685bde91005cb52`. Parent recorded eleven focused emission tests passing, then a full-suite STATUS_STACK_OVERFLOW (0xc00000fd) in the unchanged ordinary-index 128 KiB control. The prior `fa7a9804` full run completed. This note preserves the failed candidate rather than treating focused success as acceptance.
|
||||
|
||||
Parent's exact 19a4082 artifact is 42,402,816 bytes, SHA256 `777457cfefb8a3046e3f0809cabd936cfb742abe792befad2c50e05dfac1bf6b`. Its LLDB trace reproduces overflow in Windows allocation beneath formatting in Exact::parse_decimal, then exact_literal, syntax::binding, declarations::parse, State::parse_typed, Parser::unit and parse_impl. No emitter call is active. A debugger process killed with exit 0 is not a test pass. Raw parent records live in `.local/analysis-mixed-emission-gates-20260925/emission-full-19a4082-1.*` and `emission-debugger-19a4082-1.*`; integration owns publication.
|
||||
|
||||
Parent measured fixed active prologues: binding 27,272 B, declarations::parse 22,664 B, Parser::unit 11,864 B, State::parse_typed 6,952 B. The previously measured outer frames (parse_impl 11,048 B, profile wrapper 200 B, parse 280 B, accepted 1,432 B, indexed 1,496 B, worker 6,920 B) match fa7. These partial native reservations are not total stack use. No inherited owner field or parser source changed in emission, so duplicated inline policy is not the cause established here. Environment-sensitive headroom remains a limitation; the failed full run still blocks acceptance.
|
||||
|
||||
## Narrow correction
|
||||
|
||||
`syntax.rs` keeps the same iterative binding driver, task/value Vec allocations, initial active check/unbound branch, per-popped-task active check, and final value extraction. The current Task::Expression body moves mechanically to a private prefix step borrowing those Vecs and Cursor. Remaining task arms move mechanically to a continuation step, also borrowing the same containers. No recursive parser call, new collection, clone, Box/Arc, numeric parsing change, work debit, depth limit, logical reservation or producer-timing claim is introduced. All cursor mutations, pushes/pops, opening/closing, validation, spans and error order remain at the same points. Steps return unit or the same Error; the driver retains ownership and cleanup of its original stacks.
|
||||
|
||||
The intended native-stack improvement is that numeric prefix work no longer runs below all continuation construction temporaries in one large function. This is a source-level rationale, not an accepted measurement. Parent must measure the new active driver+prefix prologue sum against 27,272 B (and driver+continuation where relevant), then run unchanged stack controls and required focused/full gates. No test, fixture, allowance, RUST_MIN_STACK, Exact implementation or forwarding wrapper was changed. Independent source review checks all moved arms; targeted rustfmt and diff checks are the only implementer tools, with no compiler/runtime execution.
|
||||
Reference in New Issue
Block a user