Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29417130c7 | ||
|
|
a629ae01d4 | ||
|
|
97a8703adc | ||
|
|
6b0ef99eca | ||
|
|
8d52dab97c | ||
|
|
19a4082117 | ||
|
|
fa7a980418 | ||
|
|
d11b99bb4a | ||
|
|
18073a6f2e | ||
|
|
f1c095654a | ||
|
|
70c20c39de | ||
|
|
8159a17dd7 | ||
|
|
9e8e77e1d8 | ||
|
|
9388fcf2f0 | ||
|
|
ee500908e3 | ||
|
|
147b247398 | ||
|
|
50a5918fd6 | ||
|
|
93eb6117f9 | ||
|
|
1d02206c57 | ||
|
|
a18b7966e5 | ||
|
|
aaec78fcc9 | ||
|
|
71cadfa0cd | ||
|
|
b9a31bb848 | ||
|
|
8f0d96ca63 | ||
|
|
a578ac98a0 | ||
|
|
eeb1069cfc | ||
|
|
754a9b4b7c | ||
|
|
0fc0826435 | ||
|
|
10d7f3340d | ||
|
|
75bc285386 | ||
|
|
ded72c2339 | ||
|
|
1b1c3e3abb | ||
|
|
90fae14818 | ||
|
|
1f8283236f |
@@ -4,12 +4,12 @@
|
||||
|
||||
- Follow `PROJECT_GOAL.md` and the user's current instructions. Work autonomously within scope. Resolve discoverable questions yourself; record, test, and revise reversible assumptions. Ask only when a consequential decision needs information or authorization not already available. Continue independent useful work while blocked elsewhere.
|
||||
- Map unfamiliar domains through primary sources, comparable systems, and real user workflows. Identify underlying needs, valuable concepts, and avoidable friction; evaluate improvements rather than merely reproducing features. Synthesize findings into decisions or experiments, and stop research when another action offers greater value.
|
||||
- Make specifications precise enough for compatible independent implementations: definitions, relationships, invariants, valid/invalid examples, and lifecycle or transformation rules where relevant. Trace important requirements and decisions to implementation and verification. Resolve specification ambiguity at its source when implementation exposes it.
|
||||
- Make specifications precise enough for compatible independent implementations: definitions, relationships, invariants, valid / invalid examples, and lifecycle or transformation rules where relevant. Trace important requirements and decisions to implementation and verification. Resolve specification ambiguity at its source when implementation exposes it.
|
||||
- For consequential uncertain choices, compare materially different candidates against explicit user-derived criteria. Ask independent critics to challenge assumptions and attempt to falsify proposals; refine or replace candidates as evidence warrants. Record the choice, evidence, tradeoffs, rejected approaches, and reasons to revisit it. Use proportionate effort for reversible local choices.
|
||||
- Plan milestones that resolve uncertainty or deliver useful capability, with explicit dependencies. Test foundational assumptions before substantial downstream work. At checkpoints, assess progress toward the actual objective and replan when evidence invalidates the strategy or progress stalls.
|
||||
- Define observable success and suitable evaluators before repeated improvement: invariants, examples, round trips, transformations, performance checks, realistic user tasks, or explicit review rubrics. Ensure the evaluator measures the intended outcome. For experiments, record the question, expectations, method, result, and implications; isolate exploratory prototypes from production architecture.
|
||||
- Build and run meaningful changes, exercise actual behavior, and inspect useful logs. Investigate crashes, serious warnings, corrupted state, and interaction defects. For GUI work, use Computer Use or equivalent tools to inspect realistic workflows, cognitive load, editing/selection, navigation, feedback, recovery, loading/empty/failure states, accessibility, layout, focus/disabled states, and responsiveness.
|
||||
- Match independent reviews to the artifact and risks. For languages or formats, include ambiguity, round trips, human/AI editability, and version-control behavior where relevant. Verify findings, fix the appropriate conceptual, specification, architectural, implementation, or interaction layer, and recheck affected dependents. Reuse valid evidence; repeat checks for concrete risks, changes, findings, or required gates. Compilation, screenshots, and worker reports alone do not establish correctness or usability.
|
||||
- Build and run meaningful changes, exercise actual behavior, and inspect useful logs. Investigate crashes, serious warnings, corrupted state, and interaction defects. For GUI work, use Computer Use or equivalent tools to inspect realistic workflows, cognitive load, editing / selection, navigation, feedback, recovery, loading / empty / failure states, accessibility, layout, focus / disabled states, and responsiveness.
|
||||
- Match independent reviews to the artifact and risks. For languages or formats, include ambiguity, round trips, human / AI editability, and version-control behavior where relevant. Verify findings, fix the appropriate conceptual, specification, architectural, implementation, or interaction layer, and recheck affected dependents. Reuse valid evidence; repeat checks for concrete risks, changes, findings, or required gates. Compilation, screenshots, and worker reports alone do not establish correctness or usability.
|
||||
- Favor the simplest maintainable design satisfying known requirements and justified extensibility. Avoid speculative abstraction, unrelated refactoring, and repeating approaches already invalidated by evidence.
|
||||
|
||||
## User input
|
||||
@@ -24,10 +24,10 @@ Keep one authoritative home for each kind of information. Use links and brief sc
|
||||
|
||||
| Information | Authoritative home |
|
||||
| --- | --- |
|
||||
| Live plans, priorities, dependencies, ownership, status, blockers, and handoffs | Gitea issues/milestones; linked PRs hold change review and validation. |
|
||||
| Specifications, architecture, interfaces, and build/run documentation | Established version-controlled project documents. |
|
||||
| Live plans, priorities, dependencies, ownership, status, blockers, and handoffs | Gitea issues / milestones; linked PRs hold change review and validation. |
|
||||
| Specifications, architecture, interfaces, and build / run documentation | Established version-controlled project documents. |
|
||||
| Durable decisions and reusable evidence | `harness/DECISIONS.md` and relevant topic notes, or existing equivalents. |
|
||||
| Navigation and integrated-state checkpoint | `harness/STATE.md`: links, verified code commit/release, capabilities, and material limitations. No live task ledger. |
|
||||
| Navigation and integrated-state checkpoint | `harness/STATE.md`: links, verified code commit / release, capabilities, and material limitations. No live task ledger. |
|
||||
| Shared entrypoint | Gitea Wiki: overview and links, without duplicating versioned documentation. |
|
||||
| Published deliverables | Gitea releases, tied to source commits and verification. |
|
||||
|
||||
@@ -35,26 +35,26 @@ Use Gitea for live coordination and the relevant code version for implementation
|
||||
|
||||
## Coordination and integration
|
||||
|
||||
All independent agents coordinating through Gitea run on completely separate machines, each with its own clone, filesystem, tools, processes, and runtime. Gitea and pushed Git history are the shared communication and delivery channels. Local files (including `harness`), unpushed commits, worktrees, test results, and localhost services are not accessible to peers. Matching branch names or paths do not imply synchronized contents.
|
||||
|
||||
1. On starting or resuming, including after compaction, inspect the working branch, local changes, applicable instructions, relevant shared records, and existing implementation. Recover context from authoritative records. Check for newer shared instructions on the actual remote default branch without overwriting local work. Read what is relevant; avoid repeated full-history scans and constant polling.
|
||||
2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, branch/worktree, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work.
|
||||
3. Independent sessions are peers. Each owns its contribution and delegated work. Record one integration/release owner when a milestone needs shared coordination. Use separate worktrees or checkouts, partition files/interfaces, and coordinate changes to shared specifications and documents.
|
||||
4. At meaningful checkpoints or handoffs, record changed results, commit/PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and uncommitted or unpublished work; make required artifacts accessible to other sessions. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task.
|
||||
5. Follow project review and merge conventions. Inspect upstream changes, resolve conflicts while preserving others' edits, and verify the combined result. Do not overwrite others' work or force-push shared history to resolve coordination problems.
|
||||
2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, remote repository / branch, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work.
|
||||
3. Independent sessions are peers. Each owns its contribution and delegated work on its own machine. Record one integration / release owner when a milestone needs shared coordination. Use distinct remote work branches, partition files / interfaces, and coordinate changes to shared specifications and documents. Worktrees provide local isolation only; they do not share work between machines.
|
||||
4. At meaningful checkpoints or handoffs, record changed results, commit / PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and label uncommitted or unpublished work as local-only. Before asking peers to consume results, push the required code and versioned documentation, including relevant `harness` updates; publish other required artifacts in Gitea and link them. Use repository-relative paths with commit references, reproducible commands, and necessary environment details; local paths and localhost URLs alone are not handoffs. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task.
|
||||
5. Follow project review and merge conventions. Fetch the relevant remote branches and inspect the identified commits before reviewing or integrating another machine's work. Resolve conflicts while preserving others' edits, and verify the combined result locally; report material environment differences. Local instruction-file edits reach peers only after publication and retrieval. Do not overwrite others' work or force-push shared history to resolve coordination problems.
|
||||
6. Close work when its acceptance criteria are met and the result is integrated or delivered as appropriate. Mark duplicates and superseded tasks explicitly with replacement links. Then choose further useful unowned work while the requested objective remains incomplete.
|
||||
|
||||
Use Gitea MCP for shared coordination. If unavailable, report the failure, buffer necessary handoff information locally, and continue already-owned or clearly non-overlapping work. Reconcile with current Gitea state when access returns; do not claim an ownership update, review, or publication succeeded without confirmation.
|
||||
|
||||
## Delegation and reasoning
|
||||
|
||||
Use GPT-6 Astra exclusively. Delegate bounded independent research, design, implementation, or review when it improves progress, judgment, or preserves the parent's context. Give subagents the relevant context, ownership boundaries, result format, and evidence requirements. Use fresh critics when practical. The parent owns synthesis and integration and normally consolidates results into Gitea. Choose concurrency according to useful independent work and available resources, including other sessions; there is no agent-count quota.
|
||||
Use GPT-6 Astra exclusively. Gitea peers are independent remote sessions, not subagents controlled by this session. Spawned subagents use the execution environment actually provided by the agent tools; do not assume they share files, and publish their results before Gitea peers need them. Delegate bounded independent research, design, implementation, or review when it improves progress, judgment, or preserves the parent's context. Give subagents the relevant context, ownership boundaries, result format, and evidence requirements. Use fresh critics when practical. The parent owns synthesis and integration and normally consolidates results into Gitea. Choose concurrency according to useful independent work and available resources, including other sessions; there is no agent-count quota.
|
||||
|
||||
Optimize reasoning effort for verified outcomes per cost, including review, retries, and repair:
|
||||
|
||||
Main agent: default to high for planning, synthesis, integration, and acceptance decisions; medium for dispatching a settled plan and routine coordination.
|
||||
|
||||
Subagents: default to medium for bounded implementation and targeted research; low for straightforward retrieval, mechanical edits, and running established checks; high for complex debugging, correctness review, adversarial analysis, or ambiguous design.
|
||||
|
||||
Use extra-high selectively for difficult foundational semantic, methodological, formal, or cross-domain decisions. Reserve max for exceptional unresolved problems where additional reasoning is likely to justify its cost. Retain higher settings when evidence supports their value.
|
||||
- Main agent: default to high for planning, synthesis, integration, and acceptance decisions; medium for dispatching a settled plan and routine coordination.
|
||||
- Subagents: default to medium for bounded implementation and targeted research; low for straightforward retrieval, mechanical edits, and running established checks; high for complex debugging, correctness review, adversarial analysis, or ambiguous design.
|
||||
- Use extra-high selectively for difficult foundational semantic, methodological, formal, or cross-domain decisions. Reserve max for exceptional unresolved problems where additional reasoning is likely to justify its cost. Retain higher settings when evidence supports their value.
|
||||
|
||||
Allocate by task difficulty and consequences, not role or length alone. Start higher when a clearly difficult decision has expensive downstream consequences. Escalate for concrete reasoning difficulties; resolve missing evidence or unclear requirements at their source. Set subagent effort explicitly through supported controls to avoid unintended inheritance. Do not claim an unapplied change or alter global configuration merely to enforce these preferences.
|
||||
|
||||
@@ -64,10 +64,10 @@ Preserve concise records of decisions and rationale, useful experiments, failed
|
||||
|
||||
Consolidate duplicates, repair stale guidance, and remove obsolete temporary files alongside useful project work. Before removing material, check references, active ownership, and ongoing use; preserve unique knowledge and update affected links. Prefer closing, superseding, or archiving history over deletion. User input is excluded from autonomous cleanup.
|
||||
|
||||
If legacy local task ledgers or plans duplicate Gitea, migrate their live content once into existing or appropriately scoped issues, preserve useful decisions/evidence, and mark the old records superseded. Record the migration so later sessions do not repeat it. Do not create empty documents or maintain records for ceremony.
|
||||
If legacy local task ledgers or plans duplicate Gitea, migrate their live content once into existing or appropriately scoped issues, preserve useful decisions / evidence, and mark the old records superseded. Record the migration so later sessions do not repeat it. Do not create empty documents or maintain records for ceremony.
|
||||
|
||||
## Delivery
|
||||
|
||||
Publish versioned Gitea releases at meaningful working milestones; mark unfinished versions as prereleases. Check existing releases/tags and coordinate one owner and version before publishing. Build from an identified integrated commit and verify the packaged result and launch steps.
|
||||
Publish versioned Gitea releases at meaningful working milestones; mark unfinished versions as prereleases. Check existing releases / tags and coordinate one owner and version before publishing. Build from an identified integrated commit and verify the packaged result and launch steps.
|
||||
|
||||
Include runnable builds or appropriate artifacts, concise release notes, build/run/deployment instructions as applicable, known limitations, tested platforms, and verification evidence. Confirm publication and artifact availability. Use existing authorized API access or project tooling when MCP lacks a needed release/upload operation. Do not silently replace published versions with different contents.
|
||||
Include runnable builds or appropriate artifacts, concise release notes, build / run / deployment instructions as applicable, known limitations, tested platforms, and verification evidence. Confirm publication and artifact availability. Use existing authorized API access or project tooling when MCP lacks a needed release / upload operation. Do not silently replace published versions with different contents.
|
||||
|
||||
+351
-52
@@ -1,8 +1,8 @@
|
||||
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2/V3/V4 migration.
|
||||
mod raw_tokens;
|
||||
#[path = "language/semantic_source.rs"]
|
||||
mod semantic_source;
|
||||
mod source3_bridge;
|
||||
mod raw_tokens;
|
||||
// Staged syntax foundation only. Public source8 admission waits for complete
|
||||
// declaration lowering, source updates and the root-owned resource gate.
|
||||
#[cfg(test)]
|
||||
@@ -480,6 +480,8 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
}
|
||||
.document()
|
||||
.map_err(|e| vec![diagnostic(source, e)])
|
||||
@@ -914,6 +916,8 @@ pub(crate) fn parse_unit(
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
}
|
||||
.unit(entry, true)
|
||||
.map_err(|e| Box::new(diagnostic(source, e)))
|
||||
@@ -1005,68 +1009,153 @@ fn edit_positions(
|
||||
unit: SourceUnit,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
let tokens = strict_tokens(source).map_err(|e| Box::new(diagnostic(source, e)))?;
|
||||
let mut ends: Vec<_> = tokens.iter().map(|token| token.span.end).collect();
|
||||
edit_positions_from_tokens(source, &unit, |at| tokens[at], tokens.len(), None)
|
||||
}
|
||||
|
||||
fn edit_positions_from_tokens(
|
||||
source: &str,
|
||||
unit: &SourceUnit,
|
||||
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();
|
||||
for (index, token) in tokens.iter().enumerate() {
|
||||
for index in 0..count {
|
||||
let token = token(index);
|
||||
match token.text(source) {
|
||||
"{" | "[" => stack.push(index),
|
||||
"}" | "]" => ends[stack.pop().expect("parser checked delimiters")] = token.span.end,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
struct Positions<'a> {
|
||||
tokens: &'a [Lexeme],
|
||||
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 Positions<'_> {
|
||||
fn token(&self, start: usize) -> usize {
|
||||
self.tokens
|
||||
.partition_point(|token| token.span.start < start)
|
||||
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 fields(&self, fields: &BTreeMap<String, Ast>) -> BTreeMap<String, EditField> {
|
||||
fn token(&self, start: usize) -> usize {
|
||||
let mut low = 0;
|
||||
let mut high = self.ends.len();
|
||||
while low < high {
|
||||
let middle = low + (high - low) / 2;
|
||||
if (self.token_at)(middle).span.start < start {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
low
|
||||
}
|
||||
fn fields(
|
||||
&mut self,
|
||||
fields: &BTreeMap<String, Ast>,
|
||||
) -> Result<BTreeMap<String, EditField>, Box<Diagnostic>> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|(key, ast)| {
|
||||
let value = self.value(ast);
|
||||
let start = self.tokens[self.token(value.span.start) - 1].span.start;
|
||||
let end = self.tokens[self.token(value.span.end)].span.end;
|
||||
(
|
||||
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(),
|
||||
end: match ast {
|
||||
Ast::Reference(..) | Ast::HistoricalUuid(..) => self.tokens[index + 1].span.end,
|
||||
_ => self.ends[index],
|
||||
},
|
||||
end: self
|
||||
.value_ends
|
||||
.and_then(|ends| ends.get(&ast.offset()).copied())
|
||||
.unwrap_or_else(|| match ast {
|
||||
Ast::Reference(..) | Ast::HistoricalUuid(..) => {
|
||||
(self.token_at)(index + 1).span.end
|
||||
}
|
||||
_ => self.ends[index],
|
||||
}),
|
||||
};
|
||||
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 {
|
||||
tokens: &tokens,
|
||||
let mut positions = Positions {
|
||||
token_at: &token,
|
||||
ends,
|
||||
value_ends,
|
||||
text,
|
||||
source,
|
||||
quota: &mut quota,
|
||||
};
|
||||
unit.declarations
|
||||
.into_iter()
|
||||
let result = unit
|
||||
.declarations
|
||||
.iter()
|
||||
.map(|declaration| {
|
||||
if declaration.id_insertion.is_some() {
|
||||
return Err(Box::new(diagnostic(
|
||||
@@ -1078,6 +1167,7 @@ fn edit_positions(
|
||||
},
|
||||
)));
|
||||
}
|
||||
positions.consume(7, declaration.span.start)?;
|
||||
let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) {
|
||||
&["kind", "alias", "id_keyword", "id"]
|
||||
} else {
|
||||
@@ -1086,8 +1176,8 @@ fn edit_positions(
|
||||
let start = positions.token(declaration.span.start);
|
||||
let header = roles
|
||||
.iter()
|
||||
.zip(&tokens[start..])
|
||||
.map(|(role, token)| (*role, token.span))
|
||||
.zip(start..count)
|
||||
.map(|(role, at)| (*role, token(at).span))
|
||||
.collect();
|
||||
Ok((
|
||||
declaration.id,
|
||||
@@ -1101,12 +1191,23 @@ fn edit_positions(
|
||||
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,
|
||||
@@ -1114,10 +1215,35 @@ 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);
|
||||
}
|
||||
self.tokens.get(index).copied()
|
||||
}
|
||||
fn current(&self) -> Lexeme {
|
||||
self.tokens[self.cursor.min(self.tokens.len() - 1)]
|
||||
self.token(self.cursor).unwrap_or(Lexeme {
|
||||
class: TokenClass::Punctuation,
|
||||
span: Span {
|
||||
start: self.source.len(),
|
||||
end: self.source.len(),
|
||||
},
|
||||
})
|
||||
}
|
||||
fn syntax_entry(&mut self) -> Result<(), Error> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let span = self.current().span;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.entry(span)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn text(&self) -> &str {
|
||||
self.current().text(self.source)
|
||||
@@ -1140,11 +1266,21 @@ impl Parser<'_> {
|
||||
if self.current().class != TokenClass::String {
|
||||
return Err(self.error("Expected a double-quoted string."));
|
||||
}
|
||||
#[cfg(test)]
|
||||
let span = self.current().span;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
let raw = &self.source[span.start..span.end];
|
||||
let value = mixed.quoted_text(raw, span)?;
|
||||
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}'.")));
|
||||
@@ -1154,6 +1290,10 @@ 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 Ok(Id::new_v4());
|
||||
}
|
||||
self.cursor += 1;
|
||||
@@ -1170,11 +1310,32 @@ 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> {
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.own_text(bytes, span)?;
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
let _ = (bytes, span);
|
||||
Ok(())
|
||||
}
|
||||
fn release_text(&mut self, bytes: usize) {
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.release_text(bytes);
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
let _ = bytes;
|
||||
}
|
||||
fn fields(&mut self) -> Result<BTreeMap<String, Ast>, Error> {
|
||||
self.take("{")?;
|
||||
@@ -1182,6 +1343,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",
|
||||
@@ -1189,11 +1357,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,
|
||||
@@ -1204,15 +1377,23 @@ impl Parser<'_> {
|
||||
self.cursor += 1;
|
||||
let start = self.current().span.start;
|
||||
let value = self.value()?;
|
||||
let end = self.tokens[self.cursor - 1].span.end;
|
||||
let end = self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed token")
|
||||
.span
|
||||
.end;
|
||||
self.take(";")?;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.projection(1, Span { start, end })?;
|
||||
}
|
||||
Ok((key, value, Span { start, end }))
|
||||
}
|
||||
|
||||
fn nested_header_kind(&self) -> Option<DeclKind> {
|
||||
let kind = declaration_kind(self.text())?;
|
||||
let alias = self.tokens.get(self.cursor + 1)?;
|
||||
let following = self.tokens.get(self.cursor + 2)?;
|
||||
let alias = self.token(self.cursor + 1)?;
|
||||
let following = self.token(self.cursor + 2)?;
|
||||
// A node's ordinary `diagram @view;` field is not a declaration.
|
||||
if !crate::valid_alias(alias.text(self.source)) {
|
||||
return None;
|
||||
@@ -1227,14 +1408,38 @@ impl Parser<'_> {
|
||||
if self.value_depth >= 64 {
|
||||
return Err(self.error("Typed values may not nest more than 64 levels."));
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
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)
|
||||
.expect("consumed value")
|
||||
.span
|
||||
.end;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
let span = Span {
|
||||
start: ast.offset(),
|
||||
end,
|
||||
};
|
||||
mixed.value_end(span)?;
|
||||
mixed.projection(1, span)?;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
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(), "-" | "+"))
|
||||
{
|
||||
let value = mixed.ordinary_number(&mut self.cursor)?;
|
||||
return Ok(Ast::Literal(value, offset));
|
||||
}
|
||||
match self.text() {
|
||||
"uuid" => {
|
||||
self.cursor += 1;
|
||||
@@ -1244,6 +1449,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))
|
||||
}
|
||||
"@" => {
|
||||
@@ -1284,6 +1492,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))
|
||||
@@ -1332,16 +1541,31 @@ impl Parser<'_> {
|
||||
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
|
||||
let mut header = if entry {
|
||||
self.take("arcadia")?;
|
||||
if !matches!(self.text(), "3" | "4" | "5" | "6" | "7") {
|
||||
return Err(
|
||||
self.error("Unsupported schema version: expected arcadia 3, 4, 5, 6 or 7.")
|
||||
);
|
||||
#[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")
|
||||
} {
|
||||
return Err(self.error(if mixed_mode {
|
||||
"Private mixed syntax requires arcadia 8."
|
||||
} else {
|
||||
"Unsupported schema version: expected arcadia 3, 4, 5, 6 or 7."
|
||||
}));
|
||||
}
|
||||
let schema_version = self.text().parse().expect("checked source version");
|
||||
self.cursor += 1;
|
||||
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 })?;
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
let alias = self.identifier()?;
|
||||
let name = self.quoted()?;
|
||||
let id_insertion = (self.text() != "id").then_some(self.current().span.start);
|
||||
@@ -1382,6 +1606,14 @@ 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())
|
||||
{
|
||||
let owner = open.last().map(|&at| declarations[at].id);
|
||||
mixed.parse_typed(&mut self.cursor, owner)?;
|
||||
continue;
|
||||
}
|
||||
if let Some(&owner) = open.last() {
|
||||
if self.text() == "}" {
|
||||
declarations[owner].body.end = offset;
|
||||
@@ -1407,6 +1639,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 {
|
||||
@@ -1425,6 +1658,14 @@ 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,
|
||||
end: offset,
|
||||
})?;
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
self.cursor += 1;
|
||||
let path = self.quoted()?;
|
||||
self.take(";")?;
|
||||
@@ -1432,7 +1673,11 @@ impl Parser<'_> {
|
||||
path,
|
||||
Span {
|
||||
start: offset,
|
||||
end: self.tokens[self.cursor - 1].span.end,
|
||||
end: self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed token")
|
||||
.span
|
||||
.end,
|
||||
},
|
||||
));
|
||||
continue;
|
||||
@@ -1444,20 +1689,40 @@ impl Parser<'_> {
|
||||
if entry_header.description.is_some() {
|
||||
return Err(self.error("Duplicate project description."));
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
self.cursor += 1;
|
||||
let description = self.quoted()?;
|
||||
self.take(";")?;
|
||||
entry_header.project.description = description;
|
||||
entry_header.description = Some(Span {
|
||||
start: offset,
|
||||
end: self.tokens[self.cursor - 1].span.end,
|
||||
end: self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed token")
|
||||
.span
|
||||
.end,
|
||||
});
|
||||
header = Some(entry_header);
|
||||
continue;
|
||||
}
|
||||
let word = self.text().to_owned();
|
||||
let kind = declaration_kind(&word)
|
||||
#[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.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(..)),
|
||||
Span {
|
||||
start: offset,
|
||||
end: offset,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
graph_count += usize::from(matches!(
|
||||
kind,
|
||||
DeclKind::Element(..) | DeclKind::Relation(..)
|
||||
@@ -1485,6 +1750,15 @@ impl Parser<'_> {
|
||||
let body_start = self.current().span.end;
|
||||
self.take("{")?;
|
||||
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
let span = Span {
|
||||
start: offset,
|
||||
end: body_start,
|
||||
};
|
||||
mixed.lexical_owner(open.last().copied(), span)?;
|
||||
mixed.projection(7, span)?;
|
||||
}
|
||||
declarations.push(Declaration {
|
||||
kind,
|
||||
alias,
|
||||
@@ -1521,17 +1795,25 @@ impl Parser<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_declarations(
|
||||
// The construction phase is shared with private mixed resolution. Migration and
|
||||
// complete-model admission remain exclusively in each caller's finishing tail.
|
||||
struct ConstructedDeclarations {
|
||||
project: Project,
|
||||
legacy_elements: serde_json::Map<String, Value>,
|
||||
}
|
||||
|
||||
fn construct_declarations<'a>(
|
||||
mut project: Project,
|
||||
declarations: &[Declaration],
|
||||
declarations: impl IntoIterator<Item = &'a Declaration>,
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
limits: ParseLimits,
|
||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
||||
mut before_fields: impl FnMut(&Declaration) -> Result<(), Error>,
|
||||
) -> Result<ConstructedDeclarations, (usize, Error)> {
|
||||
let legacy = project.schema_version == 3;
|
||||
let mut legacy_elements = serde_json::Map::new();
|
||||
let mut nodes = Vec::new();
|
||||
for (index, declaration) in declarations.iter().enumerate() {
|
||||
for (index, declaration) in declarations.into_iter().enumerate() {
|
||||
(|| -> Result<(), Error> {
|
||||
before_fields(declaration)?;
|
||||
let mut fields = declaration
|
||||
.fields
|
||||
.iter()
|
||||
@@ -1756,6 +2038,23 @@ pub(crate) fn resolve_declarations(
|
||||
.map_err(|error| (index, error))?;
|
||||
target.nodes.insert(node.id, node);
|
||||
}
|
||||
Ok(ConstructedDeclarations {
|
||||
project,
|
||||
legacy_elements,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_declarations(
|
||||
project: Project,
|
||||
declarations: &[Declaration],
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
limits: ParseLimits,
|
||||
) -> Result<crate::persistence::DecodeResult, (usize, Error)> {
|
||||
let legacy = project.schema_version == 3;
|
||||
let ConstructedDeclarations {
|
||||
project,
|
||||
legacy_elements,
|
||||
} = construct_declarations(project, declarations, aliases, |_| Ok(()))?;
|
||||
if legacy {
|
||||
let migrated = source3_bridge::from_scratch(project, legacy_elements)
|
||||
.and_then(|old| crate::persistence::v3::migrate(old, limits))
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
mod budget;
|
||||
mod cursor;
|
||||
mod declarations;
|
||||
mod index;
|
||||
mod lex;
|
||||
mod lower;
|
||||
pub(super) mod mixed;
|
||||
mod syntax;
|
||||
mod update;
|
||||
mod write;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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,8 +73,157 @@ 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.
|
||||
pub(super) fn limits(&self) -> Limits {
|
||||
self.limits
|
||||
}
|
||||
pub fn active(&self) -> Result<(), Error> {
|
||||
self.failed.clone().map_or(Ok(()), Err)
|
||||
}
|
||||
@@ -101,6 +261,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(())
|
||||
}
|
||||
@@ -143,6 +306,14 @@ impl Budget {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Aggregate owned text for an index/update phase, checked before copying.
|
||||
pub fn index_bytes(&self, count: usize, span: Span) -> Result<(), Error> {
|
||||
self.active()?;
|
||||
if count > self.limits.bytes {
|
||||
return Err(error(span, "Source8 index/output byte limit exceeded."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn lowering_occurrences(&self, count: usize, span: Span) -> Result<(), Error> {
|
||||
self.active()?;
|
||||
// Parameter declarations are syntax entries, not reference uses.
|
||||
|
||||
@@ -74,6 +74,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 +88,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),
|
||||
|
||||
@@ -0,0 +1,934 @@
|
||||
//! Temporary typed source witnesses. No arena index becomes a model identity.
|
||||
//! The staged entry point parses each supplied typed file once. A future bundle
|
||||
//! parser must retain the same opaque parse/lower provenance. Occurrence
|
||||
//! completeness trusts that lowerer; the private builder does not prove the
|
||||
//! presence of every syntax reference in a caller-fabricated Lowered value.
|
||||
use super::cursor::Cursor;
|
||||
use super::lower::{self, Child, Field, Lowered, Role, Target, Unit};
|
||||
use super::{Budget, Error, Span, declarations, error, lex};
|
||||
use crate::{Id, Project, analysis as a};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Persistent structural path: cloning shares ancestors, and equality/order
|
||||
/// compare the complete edge sequence, never allocation or arena identities.
|
||||
/// Depth is checked against the inherited frame limit before allocation.
|
||||
/// Comparison and Debug walk leaf-to-root without allocating or recursing;
|
||||
/// this private total order need not be root-first lexicographic order.
|
||||
#[derive(Clone, Default)]
|
||||
pub(super) struct Path(Option<Arc<Step>>);
|
||||
struct Step {
|
||||
parent: Path,
|
||||
edge: Child,
|
||||
}
|
||||
impl PartialEq for Path {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
impl Eq for Path {}
|
||||
impl PartialOrd for Path {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
impl Ord for Path {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
let (mut left, mut right) = (self, other);
|
||||
loop {
|
||||
match (left.0.as_deref(), right.0.as_deref()) {
|
||||
(Some(a), Some(b)) => {
|
||||
let order = a.edge.cmp(&b.edge);
|
||||
if order != Ordering::Equal {
|
||||
return order;
|
||||
}
|
||||
(left, right) = (&a.parent, &b.parent);
|
||||
}
|
||||
(None, None) => return Ordering::Equal,
|
||||
(None, Some(_)) => return Ordering::Less,
|
||||
(Some(_), None) => return Ordering::Greater,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Debug for Path {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Debug intentionally shows the same leaf-to-root order as comparison.
|
||||
let mut list = formatter.debug_list();
|
||||
let mut path = self;
|
||||
while let Some(step) = &path.0 {
|
||||
list.entry(&step.edge);
|
||||
path = &step.parent;
|
||||
}
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
impl Drop for Path {
|
||||
fn drop(&mut self) {
|
||||
let mut next = self.0.take();
|
||||
while let Some(link) = next {
|
||||
// Shared ancestors remain owned elsewhere. The final owner peels
|
||||
// unique links here, taking the parent before Step is dropped.
|
||||
let Some(mut step) = Arc::into_inner(link) else {
|
||||
break;
|
||||
};
|
||||
next = step.parent.0.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Path {
|
||||
pub fn edges(&self) -> Vec<Child> {
|
||||
let mut result = Vec::new();
|
||||
let mut path = self;
|
||||
while let Some(step) = &path.0 {
|
||||
result.push(step.edge);
|
||||
path = &step.parent;
|
||||
}
|
||||
result.reverse();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) struct ExpressionKey {
|
||||
pub owner: Id,
|
||||
pub field: Field,
|
||||
pub path: Path,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) enum TokenRole {
|
||||
Declaration,
|
||||
Use(Role),
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) struct OccurrenceKey {
|
||||
pub owner: Id,
|
||||
/// None is a global declaration header, not a binding field.
|
||||
pub field: Option<Field>,
|
||||
/// None is outside an expression; Some(empty) is its root call site.
|
||||
pub path: Option<Path>,
|
||||
pub role: TokenRole,
|
||||
pub target: Target,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Location {
|
||||
pub file: usize,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Index<'s> {
|
||||
// Private provenance: callers can inspect locations, not supply replacements
|
||||
// for the source witness, keys, catalog or occurrence spans.
|
||||
sources: &'s BTreeMap<usize, String>,
|
||||
names: BTreeMap<Target, String>,
|
||||
occurrences: BTreeMap<OccurrenceKey, Location>,
|
||||
expressions: BTreeMap<ExpressionKey, Location>,
|
||||
}
|
||||
impl<'s> Index<'s> {
|
||||
pub fn sources(&self) -> &BTreeMap<usize, String> {
|
||||
self.sources
|
||||
}
|
||||
pub fn names(&self) -> &BTreeMap<Target, String> {
|
||||
&self.names
|
||||
}
|
||||
pub fn occurrences(&self) -> &BTreeMap<OccurrenceKey, Location> {
|
||||
&self.occurrences
|
||||
}
|
||||
pub fn expressions(&self) -> &BTreeMap<ExpressionKey, Location> {
|
||||
&self.expressions
|
||||
}
|
||||
|
||||
/// Typed-only staged adapter. `ordinary` has already been parsed by its
|
||||
/// owner. Missing typed IDs fail in lowering; this never invents UUIDs.
|
||||
/// The caller merges the returned additions and admits the complete model.
|
||||
pub fn parse(
|
||||
ordinary: &Project,
|
||||
sources: &'s BTreeMap<usize, String>,
|
||||
budget: &mut Budget,
|
||||
) -> Result<(Self, Lowered), lower::Failure> {
|
||||
let mut units = Vec::new();
|
||||
let mut witnesses = BTreeMap::new();
|
||||
for (&file, source) in sources {
|
||||
let failure = |error| lower::Failure {
|
||||
file: Some(file),
|
||||
error,
|
||||
};
|
||||
budget
|
||||
.lowering_entries(units.len() + 1, Span::default())
|
||||
.map_err(failure)?;
|
||||
let tokens = lex::tokens(source, budget).map_err(failure)?;
|
||||
let mut cursor = Cursor::new(source, &tokens, budget);
|
||||
let mut parsed = Vec::new();
|
||||
while cursor.cursor < tokens.len() {
|
||||
parsed.push(declarations::parse(&mut cursor, 8, None).map_err(failure)?);
|
||||
}
|
||||
units.push(Unit {
|
||||
file,
|
||||
declarations: parsed,
|
||||
});
|
||||
witnesses.insert(file, tokens);
|
||||
}
|
||||
let lowered = lower::lower(ordinary, &units, budget)?;
|
||||
// A build failure after our own parse/lower is a phase/invariant error,
|
||||
// not an invented file assignment for an ambiguous numeric span.
|
||||
let index = Self::build(ordinary, sources, &units, &witnesses, &lowered, budget)
|
||||
.map_err(|error| lower::Failure { file: None, error })?;
|
||||
Ok((index, lowered))
|
||||
}
|
||||
|
||||
// Same-parse/lower internal seam only. Checks below reject damaged supplied
|
||||
// sites but occurrence completeness is an invariant of our own lowerer.
|
||||
// Do not expose a constructor accepting independently fabricated Lowered.
|
||||
fn build(
|
||||
ordinary: &Project,
|
||||
sources: &'s BTreeMap<usize, String>,
|
||||
units: &[Unit],
|
||||
witnesses: &BTreeMap<usize, Vec<lex::Token>>,
|
||||
lowered: &Lowered,
|
||||
budget: &mut Budget,
|
||||
) -> Result<Self, Error> {
|
||||
budget.active()?;
|
||||
budget.lowering_entries(lowered.expressions.len(), Span::default())?;
|
||||
budget.lowering_occurrences(lowered.occurrences.len(), Span::default())?;
|
||||
let mut names = BTreeMap::new();
|
||||
let mut name_bytes = 0usize;
|
||||
let mut insert_name = |target, name: &str| -> Result<(), Error> {
|
||||
budget.lowering_entries(names.len() + 1, Span::default())?;
|
||||
name_bytes = name_bytes
|
||||
.checked_add(name.len())
|
||||
.ok_or_else(|| error(Span::default(), "Index name size overflow."))?;
|
||||
budget.index_bytes(name_bytes, Span::default())?;
|
||||
if names.insert(target, name.to_owned()).is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Duplicate qualified catalog identity.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
// Lowering has already bounded and validated the complete ordinary
|
||||
// identity catalog before this existing convenience visitor allocates.
|
||||
for (id, alias) in ordinary.identities() {
|
||||
insert_name(Target::Global(id), alias)?;
|
||||
}
|
||||
for definition in ordinary
|
||||
.definitions
|
||||
.values()
|
||||
.chain(lowered.definitions.values())
|
||||
{
|
||||
if lowered.definitions.contains_key(&definition.id) {
|
||||
insert_name(Target::Global(definition.id), &definition.alias)?;
|
||||
}
|
||||
if let a::DefinitionBody::Calculation(calculation) = &definition.body {
|
||||
for parameter in &calculation.parameters {
|
||||
insert_name(
|
||||
Target::Parameter {
|
||||
calculation: definition.id,
|
||||
parameter: parameter.id,
|
||||
},
|
||||
¶meter.name,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for element in lowered.elements.values() {
|
||||
insert_name(Target::Global(element.id), &element.alias)?;
|
||||
}
|
||||
let original = Original {
|
||||
kind: OriginalKind::Typed { sources, witnesses },
|
||||
at: std::cell::Cell::new(None),
|
||||
};
|
||||
let data = IndexData::build(
|
||||
names,
|
||||
units
|
||||
.iter()
|
||||
.flat_map(|u| u.declarations.iter().map(move |d| (u.file, d))),
|
||||
&lowered.expressions,
|
||||
&lowered.occurrences,
|
||||
&original,
|
||||
budget,
|
||||
&mut Storage(None),
|
||||
)?;
|
||||
Ok(Self {
|
||||
sources,
|
||||
names: data.names,
|
||||
occurrences: data.occurrences,
|
||||
expressions: data.expressions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use super::mixed::project::{
|
||||
IndexInput,
|
||||
accounting::{Ledger, Slots},
|
||||
};
|
||||
|
||||
/// Owned maps shared by the two nominal provenance wrappers. No edit authority.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct IndexData {
|
||||
pub(super) names: BTreeMap<Target, String>,
|
||||
pub(super) occurrences: BTreeMap<OccurrenceKey, Location>,
|
||||
pub(super) expressions: BTreeMap<ExpressionKey, Location>,
|
||||
}
|
||||
|
||||
// Only project::index can mint IndexInput: its private seal is never exported.
|
||||
pub(super) fn mixed_data(
|
||||
input: IndexInput<'_>,
|
||||
budget: &mut Budget,
|
||||
ledger: &mut Ledger,
|
||||
) -> Result<IndexData, lower::Failure> {
|
||||
let original = Original {
|
||||
kind: OriginalKind::Mixed(&input),
|
||||
at: std::cell::Cell::new(None),
|
||||
};
|
||||
let mut storage = Storage(Some(ledger));
|
||||
let result = (|| {
|
||||
budget.active()?;
|
||||
let mut names = BTreeMap::new();
|
||||
let mut insert = |target, name: &str| -> Result<(), Error> {
|
||||
storage.reserve(budget, 1, name.len(), Span::default())?;
|
||||
if names.insert(target, name.to_owned()).is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Duplicate qualified catalog identity.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let p = input.project;
|
||||
insert(Target::Global(p.id), &p.alias)?;
|
||||
for d in p.definitions.values() {
|
||||
insert(Target::Global(d.id), &d.alias)?;
|
||||
if let a::DefinitionBody::Calculation(c) = &d.body {
|
||||
for parameter in &c.parameters {
|
||||
insert(
|
||||
Target::Parameter {
|
||||
calculation: d.id,
|
||||
parameter: parameter.id,
|
||||
},
|
||||
¶meter.name,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for v in p.elements.values() {
|
||||
insert(Target::Global(v.id), &v.alias)?;
|
||||
}
|
||||
for v in p.relations.values() {
|
||||
insert(Target::Global(v.id), &v.alias)?;
|
||||
}
|
||||
for v in p.diagrams.values() {
|
||||
insert(Target::Global(v.id), &v.alias)?;
|
||||
for n in v.nodes.values() {
|
||||
insert(Target::Global(n.id), &n.alias)?;
|
||||
}
|
||||
}
|
||||
for v in p.artifact_links.values() {
|
||||
insert(Target::Global(v.id), &v.alias)?;
|
||||
}
|
||||
for v in p.traces.values() {
|
||||
insert(Target::Global(v.id), &v.alias)?;
|
||||
}
|
||||
IndexData::build(
|
||||
names,
|
||||
input.declarations(),
|
||||
input.expressions,
|
||||
input.occurrences,
|
||||
&original,
|
||||
budget,
|
||||
&mut storage,
|
||||
)
|
||||
})();
|
||||
result.map_err(|error| lower::Failure {
|
||||
file: original.at.get(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
struct Storage<'a>(Option<&'a mut Ledger>);
|
||||
impl Storage<'_> {
|
||||
fn reserve(
|
||||
&mut self,
|
||||
budget: &Budget,
|
||||
entries: usize,
|
||||
text: usize,
|
||||
at: Span,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(ledger) = &mut self.0 {
|
||||
ledger.reserve(
|
||||
budget,
|
||||
Slots {
|
||||
entries,
|
||||
occurrences: 0,
|
||||
text,
|
||||
},
|
||||
at,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn release(&mut self, entries: usize) {
|
||||
if let Some(ledger) = &mut self.0 {
|
||||
ledger.release(Slots::entries(entries));
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Original<'a> {
|
||||
kind: OriginalKind<'a>,
|
||||
at: std::cell::Cell<Option<usize>>,
|
||||
}
|
||||
enum OriginalKind<'a> {
|
||||
Typed {
|
||||
sources: &'a BTreeMap<usize, String>,
|
||||
witnesses: &'a BTreeMap<usize, Vec<lex::Token>>,
|
||||
},
|
||||
Mixed(&'a IndexInput<'a>),
|
||||
}
|
||||
impl Original<'_> {
|
||||
fn checked(&self, location: Location) -> Result<&str, Error> {
|
||||
self.at.set(Some(location.file));
|
||||
if location.span.start >= location.span.end {
|
||||
return Err(error(location.span, "Empty or reversed source span."));
|
||||
}
|
||||
let source = match &self.kind {
|
||||
OriginalKind::Typed { sources, .. } => sources.get(&location.file).map(String::as_str),
|
||||
OriginalKind::Mixed(input) => input.source(location.file),
|
||||
};
|
||||
source
|
||||
.and_then(|s| s.get(location.span.start..location.span.end))
|
||||
.ok_or_else(|| {
|
||||
error(
|
||||
location.span,
|
||||
"Source span is outside its original UTF-8 file.",
|
||||
)
|
||||
})
|
||||
}
|
||||
fn tokens(&self, file: usize) -> Option<&[lex::Token]> {
|
||||
match &self.kind {
|
||||
OriginalKind::Typed { witnesses, .. } => witnesses.get(&file).map(Vec::as_slice),
|
||||
OriginalKind::Mixed(input) => input.tokens(file),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl IndexData {
|
||||
fn build<'a>(
|
||||
names: BTreeMap<Target, String>,
|
||||
declarations: impl Iterator<Item = (usize, &'a declarations::Declaration)> + Clone,
|
||||
expressions: &[lower::ExpressionSite],
|
||||
occurrences: &[lower::Occurrence],
|
||||
sources: &Original<'_>,
|
||||
budget: &mut Budget,
|
||||
storage: &mut Storage<'_>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut index = Self {
|
||||
names,
|
||||
occurrences: BTreeMap::new(),
|
||||
expressions: BTreeMap::new(),
|
||||
};
|
||||
let mut owners = BTreeMap::new();
|
||||
let mut physical = BTreeSet::new();
|
||||
for (file, declaration) in declarations.clone() {
|
||||
sources.at.set(Some(file));
|
||||
let owner = declaration.header.identity.value.ok_or_else(|| {
|
||||
error(
|
||||
declaration.span,
|
||||
"Missing ID requires source stabilization.",
|
||||
)
|
||||
})?;
|
||||
let location = Location {
|
||||
file,
|
||||
span: declaration.span,
|
||||
};
|
||||
sources.checked(location)?;
|
||||
budget.lowering_entries(owners.len() + 1, declaration.span)?;
|
||||
storage.reserve(budget, 1, 0, declaration.span)?;
|
||||
if owners.insert(owner, location).is_some() {
|
||||
return Err(error(declaration.span, "Duplicate source owner."));
|
||||
}
|
||||
let key = OccurrenceKey {
|
||||
owner,
|
||||
field: None,
|
||||
path: None,
|
||||
role: TokenRole::Declaration,
|
||||
target: Target::Global(owner),
|
||||
};
|
||||
index.insert(
|
||||
key,
|
||||
Location {
|
||||
file,
|
||||
span: declaration.header.alias.span,
|
||||
},
|
||||
sources,
|
||||
&mut physical,
|
||||
budget,
|
||||
storage,
|
||||
)?;
|
||||
}
|
||||
// Parent-before-child is part of the lowering contract. One forward
|
||||
// pass rejects forward, cyclic, cross-file/owner/field and duplicate
|
||||
// paths. Each node allocates one shared edge, not a copied path vector.
|
||||
let mut paths: Vec<(ExpressionKey, usize)> = Vec::new();
|
||||
for (position, site) in expressions.iter().enumerate() {
|
||||
sources.at.set(Some(site.file));
|
||||
if !matches!(
|
||||
site.field,
|
||||
Field::Body | Field::Base | Field::Predicate | Field::Override(_)
|
||||
) {
|
||||
return Err(error(
|
||||
site.span,
|
||||
"Expression occurs outside a binding field.",
|
||||
));
|
||||
}
|
||||
let location = Location {
|
||||
file: site.file,
|
||||
span: site.span,
|
||||
};
|
||||
sources.checked(location)?;
|
||||
let owner = owners
|
||||
.get(&site.owner)
|
||||
.ok_or_else(|| error(site.span, "Unknown expression owner."))?;
|
||||
contained(*owner, location)?;
|
||||
let (path, depth) = if let Some((parent, edge)) = site.parent {
|
||||
if parent >= position {
|
||||
return Err(error(
|
||||
site.span,
|
||||
"Expression parent must precede its child.",
|
||||
));
|
||||
}
|
||||
let ancestor = &expressions[parent];
|
||||
if ancestor.owner != site.owner
|
||||
|| ancestor.field != site.field
|
||||
|| ancestor.file != site.file
|
||||
{
|
||||
return Err(error(
|
||||
site.span,
|
||||
"Expression parent crosses its owner, field or file.",
|
||||
));
|
||||
}
|
||||
contained(
|
||||
Location {
|
||||
file: ancestor.file,
|
||||
span: ancestor.span,
|
||||
},
|
||||
location,
|
||||
)?;
|
||||
if ancestor.span == site.span {
|
||||
return Err(error(
|
||||
site.span,
|
||||
"Child expression must be a proper source subtree.",
|
||||
));
|
||||
}
|
||||
let depth = paths[parent].1 + 1;
|
||||
budget.frame(depth, site.span)?;
|
||||
storage.reserve(budget, 1, 0, site.span)?;
|
||||
(
|
||||
Path(Some(Arc::new(Step {
|
||||
parent: paths[parent].0.path.clone(),
|
||||
edge,
|
||||
}))),
|
||||
depth,
|
||||
)
|
||||
} else {
|
||||
budget.frame(1, site.span)?;
|
||||
(Path::default(), 1)
|
||||
};
|
||||
let key = ExpressionKey {
|
||||
owner: site.owner,
|
||||
field: site.field,
|
||||
path,
|
||||
};
|
||||
storage.reserve(budget, 2, 0, site.span)?;
|
||||
if index.expressions.insert(key.clone(), location).is_some() {
|
||||
return Err(error(site.span, "Duplicate qualified expression path."));
|
||||
}
|
||||
paths.push((key, depth));
|
||||
}
|
||||
for occurrence in occurrences {
|
||||
sources.at.set(Some(occurrence.file));
|
||||
let location = Location {
|
||||
file: occurrence.file,
|
||||
span: occurrence.span,
|
||||
};
|
||||
let owner = owners
|
||||
.get(&occurrence.owner)
|
||||
.ok_or_else(|| error(occurrence.span, "Unknown occurrence owner."))?;
|
||||
contained(*owner, location)?;
|
||||
let path = match occurrence.expression {
|
||||
Some(site) => {
|
||||
let expression = expressions
|
||||
.get(site)
|
||||
.ok_or_else(|| error(occurrence.span, "Unknown occurrence expression."))?;
|
||||
if expression.owner != occurrence.owner
|
||||
|| expression.field != occurrence.field
|
||||
|| expression.file != occurrence.file
|
||||
{
|
||||
return Err(error(
|
||||
occurrence.span,
|
||||
"Occurrence crosses its expression owner, field or file.",
|
||||
));
|
||||
}
|
||||
contained(
|
||||
Location {
|
||||
file: expression.file,
|
||||
span: expression.span,
|
||||
},
|
||||
location,
|
||||
)?;
|
||||
Some(paths[site].0.path.clone())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
match (occurrence.role, occurrence.target) {
|
||||
(
|
||||
Role::ParameterDeclaration,
|
||||
Target::Parameter {
|
||||
calculation,
|
||||
parameter,
|
||||
},
|
||||
) if calculation == occurrence.owner
|
||||
&& occurrence.field == Field::Input(parameter)
|
||||
&& path.is_none() => {}
|
||||
(Role::ParameterUse, Target::Parameter { calculation, .. })
|
||||
if calculation == occurrence.owner
|
||||
&& occurrence.field == Field::Body
|
||||
&& path.is_some() => {}
|
||||
(Role::ArgumentLabel, Target::Parameter { .. }) if path.is_some() => {}
|
||||
(Role::OverrideTarget, Target::Global(id))
|
||||
if occurrence.field == Field::Override(id) && path.is_none() => {}
|
||||
(Role::CallTarget, Target::Global(_)) if path.is_some() => {}
|
||||
(Role::Reference, Target::Global(_)) => {}
|
||||
_ => return Err(error(occurrence.span, "Invalid qualified occurrence role.")),
|
||||
}
|
||||
let key = OccurrenceKey {
|
||||
owner: occurrence.owner,
|
||||
field: Some(occurrence.field),
|
||||
path,
|
||||
role: TokenRole::Use(occurrence.role),
|
||||
target: occurrence.target,
|
||||
};
|
||||
index.insert(key, location, sources, &mut physical, budget, storage)?;
|
||||
}
|
||||
let owner_count = owners.len();
|
||||
let physical_count = physical.len();
|
||||
drop(owners);
|
||||
drop(physical);
|
||||
storage.release(owner_count + physical_count);
|
||||
// Argument labels belong to this precise call site and its callee, not
|
||||
// merely to a parameter UUID/name present somewhere in the catalog.
|
||||
let mut calls = BTreeMap::new();
|
||||
for (key, at) in &index.occurrences {
|
||||
sources.at.set(Some(at.file));
|
||||
if key.role == TokenRole::Use(Role::CallTarget) {
|
||||
let site = (key.owner, key.field, key.path.clone());
|
||||
storage.reserve(budget, 1, 0, at.span)?;
|
||||
if calls.insert(site, key.target).is_some() {
|
||||
return Err(error(at.span, "Multiple callees at one expression site."));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, at) in &index.occurrences {
|
||||
sources.at.set(Some(at.file));
|
||||
if let (TokenRole::Use(Role::ArgumentLabel), Target::Parameter { calculation, .. }) =
|
||||
(key.role, key.target)
|
||||
&& calls.get(&(key.owner, key.field, key.path.clone()))
|
||||
!= Some(&Target::Global(calculation))
|
||||
{
|
||||
return Err(error(
|
||||
at.span,
|
||||
"Argument label crosses its called calculation.",
|
||||
));
|
||||
}
|
||||
}
|
||||
for (position, site) in expressions.iter().enumerate() {
|
||||
sources.at.set(Some(site.file));
|
||||
if let Some((parent, Child::Argument(parameter))) = site.parent {
|
||||
let key = &paths[parent].0;
|
||||
let Some(Target::Global(calculation)) =
|
||||
calls.get(&(key.owner, Some(key.field), Some(key.path.clone())))
|
||||
else {
|
||||
return Err(error(site.span, "Argument child has no owning call."));
|
||||
};
|
||||
let label = OccurrenceKey {
|
||||
owner: site.owner,
|
||||
field: Some(site.field),
|
||||
path: Some(key.path.clone()),
|
||||
role: TokenRole::Use(Role::ArgumentLabel),
|
||||
target: Target::Parameter {
|
||||
calculation: *calculation,
|
||||
parameter,
|
||||
},
|
||||
};
|
||||
if !index.occurrences.contains_key(&label) || paths[position].0.path == key.path {
|
||||
return Err(error(site.span, "Argument child has no qualified label."));
|
||||
}
|
||||
}
|
||||
}
|
||||
let path_count = paths.len();
|
||||
let call_count = calls.len();
|
||||
drop(paths);
|
||||
drop(calls);
|
||||
storage.release(path_count + call_count);
|
||||
index.check_syntax_edges(declarations, expressions, sources, budget, storage)?;
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
/// Check structural edges against the already parsed syntax, not just the
|
||||
/// arena's internally consistent claims. This is one iterative tree walk,
|
||||
/// with indexed span lookup; it does not lex or copy source/path strings.
|
||||
fn check_syntax_edges<'a>(
|
||||
&self,
|
||||
declarations: impl Iterator<Item = (usize, &'a declarations::Declaration)> + Clone,
|
||||
expressions: &[lower::ExpressionSite],
|
||||
sources: &Original<'_>,
|
||||
budget: &Budget,
|
||||
storage: &mut Storage<'_>,
|
||||
) -> Result<(), Error> {
|
||||
let mut globals = BTreeMap::new();
|
||||
let mut parameters = BTreeMap::new();
|
||||
sources.at.set(None);
|
||||
storage.reserve(budget, self.names.len(), 0, Span::default())?;
|
||||
for (target, name) in &self.names {
|
||||
match target {
|
||||
Target::Global(id) => {
|
||||
globals.insert(name.as_str(), *id);
|
||||
}
|
||||
Target::Parameter {
|
||||
calculation,
|
||||
parameter,
|
||||
} => {
|
||||
parameters.insert((*calculation, name.as_str()), *parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sites = BTreeMap::new();
|
||||
for (position, site) in expressions.iter().enumerate() {
|
||||
sources.at.set(Some(site.file));
|
||||
storage.reserve(budget, 1, 0, site.span)?;
|
||||
if sites
|
||||
.insert(
|
||||
(
|
||||
site.file,
|
||||
site.owner,
|
||||
site.field,
|
||||
site.span.start,
|
||||
site.span.end,
|
||||
),
|
||||
position,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
return Err(error(site.span, "Duplicate expression source ownership."));
|
||||
}
|
||||
}
|
||||
let mut work = Vec::new();
|
||||
let mut root = |file, owner, field, binding: &'a super::Binding| -> Result<(), Error> {
|
||||
if let super::Binding::Expression(expression) = binding {
|
||||
budget.lowering_entries(work.len() + 1, expression.span)?;
|
||||
sources.at.set(Some(file));
|
||||
storage.reserve(budget, 1, 0, expression.span)?;
|
||||
work.push((file, owner, field, expression, None));
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for (file, declaration) in declarations.clone() {
|
||||
sources.at.set(Some(file));
|
||||
let owner = declaration.header.identity.value.expect("validated owner");
|
||||
match &declaration.body {
|
||||
declarations::Body::Calculation { body, .. } => {
|
||||
root(file, owner, Field::Body, &body.value)?
|
||||
}
|
||||
declarations::Body::Property { base, .. } => {
|
||||
root(file, owner, Field::Base, &base.value)?
|
||||
}
|
||||
declarations::Body::Check { predicate, .. } => {
|
||||
root(file, owner, Field::Predicate, &predicate.value)?
|
||||
}
|
||||
declarations::Body::Case { overrides, .. } => {
|
||||
for entry in &overrides.value {
|
||||
let target = *globals.get(entry.target.name.as_str()).ok_or_else(|| {
|
||||
error(entry.target.span, "Unknown syntax override target.")
|
||||
})?;
|
||||
root(file, owner, Field::Override(target), &entry.binding)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut visited = 0usize;
|
||||
while let Some((file, owner, field, expression, parent)) = work.pop() {
|
||||
storage.release(1);
|
||||
sources.at.set(Some(file));
|
||||
if storage.0.is_none() {
|
||||
budget.lowering_entries(visited + 1, expression.span)?;
|
||||
}
|
||||
visited += 1;
|
||||
let site = *sites
|
||||
.get(&(
|
||||
file,
|
||||
owner,
|
||||
field,
|
||||
expression.span.start,
|
||||
expression.span.end,
|
||||
))
|
||||
.ok_or_else(|| {
|
||||
error(
|
||||
expression.span,
|
||||
"Parsed expression is absent from the source arena.",
|
||||
)
|
||||
})?;
|
||||
if expressions[site].parent != parent {
|
||||
return Err(error(
|
||||
expression.span,
|
||||
"Arena parent/child edge differs from original syntax.",
|
||||
));
|
||||
}
|
||||
let mut child = |value: &'a super::Expression, edge| -> Result<(), Error> {
|
||||
budget.lowering_entries(work.len() + 1, value.span)?;
|
||||
storage.reserve(budget, 1, 0, value.span)?;
|
||||
work.push((file, owner, field, value, Some((site, edge))));
|
||||
Ok(())
|
||||
};
|
||||
use super::ExpressionKind as E;
|
||||
match &expression.kind {
|
||||
E::List(items) => {
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
child(item, Child::Item(index))?;
|
||||
}
|
||||
}
|
||||
E::Unary { operand, .. } => child(operand, Child::Operand)?,
|
||||
E::Binary { left, right, .. } => {
|
||||
child(left, Child::Left)?;
|
||||
child(right, Child::Right)?;
|
||||
}
|
||||
E::If {
|
||||
condition,
|
||||
when_true,
|
||||
when_false,
|
||||
} => {
|
||||
child(condition, Child::Condition)?;
|
||||
child(when_true, Child::WhenTrue)?;
|
||||
child(when_false, Child::WhenFalse)?;
|
||||
}
|
||||
E::Call {
|
||||
definition,
|
||||
arguments,
|
||||
} => {
|
||||
let calculation = *globals
|
||||
.get(definition.name.as_str())
|
||||
.ok_or_else(|| error(definition.span, "Unknown syntax call target."))?;
|
||||
for argument in arguments {
|
||||
let parameter = *parameters
|
||||
.get(&(calculation, argument.parameter.name.as_str()))
|
||||
.ok_or_else(|| {
|
||||
error(argument.parameter.span, "Unknown syntax call parameter.")
|
||||
})?;
|
||||
child(&argument.value, Child::Argument(parameter))?;
|
||||
}
|
||||
}
|
||||
E::Sum { values, initial } => {
|
||||
child(values, Child::Values)?;
|
||||
child(initial, Child::Initial)?;
|
||||
}
|
||||
E::Power { base, .. } => child(base, Child::Base)?,
|
||||
E::AsKind { value, .. } => child(value, Child::Value)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
sources.at.set(None);
|
||||
let scratch = globals.len() + parameters.len() + sites.len();
|
||||
drop(globals);
|
||||
drop(parameters);
|
||||
drop(sites);
|
||||
drop(work);
|
||||
storage.release(scratch);
|
||||
if visited != expressions.len() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Arena contains expressions absent from parsed syntax.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert(
|
||||
&mut self,
|
||||
key: OccurrenceKey,
|
||||
location: Location,
|
||||
sources: &Original<'_>,
|
||||
physical: &mut BTreeSet<(usize, usize, usize)>,
|
||||
budget: &Budget,
|
||||
storage: &mut Storage<'_>,
|
||||
) -> Result<(), Error> {
|
||||
if storage.0.is_none() {
|
||||
budget.lowering_occurrences(self.occurrences.len() + 1, location.span)?;
|
||||
}
|
||||
storage.reserve(budget, 2, 0, location.span)?;
|
||||
let text = sources.checked(location)?;
|
||||
if self.names.get(&key.target).is_none_or(|name| name != text) {
|
||||
return Err(error(
|
||||
location.span,
|
||||
"Token differs from its qualified target name.",
|
||||
));
|
||||
}
|
||||
let tokens = sources
|
||||
.tokens(location.file)
|
||||
.ok_or_else(|| error(location.span, "Missing lexical witness."))?;
|
||||
let token = tokens
|
||||
.binary_search_by_key(&location.span.start, |token| token.span.start)
|
||||
.ok()
|
||||
.map(|at| tokens[at]);
|
||||
if token.is_none_or(|token| token.span != location.span || token.class != lex::Class::Word)
|
||||
{
|
||||
return Err(error(
|
||||
location.span,
|
||||
"Occurrence is not an original whole name token.",
|
||||
));
|
||||
}
|
||||
if self.occurrences.contains_key(&key)
|
||||
|| !physical.insert((location.file, location.span.start, location.span.end))
|
||||
{
|
||||
return Err(error(
|
||||
location.span,
|
||||
"Duplicate qualified occurrence or token ownership.",
|
||||
));
|
||||
}
|
||||
self.occurrences.insert(key, location);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn checked_source(
|
||||
sources: &BTreeMap<usize, String>,
|
||||
location: Location,
|
||||
) -> Result<&str, Error> {
|
||||
if location.span.start >= location.span.end {
|
||||
return Err(error(location.span, "Empty or reversed source span."));
|
||||
}
|
||||
sources
|
||||
.get(&location.file)
|
||||
.and_then(|source| source.get(location.span.start..location.span.end))
|
||||
.ok_or_else(|| {
|
||||
error(
|
||||
location.span,
|
||||
"Source span is outside its original UTF-8 file.",
|
||||
)
|
||||
})
|
||||
}
|
||||
fn contained(parent: Location, child: Location) -> Result<(), Error> {
|
||||
if parent.file != child.file
|
||||
|| child.span.start < parent.span.start
|
||||
|| child.span.end > parent.span.end
|
||||
{
|
||||
return Err(error(
|
||||
child.span,
|
||||
"Source span crosses its containing syntax.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,630 @@
|
||||
use super::*;
|
||||
use crate::language::ParseLimits;
|
||||
|
||||
const OA: &str = include_str!("../lower/fixtures/typed-oa.arcadia");
|
||||
const ORDINARY: &str = include_str!("../lower/fixtures/ordinary-oa.arcadia");
|
||||
fn budget() -> Budget {
|
||||
Budget::new(super::super::budget::Limits::source8(
|
||||
ParseLimits::UNLIMITED,
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
fn ordinary() -> Project {
|
||||
crate::language::parse(ORDINARY).unwrap()
|
||||
}
|
||||
fn sources() -> BTreeMap<usize, String> {
|
||||
let split = OA.find("calculation combine").unwrap();
|
||||
BTreeMap::from([
|
||||
(7, OA[..split].replace('\n', "\r\n")),
|
||||
(19, OA[split..].to_owned()),
|
||||
])
|
||||
}
|
||||
fn raw(
|
||||
sources: &BTreeMap<usize, String>,
|
||||
budget: &mut Budget,
|
||||
) -> (Vec<Unit>, BTreeMap<usize, Vec<lex::Token>>, Lowered) {
|
||||
let mut units = Vec::new();
|
||||
let mut witnesses = BTreeMap::new();
|
||||
for (&file, source) in sources {
|
||||
let tokens = lex::tokens(source, budget).unwrap();
|
||||
let mut cursor = Cursor::new(source, &tokens, budget);
|
||||
let mut declarations = Vec::new();
|
||||
while cursor.cursor < tokens.len() {
|
||||
declarations.push(declarations::parse(&mut cursor, 8, None).unwrap());
|
||||
}
|
||||
units.push(Unit { file, declarations });
|
||||
witnesses.insert(file, tokens);
|
||||
}
|
||||
let lowered = lower::lower(&ordinary(), &units, budget).unwrap();
|
||||
(units, witnesses, lowered)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_index_preserves_complete_oa_sa_models_and_original_file_spans() {
|
||||
for (layer, ordinary_source, typed) in [
|
||||
(crate::Layer::Oa, ORDINARY, OA),
|
||||
(
|
||||
crate::Layer::Sa,
|
||||
include_str!("../lower/fixtures/ordinary-sa.arcadia"),
|
||||
include_str!("../lower/fixtures/typed-sa.arcadia"),
|
||||
),
|
||||
] {
|
||||
let sources = BTreeMap::from([(37, typed.replace('\n', "\r\n"))]);
|
||||
let skeleton = crate::language::parse(ordinary_source).unwrap();
|
||||
let mut work = budget();
|
||||
let (index, lowered) = Index::parse(&skeleton, &sources, &mut work).unwrap();
|
||||
let mut actual = skeleton;
|
||||
actual.definitions.extend(lowered.definitions);
|
||||
actual.elements.extend(lowered.elements);
|
||||
assert_eq!(actual, super::super::model_oracle::project(layer));
|
||||
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
|
||||
assert_eq!(work.usage.bytes, sources[&37].len(), "one lexical pass");
|
||||
for (key, at) in index.occurrences() {
|
||||
assert_eq!(at.file, 37);
|
||||
assert_eq!(
|
||||
&sources[&37][at.span.start..at.span.end],
|
||||
index.names[&key.target]
|
||||
);
|
||||
}
|
||||
assert!(!index.expressions().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_reordered_calls_have_stable_full_paths_and_four_labels() {
|
||||
let expression = "@combine(b: @dispatch_time, a: @receive_time) // first 雪\n + @combine(a: @receive_time, b: @dispatch_time)";
|
||||
let original = OA.replace("@combine(a: @receive_time, b: @dispatch_time)", expression);
|
||||
let reordered = original.replace(
|
||||
"@combine(b: @dispatch_time, a: @receive_time)",
|
||||
"@combine(a: @receive_time, b: @dispatch_time)",
|
||||
);
|
||||
let first = BTreeMap::from([(7, original)]);
|
||||
let second = BTreeMap::from([(7, reordered)]);
|
||||
let (left, _) = Index::parse(&ordinary(), &first, &mut budget()).unwrap();
|
||||
let (right, _) = Index::parse(&ordinary(), &second, &mut budget()).unwrap();
|
||||
assert_eq!(
|
||||
left.occurrences.keys().collect::<Vec<_>>(),
|
||||
right.occurrences.keys().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
left.expressions.keys().collect::<Vec<_>>(),
|
||||
right.expressions.keys().collect::<Vec<_>>()
|
||||
);
|
||||
let labels: Vec<_> = left
|
||||
.occurrences
|
||||
.iter()
|
||||
.filter(|(key, _)| key.role == TokenRole::Use(Role::ArgumentLabel))
|
||||
.collect();
|
||||
assert_eq!(labels.len(), 4);
|
||||
let paths: BTreeSet<_> = labels
|
||||
.iter()
|
||||
.map(|(key, _)| key.path.as_ref().unwrap().edges())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
paths,
|
||||
BTreeSet::from([vec![Child::Left], vec![Child::Right]])
|
||||
);
|
||||
let spans: BTreeSet<_> = labels
|
||||
.iter()
|
||||
.map(|(_, at)| (at.span.start, at.span.end))
|
||||
.collect();
|
||||
assert_eq!(spans.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_checks_every_expression_edge_and_keeps_override_fields_distinct() {
|
||||
let source = OA
|
||||
.replace("body $a + $b;", "body if(if(true, 1 < 2, false), sum([$a, abs(-$b)], 0 [@second]), as_kind(@duration, power(1 [@second], 2) / 1 [@second]));")
|
||||
.replace("@receive_time = 1250 [@millisecond];", "@receive_time = @combine(a: 1 [@second], b: 2 [@second]); @dispatch_time = @combine(b: 2 [@second], a: 1 [@second]);");
|
||||
let sources = BTreeMap::from([(17, source)]);
|
||||
let (index, lowered) = Index::parse(&ordinary(), &sources, &mut budget()).unwrap();
|
||||
let mut project = ordinary();
|
||||
project.definitions.extend(lowered.definitions);
|
||||
project.elements.extend(lowered.elements);
|
||||
a::admit(&project, a::AdmissionLimits::REFERENCE).unwrap();
|
||||
let edges: BTreeSet<_> = index
|
||||
.expressions
|
||||
.keys()
|
||||
.flat_map(|key| key.path.edges())
|
||||
.collect();
|
||||
for edge in [
|
||||
Child::Item(0),
|
||||
Child::Item(1),
|
||||
Child::Operand,
|
||||
Child::Left,
|
||||
Child::Right,
|
||||
Child::Condition,
|
||||
Child::WhenTrue,
|
||||
Child::WhenFalse,
|
||||
Child::Values,
|
||||
Child::Initial,
|
||||
Child::Base,
|
||||
Child::Value,
|
||||
] {
|
||||
assert!(edges.contains(&edge), "missing {edge:?}");
|
||||
}
|
||||
assert!(edges.iter().any(|edge| matches!(edge, Child::Argument(_))));
|
||||
let labels: Vec<_> = index
|
||||
.occurrences
|
||||
.keys()
|
||||
.filter(|key| {
|
||||
key.role == TokenRole::Use(Role::ArgumentLabel)
|
||||
&& matches!(key.field, Some(Field::Override(_)))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(labels.len(), 4);
|
||||
assert_eq!(
|
||||
labels
|
||||
.iter()
|
||||
.map(|key| key.field)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert!(
|
||||
labels
|
||||
.iter()
|
||||
.all(|key| key.path.as_ref().unwrap().edges().is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arena_rejects_forward_cycles_and_cross_owner_field_file_parents() {
|
||||
for damage in 0..6 {
|
||||
let sources = sources();
|
||||
let mut work = budget();
|
||||
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
|
||||
let child = lowered
|
||||
.expressions
|
||||
.iter()
|
||||
.position(|site| site.parent.is_some())
|
||||
.unwrap();
|
||||
let parent = lowered.expressions[child].parent.unwrap().0;
|
||||
match damage {
|
||||
0 => lowered.expressions[child].parent = Some((child, Child::Left)),
|
||||
1 => lowered.expressions[parent].parent = Some((child, Child::Right)),
|
||||
2 => lowered.expressions[child].owner = ordinary().id,
|
||||
3 => lowered.expressions[child].field = Field::Predicate,
|
||||
4 => lowered.expressions[child].file = 12345,
|
||||
_ => lowered.expressions[child].parent = Some((parent, Child::Operand)),
|
||||
}
|
||||
assert!(
|
||||
Index::build(
|
||||
&ordinary(),
|
||||
&sources,
|
||||
&units,
|
||||
&witnesses,
|
||||
&lowered,
|
||||
&mut work
|
||||
)
|
||||
.is_err(),
|
||||
"damage {damage}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arena_rejects_duplicate_sites_occurrences_and_wrong_callee_labels() {
|
||||
for damage in 0..4 {
|
||||
let mut sources = sources();
|
||||
// The other callee is fully valid and reuses both parameter identities
|
||||
// and spellings. Catalog/token validation must not explain rejection.
|
||||
sources.insert(
|
||||
31,
|
||||
r#"calculation other "Other" id "00000000-0000-0000-0000-00000000cafe" {
|
||||
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Quantity(@duration);
|
||||
input b id "7a47cc1e-8ae5-4551-b2d8-8a68108e0976" : Quantity(@duration);
|
||||
result Quantity(@duration); body $a + $b;
|
||||
}"#
|
||||
.into(),
|
||||
);
|
||||
let mut work = budget();
|
||||
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
|
||||
let valid = Index::build(
|
||||
&ordinary(),
|
||||
&sources,
|
||||
&units,
|
||||
&witnesses,
|
||||
&lowered,
|
||||
&mut work,
|
||||
)
|
||||
.unwrap();
|
||||
let mut expected_mismatch_span = None;
|
||||
match damage {
|
||||
0 => {
|
||||
let site = &lowered.expressions[0];
|
||||
lowered.expressions.push(lower::ExpressionSite {
|
||||
file: site.file,
|
||||
owner: site.owner,
|
||||
field: site.field,
|
||||
parent: site.parent,
|
||||
span: site.span,
|
||||
});
|
||||
}
|
||||
1 => {
|
||||
let row = &lowered.occurrences[0];
|
||||
lowered.occurrences.push(lower::Occurrence {
|
||||
file: row.file,
|
||||
owner: row.owner,
|
||||
field: row.field,
|
||||
expression: row.expression,
|
||||
role: row.role,
|
||||
target: row.target,
|
||||
span: row.span,
|
||||
});
|
||||
}
|
||||
2 => {
|
||||
let label = lowered
|
||||
.occurrences
|
||||
.iter_mut()
|
||||
.find(|row| row.role == Role::ArgumentLabel)
|
||||
.unwrap();
|
||||
let original = label.target;
|
||||
let Target::Parameter {
|
||||
calculation,
|
||||
parameter,
|
||||
} = &mut label.target
|
||||
else {
|
||||
panic!("argument label must target a parameter");
|
||||
};
|
||||
let other = Target::Parameter {
|
||||
calculation: Id::from_u128(0xcafe),
|
||||
parameter: *parameter,
|
||||
};
|
||||
assert_eq!(valid.names[&original], valid.names[&other]);
|
||||
*calculation = Id::from_u128(0xcafe);
|
||||
expected_mismatch_span = Some(label.span);
|
||||
}
|
||||
_ => {
|
||||
let row = lowered
|
||||
.occurrences
|
||||
.iter_mut()
|
||||
.find(|row| row.role == Role::OverrideTarget)
|
||||
.unwrap();
|
||||
row.field = Field::Override(ordinary().id);
|
||||
}
|
||||
}
|
||||
let failure = Index::build(
|
||||
&ordinary(),
|
||||
&sources,
|
||||
&units,
|
||||
&witnesses,
|
||||
&lowered,
|
||||
&mut work,
|
||||
)
|
||||
.unwrap_err();
|
||||
if let Some(span) = expected_mismatch_span {
|
||||
assert_eq!(
|
||||
failure.message,
|
||||
"Argument label crosses its called calculation."
|
||||
);
|
||||
assert_eq!(failure.span, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_rejects_comment_partial_and_non_utf8_occurrence_spans() {
|
||||
for damage in 0..3 {
|
||||
let mut sources = sources();
|
||||
let with_comment =
|
||||
sources[&7].replace("subject @receive;", "subject @receive; // duration 雪\r\n");
|
||||
sources.insert(7, with_comment);
|
||||
let mut work = budget();
|
||||
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
|
||||
let occurrence = lowered
|
||||
.occurrences
|
||||
.iter_mut()
|
||||
.find(|row| row.file == 7 && &sources[&7][row.span.start..row.span.end] == "duration")
|
||||
.unwrap();
|
||||
match damage {
|
||||
0 => {
|
||||
let start = sources[&7].find("// duration").unwrap() + 3;
|
||||
occurrence.span = Span {
|
||||
start,
|
||||
end: start + 8,
|
||||
};
|
||||
}
|
||||
1 => occurrence.span.end -= 1,
|
||||
_ => {
|
||||
let start = sources[&7].find('雪').unwrap() + 1;
|
||||
occurrence.span = Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
Index::build(
|
||||
&ordinary(),
|
||||
&sources,
|
||||
&units,
|
||||
&witnesses,
|
||||
&lowered,
|
||||
&mut work
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_has_shared_budget_and_never_stabilizes_missing_ids() {
|
||||
let source = "calculation f \"F\" { result Number; body unbound; }";
|
||||
let files = BTreeMap::from([(91, source.into())]);
|
||||
let missing = Index::parse(&ordinary(), &files, &mut budget()).unwrap_err();
|
||||
assert_eq!(missing.file, Some(91));
|
||||
assert!(missing.error.message.contains("stabilization"));
|
||||
let mut limits = super::super::budget::Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.bytes = 1;
|
||||
let mut work = Budget::new(limits).unwrap();
|
||||
assert!(Index::parse(&ordinary(), &sources(), &mut work).is_err());
|
||||
assert!(work.active().is_err());
|
||||
}
|
||||
|
||||
fn on_small_stack(work: impl FnOnce() + Send + 'static) {
|
||||
// Match the established core language_nesting_tests worker-stack request.
|
||||
// This is a requested platform stack size, not a measured peak-byte claim.
|
||||
std::thread::Builder::new()
|
||||
.name("analysis-index-128k".into())
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(work)
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn chain(first: Child, edges: usize) -> Path {
|
||||
let mut path = Path::default();
|
||||
for position in 0..edges {
|
||||
path = Path(Some(Arc::new(Step {
|
||||
parent: path,
|
||||
edge: if position == 0 { first } else { Child::Operand },
|
||||
})));
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_index_paths_compare_lookup_debug_and_drop_on_128k_stack() {
|
||||
on_small_stack(|| {
|
||||
let frames = super::super::budget::Limits::source8(ParseLimits::UNLIMITED).frames;
|
||||
assert_eq!(frames, 256);
|
||||
let edges = frames - 1; // root has frame depth one
|
||||
let mut work = budget();
|
||||
work.frame(frames, Span::default()).unwrap();
|
||||
let left = chain(Child::Left, edges);
|
||||
let equal = chain(Child::Left, edges); // independent allocations
|
||||
let right = chain(Child::Right, edges);
|
||||
assert!(!Arc::ptr_eq(
|
||||
left.0.as_ref().unwrap(),
|
||||
equal.0.as_ref().unwrap()
|
||||
));
|
||||
assert_eq!(left, equal);
|
||||
assert_eq!(left.partial_cmp(&equal), Some(Ordering::Equal));
|
||||
// The difference is at the root-most edge: leaf-first walks every link.
|
||||
assert_eq!(left.cmp(&right), Child::Left.cmp(&Child::Right));
|
||||
assert_eq!(right.cmp(&left), left.cmp(&right).reverse());
|
||||
assert!(Path::default() < left);
|
||||
assert!(chain(Child::Operand, edges - 1) < chain(Child::Operand, edges));
|
||||
let shared = left.clone();
|
||||
assert!(Arc::ptr_eq(
|
||||
left.0.as_ref().unwrap(),
|
||||
shared.0.as_ref().unwrap()
|
||||
));
|
||||
let leaf = Arc::downgrade(left.0.as_ref().unwrap());
|
||||
let key = |path| ExpressionKey {
|
||||
owner: Id::from_u128(1),
|
||||
field: Field::Body,
|
||||
path,
|
||||
};
|
||||
let mut expressions = BTreeMap::new();
|
||||
assert_eq!(expressions.insert(key(left.clone()), 1), None);
|
||||
assert_eq!(expressions.insert(key(equal.clone()), 2), Some(1));
|
||||
assert_eq!(expressions.insert(key(right.clone()), 3), None);
|
||||
assert_eq!(expressions.get(&key(equal.clone())), Some(&2));
|
||||
assert_eq!(expressions.get(&key(chain(Child::Right, edges))), Some(&3));
|
||||
assert_eq!(expressions.len(), 2);
|
||||
let occurrence = |path| OccurrenceKey {
|
||||
owner: Id::from_u128(1),
|
||||
field: Some(Field::Body),
|
||||
path: Some(path),
|
||||
role: TokenRole::Use(Role::ParameterUse),
|
||||
target: Target::Parameter {
|
||||
calculation: Id::from_u128(1),
|
||||
parameter: a::ParameterId(Id::from_u128(2)),
|
||||
},
|
||||
};
|
||||
let mut occurrences = BTreeSet::new();
|
||||
assert!(occurrences.insert(occurrence(left.clone())));
|
||||
assert!(!occurrences.insert(occurrence(equal.clone())));
|
||||
assert!(occurrences.contains(&occurrence(chain(Child::Left, edges))));
|
||||
let debug = format!("{left:?}");
|
||||
assert_eq!(debug.matches("Operand").count(), edges - 1);
|
||||
assert!(debug.ends_with("Left]"));
|
||||
assert!(!format!("{expressions:?} {occurrences:?}").is_empty());
|
||||
drop(expressions);
|
||||
drop(occurrences);
|
||||
drop(shared);
|
||||
drop(left);
|
||||
assert!(leaf.upgrade().is_none(), "maps/clones release their chain");
|
||||
drop(equal);
|
||||
drop(right);
|
||||
// Dropping a child stops at a shared ancestor, which remains usable.
|
||||
let parent = chain(Child::Left, edges - 1);
|
||||
let ancestor = Arc::downgrade(parent.0.as_ref().unwrap());
|
||||
let child = Path(Some(Arc::new(Step {
|
||||
parent: parent.clone(),
|
||||
edge: Child::Right,
|
||||
})));
|
||||
drop(child);
|
||||
assert_eq!(parent.edges().len(), edges - 1);
|
||||
drop(parent);
|
||||
assert!(ancestor.upgrade().is_none());
|
||||
// No other strong owners: final destruction must peel the full chain.
|
||||
let unique = chain(Child::Left, edges);
|
||||
let final_leaf = Arc::downgrade(unique.0.as_ref().unwrap());
|
||||
drop(unique);
|
||||
assert!(final_leaf.upgrade().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_parse_index_plan_and_failed_build_cleanup_on_128k_stack() {
|
||||
on_small_stack(|| {
|
||||
use super::super::update::Plan;
|
||||
let deep = format!("{}$a{}", "abs(".repeat(127), ")".repeat(127));
|
||||
let source = format!(
|
||||
r#"calculation f "Deep 雪" id "00000000-0000-0000-0000-000000000081" {{
|
||||
input a id "00000000-0000-0000-0000-000000000083" : Number;
|
||||
result Number; body {deep};
|
||||
}}
|
||||
calculation caller "Caller" id "00000000-0000-0000-0000-000000000082" {{
|
||||
result Number; body @f(a: 1);
|
||||
}}
|
||||
"#
|
||||
);
|
||||
let sources = BTreeMap::from([(47, source.clone())]);
|
||||
let skeleton = ordinary();
|
||||
let mut work = budget();
|
||||
let (index, lowered) = Index::parse(&skeleton, &sources, &mut work).unwrap();
|
||||
assert_eq!(work.usage.bytes, source.len());
|
||||
assert_eq!(index.expressions.len(), 130); // 128 unary/leaf + call/argument
|
||||
assert_eq!(
|
||||
index.expressions.keys().map(|k| k.path.edges().len()).max(),
|
||||
Some(127)
|
||||
);
|
||||
let renames = BTreeMap::from([
|
||||
(Target::Global(Id::from_u128(0x81)), "renamed_deep".into()),
|
||||
(
|
||||
Target::Parameter {
|
||||
calculation: Id::from_u128(0x81),
|
||||
parameter: a::ParameterId(Id::from_u128(0x83)),
|
||||
},
|
||||
"renamed_arg".into(),
|
||||
),
|
||||
]);
|
||||
let usage = work.usage;
|
||||
let plan = Plan::renames(&index, &renames, &work).unwrap();
|
||||
assert_eq!(plan.edit_count(), 5); // declaration/call + input/use/label
|
||||
let output = plan.apply(&sources, &work).unwrap();
|
||||
assert_eq!(work.usage, usage);
|
||||
// Independent complete original-token oracle; no index-derived offsets.
|
||||
let expected_source = source
|
||||
.replace("calculation f ", "calculation renamed_deep ")
|
||||
.replace("input a id", "input renamed_arg id")
|
||||
.replace("$a", "$renamed_arg")
|
||||
.replace("@f(a: 1)", "@renamed_deep(renamed_arg: 1)");
|
||||
assert_eq!(output, BTreeMap::from([(47, expected_source)]));
|
||||
let (renamed_index, renamed) = Index::parse(&skeleton, &output, &mut budget()).unwrap();
|
||||
assert_eq!(
|
||||
index.occurrences.keys().collect::<Vec<_>>(),
|
||||
renamed_index.occurrences.keys().collect::<Vec<_>>()
|
||||
);
|
||||
// Compare complete independently constructed models, including deepest
|
||||
// parameter identity and the call's parameter-keyed argument map.
|
||||
for (lowered, renamed) in [(lowered, false), (renamed, true)] {
|
||||
let mut expected = skeleton.clone();
|
||||
let parameter = a::ParameterId(Id::from_u128(0x83));
|
||||
let mut expression = a::Expr::Parameter(parameter);
|
||||
for _ in 0..127 {
|
||||
expression = a::Expr::Unary {
|
||||
op: a::UnaryOp::Absolute,
|
||||
operand: Box::new(expression),
|
||||
};
|
||||
}
|
||||
for (id, alias, name, parameters, expression) in [
|
||||
(
|
||||
0x81,
|
||||
if renamed { "renamed_deep" } else { "f" },
|
||||
"Deep 雪",
|
||||
vec![a::Parameter {
|
||||
id: parameter,
|
||||
name: if renamed { "renamed_arg" } else { "a" }.into(),
|
||||
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
}],
|
||||
expression,
|
||||
),
|
||||
(
|
||||
0x82,
|
||||
"caller",
|
||||
"Caller",
|
||||
Vec::new(),
|
||||
a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(Id::from_u128(0x81)),
|
||||
arguments: BTreeMap::from([(
|
||||
parameter,
|
||||
a::Expr::Number(crate::analysis_numeric::Exact::from_integer(1)),
|
||||
)]),
|
||||
},
|
||||
),
|
||||
] {
|
||||
let id = Id::from_u128(id);
|
||||
expected.definitions.insert(
|
||||
id,
|
||||
a::Definition {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
|
||||
parameters,
|
||||
result: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
body: a::Binding::Expression(expression),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut actual = skeleton.clone();
|
||||
actual.definitions.extend(lowered.definitions);
|
||||
actual.elements.extend(lowered.elements);
|
||||
assert_eq!(actual, expected);
|
||||
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
|
||||
drop(actual);
|
||||
drop(expected);
|
||||
}
|
||||
drop(plan);
|
||||
drop(renamed_index);
|
||||
drop(index);
|
||||
// Failure after the deepest keys have been allocated must clean up on
|
||||
// the same stack; no modified Lowered escapes this private test seam.
|
||||
let mut work = budget();
|
||||
let (units, witnesses, mut lowered) = raw(&sources, &mut work);
|
||||
let deepest = lowered
|
||||
.expressions
|
||||
.iter()
|
||||
.find(|site| &sources[&site.file][site.span.start..site.span.end] == "$a")
|
||||
.unwrap();
|
||||
lowered.expressions.push(lower::ExpressionSite {
|
||||
file: deepest.file,
|
||||
owner: deepest.owner,
|
||||
field: deepest.field,
|
||||
parent: deepest.parent,
|
||||
span: deepest.span,
|
||||
});
|
||||
let failure =
|
||||
Index::build(&skeleton, &sources, &units, &witnesses, &lowered, &mut work).unwrap_err();
|
||||
assert_eq!(failure.message, "Duplicate qualified expression path.");
|
||||
lowered.expressions.pop();
|
||||
let mut limits = super::super::budget::Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.frames = 127;
|
||||
let mut bounded = Budget::new(limits).unwrap();
|
||||
let failure = Index::build(
|
||||
&skeleton,
|
||||
&sources,
|
||||
&units,
|
||||
&witnesses,
|
||||
&lowered,
|
||||
&mut bounded,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(failure.message.contains("syntax frames"));
|
||||
assert!(bounded.active().is_err());
|
||||
drop(lowered);
|
||||
drop(witnesses);
|
||||
drop(units);
|
||||
let rejected = BTreeMap::from([(47, source.replace(&deep, &format!("abs({deep})")))]);
|
||||
let failure = Index::parse(&skeleton, &rejected, &mut budget()).unwrap_err();
|
||||
assert!(failure.error.message.contains("logical depth"));
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) enum Category {
|
||||
Calculation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) enum Target {
|
||||
Global(Id),
|
||||
Parameter {
|
||||
@@ -43,7 +43,7 @@ pub(super) enum Target {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) enum Field {
|
||||
Algebra,
|
||||
Kind,
|
||||
@@ -59,7 +59,7 @@ pub(super) enum Field {
|
||||
Override(Id),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) enum Role {
|
||||
Reference,
|
||||
ParameterDeclaration,
|
||||
@@ -70,7 +70,7 @@ pub(super) enum Role {
|
||||
}
|
||||
|
||||
/// Stable structural edges, independent of alias spelling and argument order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(super) enum Child {
|
||||
Item(usize),
|
||||
Operand,
|
||||
@@ -124,7 +124,7 @@ struct Symbol {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Tables {
|
||||
pub(super) struct Tables {
|
||||
aliases: BTreeMap<String, Symbol>,
|
||||
ids: BTreeSet<Id>,
|
||||
parameters: BTreeMap<Id, BTreeMap<String, a::ParameterId>>,
|
||||
@@ -157,7 +157,7 @@ impl Tables {
|
||||
self.entries += 1;
|
||||
Ok(())
|
||||
}
|
||||
fn insert(
|
||||
pub(super) fn insert(
|
||||
&mut self,
|
||||
id: Id,
|
||||
alias: &str,
|
||||
@@ -233,7 +233,11 @@ impl Tables {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn typed(&mut self, declaration: &Declaration, budget: &Budget) -> Result<(), Error> {
|
||||
pub(super) fn typed(
|
||||
&mut self,
|
||||
declaration: &Declaration,
|
||||
budget: &Budget,
|
||||
) -> Result<(), Error> {
|
||||
let header = &declaration.header;
|
||||
let id = identity(&header.identity, header.alias.span)?;
|
||||
self.insert(
|
||||
@@ -299,31 +303,49 @@ pub(super) fn lower(
|
||||
})?;
|
||||
}
|
||||
}
|
||||
lower_bodies(
|
||||
&tables,
|
||||
units
|
||||
.iter()
|
||||
.flat_map(|unit| unit.declarations.iter().map(move |d| (unit.file, d))),
|
||||
budget,
|
||||
)
|
||||
}
|
||||
|
||||
/// Catalog and borrowed declarations must be from the same private witness.
|
||||
/// The mixed caller reserves aggregate live storage before entering this body;
|
||||
/// the legacy wrapper retains its original individual phase checks.
|
||||
pub(super) fn lower_bodies<'a>(
|
||||
tables: &Tables,
|
||||
declarations: impl IntoIterator<Item = (usize, &'a Declaration)>,
|
||||
budget: &Budget,
|
||||
) -> Result<Lowered, Failure> {
|
||||
budget
|
||||
.active()
|
||||
.map_err(|error| Failure { file: None, error })?;
|
||||
let mut output = Lowered {
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
occurrences: Vec::new(),
|
||||
expressions: Vec::new(),
|
||||
};
|
||||
for unit in units {
|
||||
for declaration in &unit.declarations {
|
||||
let owner = declaration
|
||||
.header
|
||||
.identity
|
||||
.value
|
||||
.expect("collected stable identity");
|
||||
let mut resolver = Resolver {
|
||||
tables: &tables,
|
||||
output: &mut output,
|
||||
budget,
|
||||
file: unit.file,
|
||||
owner,
|
||||
};
|
||||
resolver.declaration(declaration).map_err(|error| Failure {
|
||||
file: Some(unit.file),
|
||||
error,
|
||||
})?;
|
||||
}
|
||||
for (file, declaration) in declarations {
|
||||
let owner = declaration
|
||||
.header
|
||||
.identity
|
||||
.value
|
||||
.expect("collected stable identity");
|
||||
let mut resolver = Resolver {
|
||||
tables,
|
||||
output: &mut output,
|
||||
budget,
|
||||
file,
|
||||
owner,
|
||||
};
|
||||
resolver.declaration(declaration).map_err(|error| Failure {
|
||||
file: Some(file),
|
||||
error,
|
||||
})?;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,556 @@
|
||||
//! Private syntax-only complete-bundle witness. No model admission or edit authority.
|
||||
//!
|
||||
//! One scan charges original bytes and raw tokens (including trivia). Typed
|
||||
//! syntax keeps its existing entry/reference/numeric charges. Ordinary syntax
|
||||
//! charges one entry per project/declaration/include/description, field, and
|
||||
//! value before allocation; array values already account for their list slots.
|
||||
//! These private storage ceilings are independent of caller model record counts.
|
||||
//! Ordinary references consume value storage, not ordered-model-step allowance.
|
||||
//! Typed reference occurrences retain the caller max_step_references ceiling.
|
||||
//! Only declaration graph/record counts are known here; semantic nested-model
|
||||
//! records and ordinary ordered steps still require the later complete admission.
|
||||
//! Metadata phase slots aggregate across all files: retained token-end indices,
|
||||
//! 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.
|
||||
|
||||
use super::{
|
||||
budget::{Budget, Limits},
|
||||
cursor::Cursor,
|
||||
declarations, lex,
|
||||
};
|
||||
use crate::Id;
|
||||
use crate::language::{
|
||||
self, Ast, EditDeclaration, Error, Lexeme, ParseLimits, Parser, SourceUnit, Span, TokenClass,
|
||||
};
|
||||
use crate::source_project::{ProjectLimits, SourceBundle, SourceDiagnostic, traversal::Traversal};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod carried;
|
||||
|
||||
struct ModelCounts {
|
||||
limits: ProjectLimits,
|
||||
graph: usize,
|
||||
records: usize,
|
||||
includes: usize,
|
||||
}
|
||||
|
||||
pub(super) struct ParsedMixedSyntax<'s> {
|
||||
bundle: &'s SourceBundle,
|
||||
// Preserve caller policy for the later semantic pass. Syntax storage uses
|
||||
// separate ceilings and cannot stand in for these graph/record/step limits.
|
||||
limits: ProjectLimits,
|
||||
files: Vec<FileSyntax<'s>>,
|
||||
budget: Budget,
|
||||
// Includes the conservative dropped value-end subtotal. The semantic
|
||||
// phase adds syntax entries to this once, not tokens/positions again.
|
||||
metadata: usize,
|
||||
// Kept through consumed resolution/indexing; fixes the live backing owner.
|
||||
carried: Option<carried::CarriedGuard<'s>>,
|
||||
}
|
||||
|
||||
struct FileSyntax<'s> {
|
||||
path: &'s str,
|
||||
source: &'s str,
|
||||
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.
|
||||
lexical_owners: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
// Only Parser borrows this state; the stored witness has no self-reference.
|
||||
pub(in crate::language) struct State<'a> {
|
||||
source: &'a str,
|
||||
tokens: &'a [lex::Token],
|
||||
budget: &'a mut Budget,
|
||||
model: &'a mut ModelCounts,
|
||||
typed: Vec<declarations::Declaration>,
|
||||
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 {
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset: error.span.start,
|
||||
message: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
fn lexeme(token: lex::Token) -> Lexeme {
|
||||
Lexeme {
|
||||
class: match token.class {
|
||||
lex::Class::Word => TokenClass::Identifier,
|
||||
lex::Class::Number => TokenClass::Number,
|
||||
lex::Class::String => TokenClass::String,
|
||||
lex::Class::Punctuation => TokenClass::Punctuation,
|
||||
},
|
||||
span: token.span,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
pub(in crate::language) fn entry(&mut self, at: Span) -> Result<(), Error> {
|
||||
self.budget.entry(at).map_err(ordinary_error)
|
||||
}
|
||||
pub(in crate::language) fn record(&mut self, graph: bool, at: Span) -> Result<(), Error> {
|
||||
let records = self.model.records.checked_add(1);
|
||||
let graph = self.model.graph.checked_add(usize::from(graph));
|
||||
let (Some(records), Some(graph)) = (records, graph) else {
|
||||
return Err(Error {
|
||||
code: "model_limit",
|
||||
offset: at.start,
|
||||
message: "Declaration count overflow.".into(),
|
||||
});
|
||||
};
|
||||
self.model
|
||||
.limits
|
||||
.model
|
||||
.check_counts(graph, records, 0)
|
||||
.map_err(|message| Error {
|
||||
code: "model_limit",
|
||||
offset: at.start,
|
||||
message,
|
||||
})?;
|
||||
self.model.records = records;
|
||||
self.model.graph = graph;
|
||||
Ok(())
|
||||
}
|
||||
pub(in crate::language) fn include(&mut self, at: Span) -> Result<(), Error> {
|
||||
if self.model.includes >= self.model.limits.max_include_edges {
|
||||
return Err(Error {
|
||||
code: "project_limit",
|
||||
offset: at.start,
|
||||
message: format!(
|
||||
"Include graph exceeds {} edges.",
|
||||
self.model.limits.max_include_edges
|
||||
),
|
||||
});
|
||||
}
|
||||
self.model.includes += 1;
|
||||
Ok(())
|
||||
}
|
||||
pub(in crate::language) fn ordinary_number(
|
||||
&self,
|
||||
at: &mut usize,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let first = self.tokens[*at];
|
||||
let mut end = first.span.end;
|
||||
let mut after = *at + 1;
|
||||
// Match the old greedy numeric alphabet only across byte-adjacent raw
|
||||
// tokens. This is consumption of the shared stream, not another scan.
|
||||
while let Some(next) = self.tokens.get(after) {
|
||||
if next.span.start != end
|
||||
|| !self.source[next.span.start..next.span.end]
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || b"_.+-".contains(&byte))
|
||||
{
|
||||
break;
|
||||
}
|
||||
end = next.span.end;
|
||||
after += 1;
|
||||
}
|
||||
let value: serde_json::Number = serde_json::from_str(&self.source[first.span.start..end])
|
||||
.map_err(|_| Error {
|
||||
code: "invalid_source",
|
||||
offset: first.span.start,
|
||||
message: "Expected a byte-adjacent finite JSON decimal number.".into(),
|
||||
})?;
|
||||
*at = after;
|
||||
Ok(serde_json::Value::Number(value))
|
||||
}
|
||||
pub(in crate::language) fn typed_header(&self, at: usize, root: bool) -> bool {
|
||||
let Some(token) = self.tokens.get(at) else {
|
||||
return false;
|
||||
};
|
||||
if declarations::declaration_kind(&self.source[token.span.start..token.span.end]).is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// A generic field named `unit` remains an ordinary field. A declaration
|
||||
// header has an alias and display-name token; only that shape is nested.
|
||||
root || (self
|
||||
.tokens
|
||||
.get(at + 1)
|
||||
.is_some_and(|t| t.class == lex::Class::Word)
|
||||
&& self
|
||||
.tokens
|
||||
.get(at + 2)
|
||||
.is_some_and(|t| t.class == lex::Class::String))
|
||||
}
|
||||
pub(in crate::language) fn parse_typed(
|
||||
&mut self,
|
||||
at: &mut usize,
|
||||
owner: Option<Id>,
|
||||
) -> Result<(), Error> {
|
||||
// Transfer the one active position into the typed consumer and back;
|
||||
// neither consumer scans, clones a stream, nor runs concurrently.
|
||||
self.record(true, self.tokens[*at].span)?;
|
||||
let mut cursor = Cursor::new(self.source, self.tokens, self.budget);
|
||||
cursor.cursor = *at;
|
||||
let result = declarations::parse(&mut cursor, 8, owner);
|
||||
*at = cursor.cursor;
|
||||
let declaration = result.map_err(ordinary_error)?;
|
||||
let missing = declaration.header.identity.value.is_none()
|
||||
|| matches!(&declaration.body, declarations::Body::Calculation { parameters, .. }
|
||||
if parameters.iter().any(|p| p.identity.value.is_none()));
|
||||
if missing {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset: declaration.span.start,
|
||||
message: "Private mixed syntax requires explicit IDs.".into(),
|
||||
});
|
||||
}
|
||||
self.typed.push(declaration);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::language) fn long_analysis_word(word: &str) -> bool {
|
||||
matches!(
|
||||
word,
|
||||
"oa_engineering_property"
|
||||
| "sa_engineering_property"
|
||||
| "oa_analysis_check"
|
||||
| "sa_analysis_check"
|
||||
| "oa_analysis_case"
|
||||
| "sa_analysis_case"
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn parse(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
let profile = 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> {
|
||||
vec![SourceDiagnostic {
|
||||
path: path.into(),
|
||||
diagnostic: language::diagnostic(source, error),
|
||||
related: Vec::new(),
|
||||
}]
|
||||
}
|
||||
|
||||
fn parse_with_profile(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
profile: Limits,
|
||||
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
|
||||
parse_impl(bundle, limits, profile, None, false, false)
|
||||
}
|
||||
|
||||
fn parse_impl<'s>(
|
||||
bundle: &'s SourceBundle,
|
||||
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));
|
||||
let maximum = Limits::source8(ParseLimits {
|
||||
max_source_bytes: limits.model.max_source_bytes,
|
||||
max_step_references: limits.model.max_step_references,
|
||||
..ParseLimits::UNLIMITED
|
||||
});
|
||||
if profile.bytes > maximum.bytes
|
||||
|| profile.tokens > maximum.tokens
|
||||
|| profile.entries > maximum.entries
|
||||
|| profile.references > maximum.references
|
||||
|| profile.frames > maximum.frames
|
||||
|| profile.numeric_units > maximum.numeric_units
|
||||
{
|
||||
return Err(at_entry(super::error(
|
||||
Span::default(),
|
||||
"Mixed test profile may only tighten inherited limits.",
|
||||
)));
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
let bytes = bundle
|
||||
.files
|
||||
.values()
|
||||
.try_fold(0usize, |n, s| n.checked_add(s.len()))
|
||||
.ok_or_else(|| at_entry(super::error(Span::default(), "Source size overflow.")))?;
|
||||
budget
|
||||
.index_bytes(bytes, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
let mut metadata = bundle.files.len();
|
||||
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
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
// Tighter private byte/file-slot limits precede folded-path table and
|
||||
// traversal allocations. Public Traversal's own validation order is intact.
|
||||
let mut traversal = Traversal::new(bundle, limits)?;
|
||||
let mut parsed = BTreeMap::new();
|
||||
let mut model = ModelCounts {
|
||||
limits,
|
||||
graph: 0,
|
||||
records: 0,
|
||||
includes: 0,
|
||||
};
|
||||
while let Some(path) = traversal.next()? {
|
||||
let (path, source) = bundle
|
||||
.files
|
||||
.get_key_value(&path)
|
||||
.expect("traversal checked source");
|
||||
let failure = |error| located(path, source, ordinary_error(error));
|
||||
let tokens = lex::tokens(source, &mut budget).map_err(failure)?;
|
||||
let metadata_before = budget.producer_entries();
|
||||
let mut parser = Parser {
|
||||
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(),
|
||||
pending_projection: 0,
|
||||
reserved_values: 0,
|
||||
reserved_owners: 0,
|
||||
}),
|
||||
};
|
||||
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);
|
||||
// Projected ordinary AST nodes/fields are bounded before constructing a
|
||||
// second position representation. Count references as slots, not syntax.
|
||||
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()))
|
||||
.and_then(|n| n.checked_add(projection_slots))
|
||||
.and_then(|n| n.checked_add(state.lexical_owners.len()))
|
||||
.ok_or_else(|| failure(super::error(Span::default(), "Metadata size overflow.")))?;
|
||||
if let Some(input) = &carried {
|
||||
input
|
||||
.check(state.budget, state.budget.usage.entries, metadata)
|
||||
.map_err(failure)?;
|
||||
} else {
|
||||
state
|
||||
.budget
|
||||
.lowering_entries(metadata, Span::default())
|
||||
.map_err(failure)?;
|
||||
}
|
||||
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 {
|
||||
path: path.clone(),
|
||||
diagnostic: *e,
|
||||
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)?;
|
||||
}
|
||||
traversal.finish_file(path.clone());
|
||||
let State {
|
||||
typed,
|
||||
lexical_owners,
|
||||
..
|
||||
} = state;
|
||||
parsed.insert(
|
||||
path.as_str(),
|
||||
FileSyntax {
|
||||
path,
|
||||
source,
|
||||
tokens,
|
||||
ordinary,
|
||||
typed,
|
||||
positions,
|
||||
lexical_owners,
|
||||
},
|
||||
);
|
||||
}
|
||||
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)
|
||||
.map_err(at_entry)?;
|
||||
}
|
||||
Ok(ParsedMixedSyntax {
|
||||
bundle,
|
||||
limits,
|
||||
files: parsed.into_values().collect(),
|
||||
budget,
|
||||
metadata,
|
||||
carried,
|
||||
})
|
||||
}
|
||||
|
||||
fn ordinary_position_slots(unit: &SourceUnit) -> usize {
|
||||
// Iterative accounting avoids adding a second recursive walk before the
|
||||
// already depth-bounded position projection. Each reference is one value.
|
||||
let mut slots = 0usize;
|
||||
let mut stack = Vec::new();
|
||||
for declaration in &unit.declarations {
|
||||
slots = slots.saturating_add(7); // declaration/body + at most five header roles
|
||||
slots = slots.saturating_add(declaration.fields.len());
|
||||
stack.extend(declaration.fields.values());
|
||||
}
|
||||
while let Some(value) = stack.pop() {
|
||||
slots = slots.saturating_add(1);
|
||||
match value {
|
||||
Ast::Array(items, _) => stack.extend(items),
|
||||
Ast::Object(fields, _) => {
|
||||
slots = slots.saturating_add(fields.len());
|
||||
stack.extend(fields.values());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
slots
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(super) mod project;
|
||||
@@ -0,0 +1,186 @@
|
||||
//! Fixture-constructible carried phase authority, not an emitted-output result.
|
||||
//! Checks existing logical phase totals; does not meter every parser allocation.
|
||||
use super::*;
|
||||
use project::accounting::{Ledger, Slots};
|
||||
|
||||
struct SnapshotPolicy {
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
}
|
||||
|
||||
/// The allocation-owning bundle guard. No production constructor exists yet.
|
||||
struct RetainedSnapshot {
|
||||
bundle: SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
retained: Slots,
|
||||
}
|
||||
enum SnapshotBacking<'s> {
|
||||
Borrowed {
|
||||
bundle: &'s SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
},
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
/// Neither Clone nor Copy: fixes the bundle, policy and retained allocation owner.
|
||||
pub(super) struct CarriedInput<'s> {
|
||||
backing: SnapshotBacking<'s>,
|
||||
}
|
||||
enum GuardBacking<'s> {
|
||||
Borrowed,
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
/// Policy has transferred into Budget/ParsedMixedSyntax. Only backing ownership
|
||||
/// remains here, avoiding a duplicate inline policy in every later witness.
|
||||
pub(super) struct CarriedGuard<'s> {
|
||||
backing: GuardBacking<'s>,
|
||||
}
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
|
||||
match self.backing {
|
||||
SnapshotBacking::Borrowed { bundle, policy } => (
|
||||
bundle,
|
||||
policy,
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Borrowed,
|
||||
},
|
||||
),
|
||||
SnapshotBacking::Retained(owner) => (
|
||||
&owner.bundle,
|
||||
SnapshotPolicy {
|
||||
limits: owner.policy.limits,
|
||||
project_limits: owner.policy.project_limits,
|
||||
},
|
||||
CarriedGuard {
|
||||
backing: GuardBacking::Retained(owner),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CarriedGuard<'_> {
|
||||
pub(super) fn slots(&self) -> Slots {
|
||||
match &self.backing {
|
||||
GuardBacking::Borrowed => Slots::default(),
|
||||
GuardBacking::Retained(owner) => owner.retained,
|
||||
}
|
||||
}
|
||||
pub(super) fn check(
|
||||
&self,
|
||||
budget: &Budget,
|
||||
syntax: usize,
|
||||
metadata: usize,
|
||||
) -> Result<(), super::super::Error> {
|
||||
let at = Span::default();
|
||||
let total = self
|
||||
.slots()
|
||||
.add(Slots::entries(syntax), at)?
|
||||
.add(Slots::entries(metadata), at)?;
|
||||
// A local check, not another reservation retained in the syntax owner.
|
||||
Ledger::default().reserve(budget, total, at)
|
||||
}
|
||||
}
|
||||
|
||||
/// Input already binds the actual bundle; callers cannot supply a replacement.
|
||||
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),
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
/// Explicit retained text and entry producer fixture route.
|
||||
#[cfg(test)]
|
||||
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)]
|
||||
impl SnapshotPolicy {
|
||||
fn captured(syntax: &ParsedMixedSyntax<'_>) -> Self {
|
||||
Self {
|
||||
limits: syntax.budget.limits(),
|
||||
project_limits: syntax.limits,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl RetainedSnapshot {
|
||||
fn fixture(bundle: SourceBundle, policy: SnapshotPolicy) -> Result<Self, super::super::Error> {
|
||||
let at = Span::default();
|
||||
let mut text = bundle.entry.len();
|
||||
for (path, source) in &bundle.files {
|
||||
text = Slots::text(text)
|
||||
.add(Slots::text(path.len()), at)?
|
||||
.add(Slots::text(source.len()), at)?
|
||||
.text;
|
||||
}
|
||||
let retained = Slots {
|
||||
entries: bundle.files.len(),
|
||||
occurrences: 0,
|
||||
text,
|
||||
};
|
||||
Ok(Self {
|
||||
bundle,
|
||||
policy,
|
||||
retained,
|
||||
})
|
||||
}
|
||||
fn input(&self) -> CarriedInput<'_> {
|
||||
CarriedInput {
|
||||
backing: SnapshotBacking::Retained(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl<'s> CarriedInput<'s> {
|
||||
fn borrowed(bundle: &'s SourceBundle, syntax: &ParsedMixedSyntax<'_>) -> Self {
|
||||
Self {
|
||||
backing: SnapshotBacking::Borrowed {
|
||||
bundle,
|
||||
policy: SnapshotPolicy::captured(syntax),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod text_producer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod entry_producer;
|
||||
@@ -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,729 @@
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Usage;
|
||||
fn doc(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
|
||||
)
|
||||
}
|
||||
fn empty() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), doc(""))]),
|
||||
}
|
||||
}
|
||||
fn chain() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), doc("include \"part.arcadia\";")),
|
||||
(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
),
|
||||
("last.arcadia".into(), "//é\r\n".into()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
fn profile(entries: usize, bytes: usize) -> Limits {
|
||||
Limits {
|
||||
entries,
|
||||
bytes,
|
||||
..Limits::source8(ParseLimits::UNLIMITED)
|
||||
}
|
||||
}
|
||||
// Fixture-only policies may deliberately be too tight to admit the target.
|
||||
// These are validated profiles, not a production old-owner transition.
|
||||
fn fixture_policy(limits: Limits, project_limits: ProjectLimits) -> SnapshotPolicy {
|
||||
SnapshotPolicy {
|
||||
limits: Budget::new(limits).unwrap().limits(),
|
||||
project_limits,
|
||||
}
|
||||
}
|
||||
fn owner(bundle: SourceBundle, limits: Limits) -> RetainedSnapshot {
|
||||
RetainedSnapshot::fixture(bundle, fixture_policy(limits, ProjectLimits::default())).unwrap()
|
||||
}
|
||||
fn parse_failure(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 resolve_failure(syntax: ParsedMixedSyntax<'_>) -> SourceDiagnostic {
|
||||
let failure = match project::resolve(syntax) {
|
||||
Ok(_) => panic!("unexpected resolution success"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
failure.diagnostics.into_iter().next().unwrap()
|
||||
}
|
||||
fn diagnostic(d: &SourceDiagnostic, path: &str, start: usize, end: usize, message: &str) {
|
||||
assert_eq!(d.path, path);
|
||||
assert_eq!(d.diagnostic.code, "invalid_source");
|
||||
assert_eq!(d.diagnostic.span, Span { start, end });
|
||||
assert_eq!(d.diagnostic.message, message);
|
||||
assert!(d.related.is_empty());
|
||||
}
|
||||
fn expected_project() -> crate::Project {
|
||||
crate::Project {
|
||||
schema_version: 8,
|
||||
id: Id::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||
alias: "p".into(),
|
||||
name: "Project".into(),
|
||||
description: String::new(),
|
||||
definitions: BTreeMap::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn real_retained_bundles_keep_exact_guard_work_and_once_only_resolved_baseline() {
|
||||
for (bundle, c, u, m, live, entries, bytes) in [
|
||||
(
|
||||
empty(),
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text: 102,
|
||||
},
|
||||
Usage {
|
||||
bytes: 78,
|
||||
tokens: 19,
|
||||
entries: 1,
|
||||
references: 0,
|
||||
deepest_frame: 0,
|
||||
},
|
||||
11,
|
||||
Slots {
|
||||
entries: 13,
|
||||
occurrences: 0,
|
||||
text: 110,
|
||||
},
|
||||
17,
|
||||
112,
|
||||
),
|
||||
(
|
||||
chain(),
|
||||
Slots {
|
||||
entries: 3,
|
||||
occurrences: 0,
|
||||
text: 180,
|
||||
},
|
||||
Usage {
|
||||
bytes: 132,
|
||||
tokens: 31,
|
||||
entries: 3,
|
||||
references: 0,
|
||||
deepest_frame: 0,
|
||||
},
|
||||
19,
|
||||
Slots {
|
||||
entries: 25,
|
||||
occurrences: 0,
|
||||
text: 212,
|
||||
},
|
||||
29,
|
||||
214,
|
||||
),
|
||||
] {
|
||||
let owner = owner(bundle, profile(entries, bytes));
|
||||
assert_eq!(owner.retained, c);
|
||||
let original = owner.bundle.clone();
|
||||
let parsed = parse_carried(owner.input()).unwrap();
|
||||
assert_eq!(parsed.metadata, m);
|
||||
assert_eq!(parsed.budget.usage, u);
|
||||
assert_eq!(parsed.budget.numeric_units(), 0);
|
||||
assert!(std::ptr::eq(parsed.bundle, &owner.bundle));
|
||||
assert!(
|
||||
matches!(&parsed.carried.as_ref().unwrap().backing,GuardBacking::Retained(retained) if std::ptr::eq(*retained,&owner))
|
||||
);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
assert_eq!(resolved.project(), &expected_project());
|
||||
assert!(resolved.warnings().is_empty());
|
||||
let (syntax, stored) = resolved.phase_state();
|
||||
assert_eq!(stored, live);
|
||||
assert_eq!(syntax.budget.usage, u);
|
||||
assert_eq!(syntax.carried.as_ref().unwrap().slots(), c);
|
||||
assert_eq!(
|
||||
syntax.files.iter().map(|f| f.path).collect::<Vec<_>>(),
|
||||
original
|
||||
.files
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
drop(resolved);
|
||||
assert_eq!(owner.bundle, original);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_combined_syntax_caps_and_canonical_failure_files() {
|
||||
for (bundle, cap, path) in [(empty(), 13, "main.arcadia"), (chain(), 25, "part.arcadia")] {
|
||||
let pass = owner(bundle.clone(), profile(cap, 1024));
|
||||
drop(parse_carried(pass.input()).unwrap());
|
||||
let fail = owner(bundle, profile(cap - 1, 1024));
|
||||
let d = parse_failure(parse_carried(fail.input()));
|
||||
diagnostic(&d, path, 0, 1, "Source8 lowering entry limit exceeded.");
|
||||
assert_eq!((d.diagnostic.line, d.diagnostic.column), (1, 1));
|
||||
}
|
||||
let owner = owner(chain(), profile(20, 1024));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(owner.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn legacy_zero_timing_is_preserved_but_explicit_zero_carry_still_combines() {
|
||||
let bundle = empty();
|
||||
let original =
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), profile(11, 1024)).unwrap();
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(CarriedInput::borrowed(&bundle, &original))),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
diagnostic(
|
||||
&resolve_failure(original),
|
||||
"main.arcadia",
|
||||
0,
|
||||
0,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
let original =
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), profile(12, 1024)).unwrap();
|
||||
let carried = parse_carried(CarriedInput::borrowed(&bundle, &original)).unwrap();
|
||||
assert_eq!(carried.budget.usage, original.budget.usage);
|
||||
assert_eq!(carried.metadata, 11);
|
||||
}
|
||||
#[test]
|
||||
fn resolver_entry_and_catalog_text_caps_include_c_once_after_parser_success() {
|
||||
for (bundle, entries, bytes) in [(empty(), 17, 112), (chain(), 29, 214)] {
|
||||
for limits in [profile(entries - 1, bytes), profile(entries, bytes - 1)] {
|
||||
let held = owner(bundle.clone(), limits);
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
"main.arcadia",
|
||||
11,
|
||||
73,
|
||||
if limits.entries == entries - 1 {
|
||||
"Source8 lowering entry limit exceeded."
|
||||
} else {
|
||||
"Source8 index/output byte limit exceeded."
|
||||
},
|
||||
);
|
||||
drop(
|
||||
project::resolve(
|
||||
parse_with_profile(&bundle, ProjectLimits::default(), limits).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn syntax_text_accounting_is_after_decoding_and_has_precise_original_location() {
|
||||
for (bundle, bytes, path, start, end) in [
|
||||
(empty(), 109, "main.arcadia", 11, 73),
|
||||
(chain(), 211, "part.arcadia", 0, 23),
|
||||
] {
|
||||
let held = owner(bundle, profile(100, bytes));
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
assert_eq!(
|
||||
parsed
|
||||
.files
|
||||
.iter()
|
||||
.find_map(|f| f.ordinary.header.as_ref())
|
||||
.unwrap()
|
||||
.project
|
||||
.name,
|
||||
"Project"
|
||||
);
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
path,
|
||||
start,
|
||||
end,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn initial_c_and_file_slots_are_checked_before_traversal() {
|
||||
for (bundle, cap) in [(empty(), 1), (chain(), 5)] {
|
||||
let held = owner(bundle, profile(cap, 1024));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
);
|
||||
}
|
||||
for (cap, code) in [(1, "invalid_source"), (2, "project_limit")] {
|
||||
let held = RetainedSnapshot::fixture(
|
||||
empty(),
|
||||
fixture_policy(
|
||||
profile(cap, 1024),
|
||||
ProjectLimits {
|
||||
max_files: 0,
|
||||
..ProjectLimits::default()
|
||||
},
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parse_failure(parse_carried(held.input())).diagnostic.code,
|
||||
code
|
||||
);
|
||||
}
|
||||
let held = owner(empty(), profile(100, 101));
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
let held = owner(empty(), profile(100, 102));
|
||||
drop(parse_carried(held.input()).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn explicit_fault_injections_cover_occurrence_caps_and_unreachable_overflows() {
|
||||
let mut held = owner(empty(), profile(100, 1024));
|
||||
let maximum = held.policy.limits.entries + held.policy.limits.references;
|
||||
// Not real bundle ownership: only a fault-injected dimension check.
|
||||
held.retained.occurrences = maximum;
|
||||
drop(parse_carried(held.input()).unwrap());
|
||||
held.retained.occurrences = maximum + 1;
|
||||
diagnostic(
|
||||
&parse_failure(parse_carried(held.input())),
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 lowering occurrence limit exceeded.",
|
||||
);
|
||||
held.retained = Slots {
|
||||
entries: usize::MAX,
|
||||
occurrences: 0,
|
||||
text: 0,
|
||||
};
|
||||
let budget = Budget::new(profile(100, 1024)).unwrap();
|
||||
// Valid hard limits reject C first: direct injection reaches arithmetic.
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.into_parts()
|
||||
.2
|
||||
.check(&budget, 0, 1)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
held.retained = Slots::default();
|
||||
assert_eq!(
|
||||
held.input()
|
||||
.into_parts()
|
||||
.2
|
||||
.check(&budget, usize::MAX, 1)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
assert_eq!(
|
||||
Slots::text(usize::MAX)
|
||||
.add(Slots::text(1), Span::default())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
let mut ledger = Ledger {
|
||||
live: Slots::entries(usize::MAX),
|
||||
};
|
||||
assert_eq!(
|
||||
ledger
|
||||
.reserve(&budget, Slots::entries(1), Span::default())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Mixed auxiliary storage size overflow."
|
||||
);
|
||||
}
|
||||
|
||||
fn same_policy(syntax: &ParsedMixedSyntax<'_>, policy: &SnapshotPolicy) {
|
||||
let a = syntax.budget.limits();
|
||||
let b = policy.limits;
|
||||
assert_eq!(
|
||||
[a.bytes, a.tokens, a.entries, a.references, a.frames],
|
||||
[b.bytes, b.tokens, b.entries, b.references, b.frames]
|
||||
);
|
||||
assert_eq!(a.numeric_units, b.numeric_units);
|
||||
let a = syntax.limits;
|
||||
let b = policy.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
|
||||
],
|
||||
[
|
||||
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
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn exact_policy_capture_preserves_every_field_and_starts_fresh_snapshot_work() {
|
||||
let base = empty();
|
||||
let next = chain();
|
||||
let limits = Limits {
|
||||
bytes: 4096,
|
||||
tokens: 2048,
|
||||
entries: 1024,
|
||||
references: 128,
|
||||
frames: 64,
|
||||
numeric_units: 4096,
|
||||
};
|
||||
let caller = ProjectLimits {
|
||||
max_files: 7,
|
||||
max_include_depth: 5,
|
||||
max_include_edges: 9,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 8192,
|
||||
max_graph_objects: 10,
|
||||
max_records: 30,
|
||||
max_step_references: 256,
|
||||
},
|
||||
};
|
||||
let seed = parse_with_profile(&base, caller, limits).unwrap();
|
||||
assert_eq!(seed.budget.usage.bytes, 78);
|
||||
let input = CarriedInput::borrowed(&next, &seed);
|
||||
let captured = SnapshotPolicy::captured(&seed);
|
||||
drop(seed);
|
||||
let parsed = parse_carried(input).unwrap();
|
||||
same_policy(&parsed, &captured);
|
||||
assert_eq!(
|
||||
parsed.budget.usage,
|
||||
Usage {
|
||||
bytes: 132,
|
||||
tokens: 31,
|
||||
entries: 3,
|
||||
references: 0,
|
||||
deepest_frame: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(parsed.budget.numeric_units(), 0);
|
||||
let resolved = project::resolve(parsed).unwrap();
|
||||
same_policy(resolved.phase_state().0, &captured);
|
||||
}
|
||||
#[test]
|
||||
fn inherited_byte_token_and_traversal_limits_reach_the_real_parser() {
|
||||
let seed_bundle = empty();
|
||||
let bundle = chain();
|
||||
for (bytes, tokens, success) in [(132, 31, true), (131, 31, false), (132, 30, false)] {
|
||||
let limits = Limits {
|
||||
tokens,
|
||||
..profile(100, bytes)
|
||||
};
|
||||
let seed = parse_with_profile(&seed_bundle, ProjectLimits::default(), limits).unwrap();
|
||||
let result = parse_carried(CarriedInput::borrowed(&bundle, &seed));
|
||||
if success {
|
||||
drop(project::resolve(result.unwrap()).unwrap());
|
||||
} else {
|
||||
let d = parse_failure(result);
|
||||
if bytes == 131 {
|
||||
diagnostic(
|
||||
&d,
|
||||
"main.arcadia",
|
||||
0,
|
||||
1,
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
} else {
|
||||
diagnostic(&d, "last.arcadia", 5, 6, "Source8 tokens limit exceeded.");
|
||||
assert_eq!((d.diagnostic.line, d.diagnostic.column), (1, 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (files, depth, edges, success) in [
|
||||
(3, 2, 2, true),
|
||||
(2, 2, 2, false),
|
||||
(3, 1, 2, false),
|
||||
(3, 2, 1, false),
|
||||
] {
|
||||
let caller = ProjectLimits {
|
||||
max_files: files,
|
||||
max_include_depth: depth,
|
||||
max_include_edges: edges,
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let seed = parse_with_profile(&seed_bundle, caller, profile(100, 1024)).unwrap();
|
||||
let result = parse_carried(CarriedInput::borrowed(&bundle, &seed));
|
||||
if success {
|
||||
drop(project::resolve(result.unwrap()).unwrap());
|
||||
} else {
|
||||
let d = parse_failure(result);
|
||||
assert_eq!(d.diagnostic.code, "project_limit");
|
||||
if files == 2 {
|
||||
assert_eq!(d.path, "main.arcadia");
|
||||
} else {
|
||||
assert_eq!(d.path, "part.arcadia");
|
||||
assert_eq!(
|
||||
d.diagnostic.span,
|
||||
Span {
|
||||
start: 0,
|
||||
end: if edges == 1 { 1 } else { 23 }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn typed(expression: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
doc(&format!(
|
||||
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000002\" {{ input x id \"00000000-0000-0000-0000-000000000003\" : Number; result Number; body {expression}; }}"
|
||||
)),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn captured_numeric_reference_frame_and_model_policies_are_behavioral_not_just_copied() {
|
||||
let base = empty();
|
||||
for (target, limits, expected) in [
|
||||
(
|
||||
typed("1"),
|
||||
Limits {
|
||||
numeric_units: 72,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"NumericUnits",
|
||||
),
|
||||
(
|
||||
typed("$x"),
|
||||
Limits {
|
||||
references: 0,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"Source8 references limit exceeded.",
|
||||
),
|
||||
(
|
||||
typed("abs(1)"),
|
||||
Limits {
|
||||
frames: 0,
|
||||
..profile(1000, 4096)
|
||||
},
|
||||
"Source8 syntax frames limit exceeded.",
|
||||
),
|
||||
] {
|
||||
let seed = parse_with_profile(&base, ProjectLimits::default(), limits).unwrap();
|
||||
let d = parse_failure(parse_carried(CarriedInput::borrowed(&target, &seed)));
|
||||
assert!(d.diagnostic.message.contains(expected), "{:?}", d);
|
||||
}
|
||||
for expression in ["1", "$x", "abs(1)"] {
|
||||
let target = typed(expression);
|
||||
let seed =
|
||||
parse_with_profile(&base, ProjectLimits::default(), profile(1000, 4096)).unwrap();
|
||||
drop(
|
||||
project::resolve(parse_carried(CarriedInput::borrowed(&target, &seed)).unwrap())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
// Nonzero old numeric work must not leak into the second snapshot.
|
||||
let one = typed("1");
|
||||
let limits = Limits {
|
||||
numeric_units: 73,
|
||||
..profile(1000, 4096)
|
||||
};
|
||||
let seed = parse_with_profile(&one, ProjectLimits::default(), limits).unwrap();
|
||||
assert_eq!(seed.budget.numeric_units(), 73);
|
||||
let parsed = parse_carried(CarriedInput::borrowed(&one, &seed)).unwrap();
|
||||
assert_eq!(parsed.budget.numeric_units(), 73);
|
||||
assert_eq!(parsed.budget.usage, seed.budget.usage);
|
||||
for (graph, records, steps) in [(0, 10, 10), (10, 1, 10), (10, 10, 0)] {
|
||||
let caller = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_graph_objects: graph,
|
||||
max_records: records,
|
||||
max_step_references: steps,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
let seed = parse_with_profile(
|
||||
&base,
|
||||
caller,
|
||||
Limits::source8(ParseLimits {
|
||||
max_step_references: steps,
|
||||
..ParseLimits::UNLIMITED
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let target = if steps == 0 { typed("$x") } else { typed("1") };
|
||||
let errors = parse_carried(CarriedInput::borrowed(&target, &seed));
|
||||
if steps == 0 {
|
||||
assert!(
|
||||
parse_failure(errors)
|
||||
.diagnostic
|
||||
.message
|
||||
.contains("references")
|
||||
);
|
||||
} else {
|
||||
assert!(parse_failure(errors).diagnostic.message.contains("limit"));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn zero_carried_mixed_oa_sa_unicode_results_and_index_ownership_match_legacy() {
|
||||
for (activity, property, layer) in [
|
||||
("oa_activity", "oa_property", "Oa"),
|
||||
("sa_function", "sa_property", "Sa"),
|
||||
] {
|
||||
let body = format!(
|
||||
"node n id \"00000000-0000-0000-0000-000000000004\" {{ diagram @d; element @v; }}\n{property} v \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @w; type Number; base 1; }}\n{activity} w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram d \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ layer {layer}; }}"
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
format!("// 雨\r\n{}", doc(&body).replace('\n', "\r\n")),
|
||||
)]),
|
||||
};
|
||||
let legacy = parse(&bundle, ProjectLimits::default()).unwrap();
|
||||
let carried = parse_carried(CarriedInput::borrowed(&bundle, &legacy)).unwrap();
|
||||
assert_eq!(legacy.budget.usage, carried.budget.usage);
|
||||
assert_eq!(
|
||||
legacy.budget.numeric_units(),
|
||||
carried.budget.numeric_units()
|
||||
);
|
||||
assert_eq!(legacy.metadata, carried.metadata);
|
||||
let legacy = project::resolve(legacy).unwrap();
|
||||
let carried = project::resolve(carried).unwrap();
|
||||
assert_eq!(legacy.project(), carried.project());
|
||||
assert_eq!(legacy.warnings(), carried.warnings());
|
||||
assert_eq!(legacy.phase_state().1, carried.phase_state().1);
|
||||
assert!(carried.phase_state().0.carried.is_some());
|
||||
let legacy = legacy.index().unwrap().index_ordinary().unwrap();
|
||||
let carried = carried.index().unwrap().index_ordinary().unwrap();
|
||||
assert_eq!(legacy.names(), carried.names());
|
||||
assert_eq!(legacy.typed_occurrences(), carried.typed_occurrences());
|
||||
assert_eq!(
|
||||
legacy.ordinary_occurrences(),
|
||||
carried.ordinary_occurrences()
|
||||
);
|
||||
assert!(std::ptr::eq(carried.bundle(), &bundle));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_position_key_copy_is_counted_at_existing_postparse_text_phase() {
|
||||
let source = doc(
|
||||
"oa_activity a \"A\" id \"00000000-0000-0000-0000-000000000002\" { description \"é\"; }",
|
||||
);
|
||||
let start = source.find("description \"é\";").unwrap();
|
||||
let end = start + "description \"é\";".len();
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), source)]),
|
||||
};
|
||||
let retained_text = bundle.files["main.arcadia"].len() + 24;
|
||||
// Existing text metric: project8 + alias/name2 + field-span key11 +
|
||||
// AST field key11 + decoded string2 + projected field key11 =45.
|
||||
// The final11-byte key is copied before resolve accounts this phase.
|
||||
let held = owner(bundle, profile(1000, retained_text + 44));
|
||||
let parsed = parse_carried(held.input()).unwrap();
|
||||
assert_eq!(parsed.files[0].positions.len(), 1);
|
||||
diagnostic(
|
||||
&resolve_failure(parsed),
|
||||
"main.arcadia",
|
||||
start,
|
||||
end,
|
||||
"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(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,760 @@
|
||||
//! Private complete-Project admission from the actual complete syntax witness.
|
||||
//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy.
|
||||
use super::super::{
|
||||
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
|
||||
declarations as d, error, lower,
|
||||
};
|
||||
use super::{FileSyntax, ParsedMixedSyntax};
|
||||
use crate::language::{Ast, DeclKind};
|
||||
use crate::source_project::{SourceDiagnostic, SourceLocation};
|
||||
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)
|
||||
}
|
||||
pub(in crate::language::analysis_source) mod ordinary_index;
|
||||
|
||||
/// Read-only authority: every component originates in the consumed syntax.
|
||||
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
|
||||
syntax: ParsedMixedSyntax<'s>,
|
||||
project: Project,
|
||||
warnings: Vec<SourceDiagnostic>,
|
||||
static_limits: analysis::AdmissionLimits,
|
||||
static_usage: analysis::AdmissionUsage,
|
||||
expressions: Vec<lower::ExpressionSite>,
|
||||
occurrences: Vec<lower::Occurrence>,
|
||||
storage: Ledger,
|
||||
}
|
||||
impl ResolvedMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn phase_state(&self) -> (&ParsedMixedSyntax<'_>, Slots) {
|
||||
(&self.syntax, self.storage.live)
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
&self.warnings
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> {
|
||||
resolved: ResolvedMixedProject<'s>,
|
||||
data: super::super::index::IndexData,
|
||||
}
|
||||
impl<'s> ResolvedMixedProject<'s> {
|
||||
pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> {
|
||||
let input = IndexInput {
|
||||
files: &self.syntax.files,
|
||||
project: &self.project,
|
||||
expressions: &self.expressions,
|
||||
occurrences: &self.occurrences,
|
||||
};
|
||||
let data =
|
||||
super::super::index::mixed_data(input, &mut self.syntax.budget, &mut self.storage)
|
||||
.map_err(|failure| Failure::index(&self.syntax, failure))?;
|
||||
Ok(IndexedMixedProject {
|
||||
resolved: self,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl IndexedMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
self.resolved.project()
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
self.resolved.warnings()
|
||||
}
|
||||
pub fn bundle(&self) -> &crate::source_project::SourceBundle {
|
||||
self.resolved.syntax.bundle
|
||||
}
|
||||
/// Canonical file key, canonical path, and the complete original bytes.
|
||||
pub fn files(&self) -> impl Iterator<Item = (usize, &str, &str)> {
|
||||
self.resolved
|
||||
.syntax
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(key, f)| (key, f.path, f.source))
|
||||
}
|
||||
pub fn names(&self) -> &BTreeMap<lower::Target, String> {
|
||||
&self.data.names
|
||||
}
|
||||
pub fn occurrences(
|
||||
&self,
|
||||
) -> &BTreeMap<super::super::index::OccurrenceKey, super::super::index::Location> {
|
||||
&self.data.occurrences
|
||||
}
|
||||
pub fn expressions(
|
||||
&self,
|
||||
) -> &BTreeMap<super::super::index::ExpressionKey, super::super::index::Location> {
|
||||
&self.data.expressions
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor-only borrowed adapter. Its private file table prevents fabrication;
|
||||
/// no API returns it and no alternative source/token table can be installed.
|
||||
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],
|
||||
}
|
||||
impl IndexInput<'_> {
|
||||
pub(in crate::language::analysis_source) fn declarations(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (usize, &d::Declaration)> + Clone {
|
||||
self.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(file, f)| f.typed.iter().map(move |d| (file, d)))
|
||||
}
|
||||
pub(in crate::language::analysis_source) fn source(&self, file: usize) -> Option<&str> {
|
||||
self.files.get(file).map(|f| f.source)
|
||||
}
|
||||
pub(in crate::language::analysis_source) fn tokens(
|
||||
&self,
|
||||
file: usize,
|
||||
) -> Option<&[super::super::lex::Token]> {
|
||||
self.files.get(file).map(|f| f.tokens.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(in crate::language::analysis_source) struct Failure {
|
||||
pub diagnostics: Vec<SourceDiagnostic>,
|
||||
}
|
||||
impl Failure {
|
||||
/// Canonical attribution when justified; otherwise a safe explicit start fallback.
|
||||
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| {
|
||||
syntax.files[file]
|
||||
.source
|
||||
.get(failure.error.span.start..failure.error.span.end)
|
||||
.is_none()
|
||||
}) {
|
||||
failure.error.span = Span::default();
|
||||
}
|
||||
Self::typed(syntax, failure.file, failure.error)
|
||||
}
|
||||
|
||||
// Rejection transport is not retained in a successful witness. In particular
|
||||
// a storage refusal needs a small diagnostic even when its allowance is zero.
|
||||
fn typed(syntax: &ParsedMixedSyntax<'_>, file: Option<usize>, e: TypedError) -> Self {
|
||||
let mut failure = Self::ordinary(
|
||||
syntax,
|
||||
file,
|
||||
language::Error {
|
||||
code: "invalid_source",
|
||||
offset: e.span.start,
|
||||
message: e.message,
|
||||
},
|
||||
);
|
||||
failure.diagnostics[0].diagnostic.span = e.span;
|
||||
failure
|
||||
}
|
||||
fn ordinary(syntax: &ParsedMixedSyntax<'_>, file: Option<usize>, e: language::Error) -> Self {
|
||||
let file = file.unwrap_or_else(|| entry(syntax));
|
||||
let unit = &syntax.files[file];
|
||||
Self {
|
||||
diagnostics: super::located(unit.path, unit.source, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum OwnerKind {
|
||||
Project,
|
||||
Ordinary(usize),
|
||||
Typed(usize),
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct Owner {
|
||||
file: usize,
|
||||
kind: OwnerKind,
|
||||
span: Span,
|
||||
}
|
||||
struct Catalog {
|
||||
tables: lower::Tables,
|
||||
aliases: BTreeMap<String, Id>,
|
||||
owners: BTreeMap<Id, Owner>,
|
||||
tables_storage: Slots,
|
||||
aliases_storage: Slots,
|
||||
owners_storage: Slots,
|
||||
}
|
||||
fn entry(syntax: &ParsedMixedSyntax<'_>) -> usize {
|
||||
syntax
|
||||
.files
|
||||
.iter()
|
||||
.position(|f| f.path == syntax.bundle.entry)
|
||||
.expect("complete entry")
|
||||
}
|
||||
fn category(kind: DeclKind) -> lower::Category {
|
||||
match kind {
|
||||
DeclKind::Element(kind, layer) => lower::Category::Element(kind, layer),
|
||||
DeclKind::Relation(_) => lower::Category::Relation,
|
||||
DeclKind::Diagram => lower::Category::Diagram,
|
||||
DeclKind::Node => lower::Category::Node,
|
||||
DeclKind::Artifact => lower::Category::Artifact,
|
||||
DeclKind::Trace => lower::Category::Trace,
|
||||
}
|
||||
}
|
||||
impl Catalog {
|
||||
fn insert(
|
||||
&mut self,
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
ledger: &mut Ledger,
|
||||
id: Id,
|
||||
alias: &str,
|
||||
owner: Owner,
|
||||
) -> Result<(), Failure> {
|
||||
// Alias first, even when both aliases and IDs collide. Owner is an
|
||||
// occurrence position, never a lookup through the rejected UUID.
|
||||
let prior = self
|
||||
.aliases
|
||||
.get(alias)
|
||||
.map(|id| ("alias", self.owners[id]))
|
||||
.or_else(|| self.owners.get(&id).map(|o| ("identity", *o)));
|
||||
if let Some((domain, prior)) = prior {
|
||||
let mut failure = Failure::typed(
|
||||
syntax,
|
||||
Some(owner.file),
|
||||
error(owner.span, format!("Duplicate global {domain}.")),
|
||||
);
|
||||
failure.diagnostics[0].related.push(SourceLocation {
|
||||
path: syntax.files[prior.file].path.into(),
|
||||
span: prior.span,
|
||||
});
|
||||
return Err(failure);
|
||||
}
|
||||
let alias_slots = Slots {
|
||||
entries: 1,
|
||||
text: alias.len(),
|
||||
..Slots::default()
|
||||
};
|
||||
ledger
|
||||
.reserve(
|
||||
&syntax.budget,
|
||||
alias_slots
|
||||
.add(Slots::entries(1), owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?,
|
||||
owner.span,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.aliases_storage = self
|
||||
.aliases_storage
|
||||
.add(alias_slots, owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.owners_storage = self
|
||||
.owners_storage
|
||||
.add(Slots::entries(1), owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.aliases.insert(alias.into(), id);
|
||||
self.owners.insert(id, owner);
|
||||
Ok(())
|
||||
}
|
||||
fn reserve_tables(
|
||||
&mut self,
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
ledger: &mut Ledger,
|
||||
slots: Slots,
|
||||
owner: Owner,
|
||||
) -> Result<(), Failure> {
|
||||
ledger
|
||||
.reserve(&syntax.budget, slots, owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
self.tables_storage = self
|
||||
.tables_storage
|
||||
.add(slots, owner.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(owner.file), e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn catalog(syntax: &ParsedMixedSyntax<'_>, ledger: &mut Ledger) -> Result<Catalog, Failure> {
|
||||
let mut catalog = Catalog {
|
||||
tables: lower::Tables::default(),
|
||||
aliases: BTreeMap::new(),
|
||||
owners: BTreeMap::new(),
|
||||
tables_storage: Slots::default(),
|
||||
aliases_storage: Slots::default(),
|
||||
owners_storage: Slots::default(),
|
||||
};
|
||||
let file = entry(syntax);
|
||||
let header = syntax.files[file]
|
||||
.ordinary
|
||||
.header
|
||||
.as_ref()
|
||||
.expect("entry header");
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Project,
|
||||
span: header.span,
|
||||
};
|
||||
catalog.insert(
|
||||
syntax,
|
||||
ledger,
|
||||
header.project.id,
|
||||
&header.project.alias,
|
||||
owner,
|
||||
)?;
|
||||
catalog.reserve_tables(
|
||||
syntax,
|
||||
ledger,
|
||||
Slots {
|
||||
entries: 2,
|
||||
text: header.project.alias.len(),
|
||||
..Slots::default()
|
||||
},
|
||||
owner,
|
||||
)?;
|
||||
catalog
|
||||
.tables
|
||||
.insert(
|
||||
header.project.id,
|
||||
&header.project.alias,
|
||||
lower::Category::Project,
|
||||
header.span,
|
||||
&syntax.budget,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
for (file, unit) in syntax.files.iter().enumerate() {
|
||||
let mut ordinary = unit.ordinary.declarations.iter().enumerate().peekable();
|
||||
let mut typed = unit.typed.iter().enumerate().peekable();
|
||||
while ordinary.peek().is_some() || typed.peek().is_some() {
|
||||
let use_ordinary = match (ordinary.peek(), typed.peek()) {
|
||||
(Some((_, a)), Some((_, b))) => a.span.start < b.span.start,
|
||||
(Some(_), None) => true,
|
||||
_ => false,
|
||||
};
|
||||
if use_ordinary {
|
||||
let (index, d) = ordinary.next().expect("ordinary head");
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Ordinary(index),
|
||||
span: d.span,
|
||||
};
|
||||
catalog.insert(syntax, ledger, d.id, &d.alias, owner)?;
|
||||
catalog.reserve_tables(
|
||||
syntax,
|
||||
ledger,
|
||||
Slots {
|
||||
entries: 2,
|
||||
text: d.alias.len(),
|
||||
..Slots::default()
|
||||
},
|
||||
owner,
|
||||
)?;
|
||||
catalog
|
||||
.tables
|
||||
.insert(d.id, &d.alias, category(d.kind), d.span, &syntax.budget)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
} else {
|
||||
let (index, d) = typed.next().expect("typed head");
|
||||
let owner = Owner {
|
||||
file,
|
||||
kind: OwnerKind::Typed(index),
|
||||
span: d.span,
|
||||
};
|
||||
catalog.insert(
|
||||
syntax,
|
||||
ledger,
|
||||
d.header.identity.value.expect("syntax explicit ID"),
|
||||
&d.header.alias.name,
|
||||
owner,
|
||||
)?;
|
||||
let mut slots = Slots {
|
||||
entries: 2,
|
||||
text: d.header.alias.name.len(),
|
||||
..Slots::default()
|
||||
};
|
||||
let mut scratch = 0;
|
||||
if let d::Body::Calculation { parameters, .. } = &d.body {
|
||||
slots.entries = add(
|
||||
slots.entries,
|
||||
add(1, parameters.len(), d.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?,
|
||||
d.span,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
scratch = parameters.len(); // per-signature duplicate-ID set
|
||||
for p in parameters {
|
||||
slots.text = add(slots.text, p.name.name.len(), p.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
}
|
||||
}
|
||||
catalog.reserve_tables(syntax, ledger, slots, owner)?;
|
||||
ledger
|
||||
.reserve(&syntax.budget, Slots::entries(scratch), d.span)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
catalog
|
||||
.tables
|
||||
.typed(d, &syntax.budget)
|
||||
.map_err(|e| Failure::typed(syntax, Some(file), e))?;
|
||||
ledger.release(Slots::entries(scratch));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(catalog)
|
||||
}
|
||||
|
||||
fn object<'a>(value: Option<&'a Ast>, key: &str) -> Option<&'a Ast> {
|
||||
match value {
|
||||
Some(Ast::Object(fields, _)) => fields.get(key),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Selector only, not a decoder. This executes before ordinary fields clone.
|
||||
fn active_references(
|
||||
declaration: &language::Declaration,
|
||||
budget: &mut Budget,
|
||||
) -> Result<(), language::Error> {
|
||||
let field = |key: &str| declaration.fields.get(key);
|
||||
let arrays = match declaration.kind {
|
||||
DeclKind::Element(Kind::Process | Kind::FunctionalChain | Kind::Scenario, _) => {
|
||||
[field("steps"), None]
|
||||
}
|
||||
DeclKind::Element(Kind::Occurrence, _) => {
|
||||
[object(object(field("body"), "Branch"), "occurrences"), None]
|
||||
}
|
||||
DeclKind::Relation(RelationKind::ContextualLink) => [
|
||||
object(field("source"), "references"),
|
||||
object(field("target"), "references"),
|
||||
],
|
||||
_ => [None, None],
|
||||
};
|
||||
for array in arrays.into_iter().flatten() {
|
||||
if let Ast::Array(items, _) = array {
|
||||
for item in items {
|
||||
let start = item.offset();
|
||||
budget
|
||||
.reference(Span { start, end: start })
|
||||
.map_err(super::ordinary_error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::language::analysis_source) fn resolve(
|
||||
mut syntax: ParsedMixedSyntax<'_>,
|
||||
) -> Result<ResolvedMixedProject<'_>, Failure> {
|
||||
let mut storage = Ledger {
|
||||
live: syntax
|
||||
.carried
|
||||
.as_ref()
|
||||
.map_or_else(Slots::default, |input| input.slots()),
|
||||
};
|
||||
let lower_storage = accounting::syntax(&syntax, &mut storage)?;
|
||||
let catalog = catalog(&syntax, &mut storage)?;
|
||||
let entry = entry(&syntax);
|
||||
// Only the empty model header is copied: retain its original syntax/header
|
||||
// ownership and construct the final model through the shared decoder once.
|
||||
let project = syntax.files[entry]
|
||||
.ordinary
|
||||
.header
|
||||
.as_ref()
|
||||
.expect("entry header")
|
||||
.project
|
||||
.clone();
|
||||
let count = syntax
|
||||
.files
|
||||
.iter()
|
||||
.try_fold(0usize, |n, f| {
|
||||
add(n, f.ordinary.declarations.len(), Span::default())
|
||||
})
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
storage
|
||||
.reserve(&syntax.budget, Slots::entries(count), Span::default())
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
let declarations: Vec<_> = syntax
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(file, unit)| unit.ordinary.declarations.iter().map(move |d| (file, d)))
|
||||
.collect();
|
||||
let mut nodes = 0;
|
||||
let constructed = language::construct_declarations(
|
||||
project,
|
||||
declarations.iter().map(|(_, d)| *d),
|
||||
&catalog.aliases,
|
||||
|d| {
|
||||
active_references(d, &mut syntax.budget)?;
|
||||
if matches!(d.kind, DeclKind::Node) {
|
||||
storage
|
||||
.reserve(&syntax.budget, Slots::entries(1), d.span)
|
||||
.map_err(super::ordinary_error)?;
|
||||
nodes += 1;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.map_err(|(index, error)| {
|
||||
Failure::ordinary(
|
||||
&syntax,
|
||||
declarations.get(index).map(|(file, _)| *file),
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
let language::ConstructedDeclarations {
|
||||
mut project,
|
||||
legacy_elements,
|
||||
} = constructed;
|
||||
debug_assert!(legacy_elements.is_empty()); // mixed syntax only admits source8
|
||||
drop(legacy_elements);
|
||||
drop(declarations);
|
||||
storage.release(Slots::entries(count));
|
||||
storage.release(Slots::entries(nodes));
|
||||
drop(catalog.aliases);
|
||||
storage.release(catalog.aliases_storage);
|
||||
|
||||
let all_lower = lower_storage
|
||||
.all()
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
storage
|
||||
.reserve(&syntax.budget, all_lower, Span::default())
|
||||
.map_err(|e| Failure::typed(&syntax, None, e))?;
|
||||
let lower::Lowered {
|
||||
definitions,
|
||||
elements,
|
||||
occurrences,
|
||||
expressions,
|
||||
} = lower::lower_bodies(
|
||||
&catalog.tables,
|
||||
syntax
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(file, unit)| unit.typed.iter().map(move |d| (file, d))),
|
||||
&syntax.budget,
|
||||
)
|
||||
.map_err(|e| Failure::typed(&syntax, e.file, e.error))?;
|
||||
// One global arena transfers verbatim; only now are all Resolver temporaries
|
||||
// dead. Output counts must fit the independently counted reservation.
|
||||
storage.release(Slots {
|
||||
entries: all_lower
|
||||
.entries
|
||||
.checked_sub(expressions.len())
|
||||
.expect("expression bound"),
|
||||
occurrences: all_lower
|
||||
.occurrences
|
||||
.checked_sub(occurrences.len())
|
||||
.expect("occurrence bound"),
|
||||
text: 0,
|
||||
});
|
||||
drop(catalog.tables);
|
||||
storage.release(catalog.tables_storage);
|
||||
for (id, value) in definitions {
|
||||
if project.definitions.contains_key(&id) {
|
||||
return Err(Failure::typed(
|
||||
&syntax,
|
||||
None,
|
||||
error(Span::default(), "Non-disjoint definition merge."),
|
||||
));
|
||||
}
|
||||
project.definitions.insert(id, value);
|
||||
}
|
||||
for (id, value) in elements {
|
||||
if project.elements.contains_key(&id) {
|
||||
return Err(Failure::typed(
|
||||
&syntax,
|
||||
None,
|
||||
error(Span::default(), "Non-disjoint element merge."),
|
||||
));
|
||||
}
|
||||
project.elements.insert(id, value);
|
||||
}
|
||||
syntax
|
||||
.limits
|
||||
.model
|
||||
.check_model(&project)
|
||||
.map_err(|message| {
|
||||
Failure::ordinary(
|
||||
&syntax,
|
||||
None,
|
||||
language::Error {
|
||||
code: "model_limit",
|
||||
offset: 0,
|
||||
message,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let (issues, admission) = validation::validate_with_usage(&project);
|
||||
// Raw architecture Issues are excluded. Each transferred string, copied path
|
||||
// and destination slot is admitted before mapping/retention; never truncate.
|
||||
let mut warnings = Vec::new();
|
||||
let mut rejected = false;
|
||||
for issue in issues {
|
||||
let owner = issue
|
||||
.element
|
||||
.and_then(|id| catalog.owners.get(&id))
|
||||
.copied()
|
||||
.unwrap_or(catalog.owners[&project.id]);
|
||||
let span = field_span(&syntax.files[owner.file], owner, issue.field.as_deref());
|
||||
let unit = &syntax.files[owner.file];
|
||||
let text = [
|
||||
unit.path.len(),
|
||||
issue.code.len(),
|
||||
issue.field.as_ref().map_or(0, String::len),
|
||||
issue.message.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.try_fold(0, |n, k| add(n, k, span))
|
||||
.map_err(|e| Failure::typed(&syntax, Some(owner.file), e))?;
|
||||
storage
|
||||
.reserve(
|
||||
&syntax.budget,
|
||||
Slots {
|
||||
entries: 1,
|
||||
occurrences: 0,
|
||||
text,
|
||||
},
|
||||
span,
|
||||
)
|
||||
.map_err(|e| Failure::typed(&syntax, Some(owner.file), e))?;
|
||||
rejected |= issue.severity == validation::Severity::Error;
|
||||
let prefix = &unit.source[..span.start];
|
||||
let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
|
||||
let column = prefix.rsplit('\n').next().expect("prefix").chars().count() + 1;
|
||||
warnings.push(SourceDiagnostic {
|
||||
path: unit.path.into(),
|
||||
related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: issue.severity,
|
||||
code: issue.code,
|
||||
object_id: issue.element,
|
||||
field: issue.field,
|
||||
message: issue.message,
|
||||
span,
|
||||
line,
|
||||
column,
|
||||
},
|
||||
});
|
||||
}
|
||||
if rejected {
|
||||
return Err(Failure {
|
||||
diagnostics: warnings,
|
||||
});
|
||||
}
|
||||
let (static_limits, static_usage) = admission.expect("successful validation retains admission");
|
||||
drop(catalog.owners);
|
||||
storage.release(catalog.owners_storage);
|
||||
Ok(ResolvedMixedProject {
|
||||
syntax,
|
||||
project,
|
||||
warnings,
|
||||
static_limits,
|
||||
static_usage,
|
||||
expressions,
|
||||
occurrences,
|
||||
storage,
|
||||
})
|
||||
}
|
||||
|
||||
/// Exact known field or explicit whole-owner fallback. Never guess by offsets.
|
||||
fn field_span(file: &FileSyntax<'_>, owner: Owner, field: Option<&str>) -> Span {
|
||||
let Some(field) = field else {
|
||||
return owner.span;
|
||||
};
|
||||
match owner.kind {
|
||||
OwnerKind::Project => file
|
||||
.ordinary
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(|h| (field == "description").then_some(h.description).flatten())
|
||||
.unwrap_or(owner.span),
|
||||
OwnerKind::Ordinary(index) => file.ordinary.declarations[index]
|
||||
.field_spans
|
||||
.get(field)
|
||||
.copied()
|
||||
.unwrap_or(owner.span),
|
||||
OwnerKind::Typed(index) => typed_field(&file.typed[index], field).unwrap_or(owner.span),
|
||||
}
|
||||
}
|
||||
fn typed_field(d: &d::Declaration, field: &str) -> Option<Span> {
|
||||
if field == "description" {
|
||||
return d.description.as_ref().map(|f| f.value_span);
|
||||
}
|
||||
if field == "alias" {
|
||||
return Some(d.header.alias.span);
|
||||
}
|
||||
if field == "name" {
|
||||
return Some(d.header.name_span);
|
||||
}
|
||||
match &d.body {
|
||||
d::Body::QuantityKind { dimension, algebra } => match field {
|
||||
"dimension" => Some(dimension.value_span),
|
||||
"algebra" => Some(algebra.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Unit {
|
||||
kind,
|
||||
scale,
|
||||
offset,
|
||||
symbol,
|
||||
} => match field {
|
||||
"kind" => Some(kind.value_span),
|
||||
"scale" => Some(scale.value_span),
|
||||
"offset" => Some(offset.value_span),
|
||||
"symbol" => Some(symbol.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Calculation { result, body, .. } => match field {
|
||||
"result" => Some(result.value_span),
|
||||
"body" => Some(body.value_span),
|
||||
// Current semantic admission reports aggregate "parameters", not a
|
||||
// qualified parameter/type path. Do not manufacture precision from
|
||||
// a UUID substring; lowerer failures already carry exact file/span.
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Property {
|
||||
subject,
|
||||
value_type,
|
||||
base,
|
||||
display_unit,
|
||||
} => match field {
|
||||
"subject" => Some(subject.value_span),
|
||||
"type" | "value_type" => Some(value_type.value_span),
|
||||
"base" => Some(base.value_span),
|
||||
"display_unit" => display_unit.as_ref().map(|f| f.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Check {
|
||||
subject,
|
||||
documented_constraint,
|
||||
predicate,
|
||||
} => match field {
|
||||
"subject" => subject.as_ref().map(|f| f.value_span),
|
||||
"documented_constraint" => documented_constraint.as_ref().map(|f| f.value_span),
|
||||
"predicate" => Some(predicate.value_span),
|
||||
_ => None,
|
||||
},
|
||||
d::Body::Case {
|
||||
subject,
|
||||
base,
|
||||
overrides,
|
||||
} => match field {
|
||||
"subject" => subject.as_ref().map(|f| f.value_span),
|
||||
"base" => Some(base.value_span),
|
||||
"overrides" => Some(overrides.value_span),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,378 @@
|
||||
//! Logical auxiliary storage, separate from model/serde and raw validator heap.
|
||||
use super::*;
|
||||
use crate::language::{EditKind, EditValue};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(in crate::language::analysis_source) struct Slots {
|
||||
pub entries: usize,
|
||||
pub occurrences: usize,
|
||||
pub text: usize,
|
||||
}
|
||||
impl Slots {
|
||||
pub fn entries(entries: usize) -> Self {
|
||||
Self {
|
||||
entries,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn text(text: usize) -> Self {
|
||||
Self {
|
||||
text,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn add(self, other: Self, at: Span) -> Result<Self, super::super::super::Error> {
|
||||
Ok(Self {
|
||||
entries: add(self.entries, other.entries, at)?,
|
||||
occurrences: add(self.occurrences, other.occurrences, at)?,
|
||||
text: add(self.text, other.text, at)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub(super) fn add(a: usize, b: usize, at: Span) -> Result<usize, super::super::super::Error> {
|
||||
a.checked_add(b)
|
||||
.ok_or_else(|| error(at, "Mixed auxiliary storage size overflow."))
|
||||
}
|
||||
pub(super) fn mul(a: usize, b: usize, at: Span) -> Result<usize, super::super::super::Error> {
|
||||
a.checked_mul(b)
|
||||
.ok_or_else(|| error(at, "Mixed auxiliary storage size overflow."))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(in crate::language::analysis_source) struct Ledger {
|
||||
pub live: Slots,
|
||||
}
|
||||
impl Ledger {
|
||||
pub fn reserve(&mut self, budget: &Budget, slots: Slots, at: Span) -> Result<(), TypedError> {
|
||||
let next = self.live.add(slots, at)?;
|
||||
budget.lowering_entries(next.entries, at)?;
|
||||
budget.lowering_occurrences(next.occurrences, at)?;
|
||||
budget.index_bytes(next.text, at)?;
|
||||
self.live = next;
|
||||
Ok(())
|
||||
}
|
||||
pub fn release(&mut self, slots: Slots) {
|
||||
self.live.entries = self
|
||||
.live
|
||||
.entries
|
||||
.checked_sub(slots.entries)
|
||||
.expect("entry reservation");
|
||||
self.live.occurrences = self
|
||||
.live
|
||||
.occurrences
|
||||
.checked_sub(slots.occurrences)
|
||||
.expect("occurrence reservation");
|
||||
self.live.text = self
|
||||
.live
|
||||
.text
|
||||
.checked_sub(slots.text)
|
||||
.expect("text reservation");
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact output-site counts and conservative whole-lowering temporary bound.
|
||||
/// N expression nodes: at most 2N Visit/Build slots, N borrowed child slots,
|
||||
/// N value/split slots. A arguments: A signature slots plus A seen-set slots.
|
||||
/// `Resolver::call` borrows the callee signature. Its parameter Vec/seen set
|
||||
/// contain only supplied arguments, including on incomplete/invalid calls;
|
||||
/// no allocation is proportional to the full signature during body lowering.
|
||||
/// The model expressions/maps themselves are outside this auxiliary ledger.
|
||||
#[derive(Default)]
|
||||
pub(super) struct LowerStorage {
|
||||
pub nodes: usize,
|
||||
pub occurrences: usize,
|
||||
pub arguments: usize,
|
||||
}
|
||||
impl LowerStorage {
|
||||
pub fn all(&self) -> Result<Slots, TypedError> {
|
||||
let at = Span::default();
|
||||
Ok(Slots {
|
||||
entries: add(mul(self.nodes, 5, at)?, mul(self.arguments, 2, at)?, at)?,
|
||||
occurrences: self.occurrences,
|
||||
text: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum Item<'a> {
|
||||
Ast(&'a Ast),
|
||||
Edit(&'a EditValue),
|
||||
Expr(&'a Expression),
|
||||
}
|
||||
struct Walk<'a, 'b> {
|
||||
ledger: &'b mut Ledger,
|
||||
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)
|
||||
}
|
||||
fn push(&mut self, item: Item<'a>) -> Result<(), TypedError> {
|
||||
self.ledger
|
||||
.reserve(self.budget, Slots::entries(1), Span::default())?;
|
||||
self.work.push(item);
|
||||
Ok(())
|
||||
}
|
||||
fn reference(&mut self, r: &Reference) -> Result<(), TypedError> {
|
||||
self.text(&r.name, r.span)?;
|
||||
self.lower.occurrences = add(self.lower.occurrences, 1, r.span)?;
|
||||
Ok(())
|
||||
}
|
||||
fn optional(&mut self, field: &Option<d::Field<Option<Reference>>>) -> Result<(), TypedError> {
|
||||
if let Some(r) = field.as_ref().and_then(|f| f.value.as_ref()) {
|
||||
self.reference(r)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn ty(&mut self, ty: &d::TypeSyntax) -> Result<(), TypedError> {
|
||||
if let d::ScalarKind::Quantity(r) = &ty.scalar.kind {
|
||||
self.reference(r)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn binding(&mut self, binding: &'a Binding) -> Result<(), TypedError> {
|
||||
if let Binding::Expression(e) = binding {
|
||||
self.push(Item::Expr(e))?;
|
||||
}
|
||||
self.drain()
|
||||
}
|
||||
fn drain(&mut self) -> Result<(), TypedError> {
|
||||
while let Some(item) = self.work.pop() {
|
||||
self.ledger.release(Slots::entries(1));
|
||||
match item {
|
||||
Item::Ast(ast) => match ast {
|
||||
Ast::Literal(serde_json::Value::String(s), _)
|
||||
| Ast::Word(s, _)
|
||||
| Ast::Reference(s, _) => self.text(s, Span::default())?,
|
||||
Ast::Array(items, _) => {
|
||||
for child in items {
|
||||
self.push(Item::Ast(child))?;
|
||||
}
|
||||
}
|
||||
Ast::Object(fields, _) => {
|
||||
for (key, child) in fields {
|
||||
self.text(key, Span::default())?;
|
||||
self.push(Item::Ast(child))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Item::Edit(value) => match &value.kind {
|
||||
EditKind::Reference(s) => self.text(s, value.span)?,
|
||||
EditKind::Record(fields) => {
|
||||
for (key, field) in fields {
|
||||
self.text(key, field.span)?;
|
||||
self.push(Item::Edit(&field.value))?;
|
||||
}
|
||||
}
|
||||
EditKind::List(items) => {
|
||||
for child in items {
|
||||
self.push(Item::Edit(child))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Item::Expr(e) => {
|
||||
self.lower.nodes = add(self.lower.nodes, 1, e.span)?;
|
||||
use ExpressionKind as E;
|
||||
match &e.kind {
|
||||
E::Quantity { unit, .. } | E::Property(unit) | E::Parameter(unit) => {
|
||||
self.reference(unit)?
|
||||
}
|
||||
E::List(items) => {
|
||||
for child in items {
|
||||
self.push(Item::Expr(child))?;
|
||||
}
|
||||
}
|
||||
E::Unary { operand, .. } => self.push(Item::Expr(operand))?,
|
||||
E::Binary { left, right, .. } => {
|
||||
self.push(Item::Expr(left))?;
|
||||
self.push(Item::Expr(right))?;
|
||||
}
|
||||
E::If {
|
||||
condition,
|
||||
when_true,
|
||||
when_false,
|
||||
} => {
|
||||
self.push(Item::Expr(condition))?;
|
||||
self.push(Item::Expr(when_true))?;
|
||||
self.push(Item::Expr(when_false))?;
|
||||
}
|
||||
E::Call {
|
||||
definition,
|
||||
arguments,
|
||||
} => {
|
||||
self.reference(definition)?;
|
||||
self.lower.arguments =
|
||||
add(self.lower.arguments, arguments.len(), e.span)?;
|
||||
for argument in arguments {
|
||||
self.reference(&argument.parameter)?;
|
||||
self.push(Item::Expr(&argument.value))?;
|
||||
}
|
||||
}
|
||||
E::Sum { values, initial } => {
|
||||
self.push(Item::Expr(values))?;
|
||||
self.push(Item::Expr(initial))?;
|
||||
}
|
||||
E::Power { base, .. } => self.push(Item::Expr(base))?,
|
||||
E::AsKind { kind, value } => {
|
||||
self.reference(kind)?;
|
||||
self.push(Item::Expr(value))?;
|
||||
}
|
||||
E::Boolean(_) | E::Number(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn syntax(
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
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,
|
||||
Slots::entries(
|
||||
add(syntax.metadata, syntax.budget.usage.entries, at)
|
||||
.map_err(|e| Failure::typed(syntax, None, e))?,
|
||||
),
|
||||
at,
|
||||
)
|
||||
.map_err(|e| Failure::typed(syntax, None, e))?;
|
||||
let mut walk = Walk {
|
||||
ledger,
|
||||
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> {
|
||||
if let Some(header) = &unit.ordinary.header {
|
||||
for s in [
|
||||
&header.project.alias,
|
||||
&header.project.name,
|
||||
&header.project.description,
|
||||
] {
|
||||
walk.text(s, header.span)?;
|
||||
}
|
||||
}
|
||||
for (path, span) in &unit.ordinary.includes {
|
||||
walk.text(path, *span)?;
|
||||
}
|
||||
for declaration in &unit.ordinary.declarations {
|
||||
walk.text(&declaration.alias, declaration.span)?;
|
||||
walk.text(&declaration.name, declaration.span)?;
|
||||
for key in declaration.field_spans.keys() {
|
||||
walk.text(key, declaration.span)?;
|
||||
}
|
||||
for (key, value) in &declaration.fields {
|
||||
walk.text(key, declaration.span)?;
|
||||
walk.push(Item::Ast(value))?;
|
||||
walk.drain()?;
|
||||
}
|
||||
}
|
||||
for (_, position) in &unit.positions {
|
||||
walk.push(Item::Edit(&position.fields))?;
|
||||
walk.drain()?;
|
||||
}
|
||||
for declaration in &unit.typed {
|
||||
walk.text(
|
||||
&declaration.header.alias.name,
|
||||
declaration.header.alias.span,
|
||||
)?;
|
||||
walk.text(&declaration.header.name, declaration.header.name_span)?;
|
||||
if let Some(f) = &declaration.description {
|
||||
walk.text(&f.value, f.span)?;
|
||||
}
|
||||
match &declaration.body {
|
||||
d::Body::QuantityKind { algebra, .. } => {
|
||||
if let d::AlgebraSyntax::Affine(r) = &algebra.value {
|
||||
walk.reference(r)?;
|
||||
}
|
||||
}
|
||||
d::Body::Unit { kind, symbol, .. } => {
|
||||
walk.reference(&kind.value)?;
|
||||
walk.text(&symbol.value, symbol.span)?;
|
||||
}
|
||||
d::Body::Calculation {
|
||||
parameters,
|
||||
result,
|
||||
body,
|
||||
} => {
|
||||
for p in parameters {
|
||||
walk.reference(&p.name)?;
|
||||
walk.ty(&p.value_type)?;
|
||||
}
|
||||
walk.ty(&result.value)?;
|
||||
walk.binding(&body.value)?;
|
||||
}
|
||||
d::Body::Property {
|
||||
subject,
|
||||
value_type,
|
||||
base,
|
||||
display_unit,
|
||||
} => {
|
||||
walk.reference(&subject.value)?;
|
||||
walk.ty(&value_type.value)?;
|
||||
walk.binding(&base.value)?;
|
||||
walk.optional(display_unit)?;
|
||||
}
|
||||
d::Body::Check {
|
||||
subject,
|
||||
documented_constraint,
|
||||
predicate,
|
||||
} => {
|
||||
walk.optional(subject)?;
|
||||
walk.optional(documented_constraint)?;
|
||||
walk.binding(&predicate.value)?;
|
||||
}
|
||||
d::Body::Case {
|
||||
subject, overrides, ..
|
||||
} => {
|
||||
walk.optional(subject)?;
|
||||
for entry in &overrides.value {
|
||||
walk.reference(&entry.target)?;
|
||||
walk.binding(&entry.binding)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
//! Complete authored-name ownership from the sealed ordinary position projection.
|
||||
//! Physical keys belong only to this immutable snapshot. This is not edit authority.
|
||||
use super::super::super::{index, lex};
|
||||
use super::*;
|
||||
use crate::language::{EditField, EditKind, EditValue};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Canonical file key and the whole alias Word's half-open byte range.
|
||||
pub(in crate::language::analysis_source) type OrdinaryKey = (usize, usize, usize);
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(in crate::language::analysis_source) enum OrdinaryRole {
|
||||
Header,
|
||||
Reference,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(in crate::language::analysis_source) struct OrdinaryOccurrence {
|
||||
pub owner: Id,
|
||||
pub target: Id,
|
||||
pub role: OrdinaryRole,
|
||||
/// Alias token for Header; marker, intervening trivia and alias for Reference.
|
||||
pub extent: Span,
|
||||
}
|
||||
|
||||
/// Owns both occurrence families and their complete original admission witness.
|
||||
pub(in crate::language::analysis_source) struct AuthoredMixedProject<'s> {
|
||||
indexed: IndexedMixedProject<'s>,
|
||||
ordinary: BTreeMap<OrdinaryKey, OrdinaryOccurrence>,
|
||||
}
|
||||
impl<'s> IndexedMixedProject<'s> {
|
||||
pub fn index_ordinary(mut self) -> Result<AuthoredMixedProject<'s>, Failure> {
|
||||
let ordinary = build(
|
||||
&self.resolved.syntax,
|
||||
&self.data,
|
||||
&mut self.resolved.storage,
|
||||
)
|
||||
.map_err(|failure| Failure::index(&self.resolved.syntax, failure))?;
|
||||
Ok(AuthoredMixedProject {
|
||||
indexed: self,
|
||||
ordinary,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl AuthoredMixedProject<'_> {
|
||||
pub fn project(&self) -> &Project {
|
||||
self.indexed.project()
|
||||
}
|
||||
pub fn warnings(&self) -> &[SourceDiagnostic] {
|
||||
self.indexed.warnings()
|
||||
}
|
||||
pub fn bundle(&self) -> &crate::source_project::SourceBundle {
|
||||
self.indexed.bundle()
|
||||
}
|
||||
pub fn files(&self) -> impl Iterator<Item = (usize, &str, &str)> {
|
||||
self.indexed.files()
|
||||
}
|
||||
pub fn names(&self) -> &BTreeMap<lower::Target, String> {
|
||||
self.indexed.names()
|
||||
}
|
||||
pub fn typed_occurrences(&self) -> &BTreeMap<index::OccurrenceKey, index::Location> {
|
||||
self.indexed.occurrences()
|
||||
}
|
||||
pub fn expressions(&self) -> &BTreeMap<index::ExpressionKey, index::Location> {
|
||||
self.indexed.expressions()
|
||||
}
|
||||
pub fn ordinary_occurrences(&self) -> &BTreeMap<OrdinaryKey, OrdinaryOccurrence> {
|
||||
&self.ordinary
|
||||
}
|
||||
}
|
||||
|
||||
struct Builder<'a, 'b> {
|
||||
budget: &'a Budget,
|
||||
storage: &'b mut Ledger,
|
||||
globals: BTreeMap<&'a str, (Id, bool)>,
|
||||
typed: BTreeSet<OrdinaryKey>,
|
||||
ordinary: BTreeMap<OrdinaryKey, OrdinaryOccurrence>,
|
||||
}
|
||||
fn build(
|
||||
syntax: &ParsedMixedSyntax<'_>,
|
||||
data: &index::IndexData,
|
||||
storage: &mut Ledger,
|
||||
) -> Result<BTreeMap<OrdinaryKey, OrdinaryOccurrence>, lower::Failure> {
|
||||
let mut file = None;
|
||||
let mut b = Builder {
|
||||
budget: &syntax.budget,
|
||||
storage,
|
||||
globals: BTreeMap::new(),
|
||||
typed: BTreeSet::new(),
|
||||
ordinary: BTreeMap::new(),
|
||||
};
|
||||
let result = (|| {
|
||||
b.budget.active()?;
|
||||
for (target, name) in &data.names {
|
||||
if let lower::Target::Global(id) = target {
|
||||
b.reserve(Span::default())?;
|
||||
if b.globals.insert(name, (*id, false)).is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Duplicate ordinary global spelling.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, at) in &data.occurrences {
|
||||
file = Some(at.file);
|
||||
let unit = syntax
|
||||
.files
|
||||
.get(at.file)
|
||||
.ok_or_else(|| error(at.span, "Ordinary index has no canonical file."))?;
|
||||
source(unit, at.span)?;
|
||||
b.reserve(at.span)?;
|
||||
if !b.typed.insert((at.file, at.span.start, at.span.end)) {
|
||||
return Err(error(at.span, "Duplicate typed physical name token."));
|
||||
}
|
||||
if key.role == index::TokenRole::Declaration {
|
||||
if key.target != lower::Target::Global(key.owner) {
|
||||
return Err(error(at.span, "Typed global header has a different owner."));
|
||||
}
|
||||
b.header(key.owner, source(unit, at.span)?, at.span)?;
|
||||
}
|
||||
}
|
||||
for (number, unit) in syntax.files.iter().enumerate() {
|
||||
file = Some(number);
|
||||
if let Some(header) = &unit.ordinary.header {
|
||||
if unit.path != syntax.bundle.entry {
|
||||
return Err(error(
|
||||
header.span,
|
||||
"Project header is outside the canonical entry.",
|
||||
));
|
||||
}
|
||||
source(unit, header.span)?;
|
||||
let start = token_at(unit, header.span.start, header.span)?;
|
||||
let keyword = unit.tokens[start];
|
||||
if keyword.class != lex::Class::Word || source(unit, keyword.span)? != "project" {
|
||||
return Err(error(
|
||||
header.span,
|
||||
"Project header lacks its original project token.",
|
||||
));
|
||||
}
|
||||
let alias = *unit
|
||||
.tokens
|
||||
.get(start + 1)
|
||||
.ok_or_else(|| error(header.span, "Project header lacks an alias token."))?;
|
||||
contains(header.span, alias.span)?;
|
||||
word(unit, alias.span, &header.project.alias)?;
|
||||
b.header(header.project.id, &header.project.alias, alias.span)?;
|
||||
b.insert(
|
||||
number,
|
||||
alias.span,
|
||||
OrdinaryOccurrence {
|
||||
owner: header.project.id,
|
||||
target: header.project.id,
|
||||
role: OrdinaryRole::Header,
|
||||
extent: alias.span,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
if unit.ordinary.declarations.len() != unit.positions.len() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Ordinary declaration position count differs from syntax.",
|
||||
));
|
||||
}
|
||||
for (declaration, (owner, position)) in
|
||||
unit.ordinary.declarations.iter().zip(&unit.positions)
|
||||
{
|
||||
if *owner != declaration.id
|
||||
|| position.span != declaration.span
|
||||
|| position.body != declaration.body
|
||||
|| position.lexical_owner != declaration.lexical_owner
|
||||
{
|
||||
return Err(error(
|
||||
position.span,
|
||||
"Ordinary position differs from its declaration owner.",
|
||||
));
|
||||
}
|
||||
source(unit, declaration.span)?;
|
||||
source(unit, declaration.body)?;
|
||||
contains(declaration.span, declaration.body)?;
|
||||
let alias = *position.header.get("alias").ok_or_else(|| {
|
||||
error(
|
||||
position.span,
|
||||
"Ordinary declaration lacks an alias position.",
|
||||
)
|
||||
})?;
|
||||
let start = token_at(unit, declaration.span.start, declaration.span)?;
|
||||
if unit.tokens.get(start + 1).map(|t| t.span) != Some(alias)
|
||||
|| alias.end > declaration.body.start
|
||||
{
|
||||
return Err(error(
|
||||
alias,
|
||||
"Ordinary alias is not its declaration header token.",
|
||||
));
|
||||
}
|
||||
contains(declaration.span, alias)?;
|
||||
word(unit, alias, &declaration.alias)?;
|
||||
b.header(*owner, &declaration.alias, alias)?;
|
||||
b.insert(
|
||||
number,
|
||||
alias,
|
||||
OrdinaryOccurrence {
|
||||
owner: *owner,
|
||||
target: *owner,
|
||||
role: OrdinaryRole::Header,
|
||||
extent: alias,
|
||||
},
|
||||
)?;
|
||||
// The synthetic root includes braces; actual fields lie in the interior.
|
||||
let root_start = declaration.body.start.checked_sub(1).ok_or_else(|| {
|
||||
error(position.fields.span, "Invalid ordinary body boundary.")
|
||||
})?;
|
||||
let root_end = declaration.body.end.checked_add(1).ok_or_else(|| {
|
||||
error(position.fields.span, "Invalid ordinary body boundary.")
|
||||
})?;
|
||||
if position.fields.span
|
||||
!= (Span {
|
||||
start: root_start,
|
||||
end: root_end,
|
||||
})
|
||||
{
|
||||
return Err(error(
|
||||
position.fields.span,
|
||||
"Ordinary synthetic root differs from its body braces.",
|
||||
));
|
||||
}
|
||||
source(unit, position.fields.span)?;
|
||||
contains(declaration.span, position.fields.span)?;
|
||||
let EditKind::Record(fields) = &position.fields.kind else {
|
||||
return Err(error(
|
||||
position.fields.span,
|
||||
"Ordinary synthetic root must be a record.",
|
||||
));
|
||||
};
|
||||
b.fields(
|
||||
number,
|
||||
unit,
|
||||
*owner,
|
||||
declaration.span,
|
||||
declaration.body,
|
||||
fields,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
file = None;
|
||||
if b.globals.values().any(|(_, seen)| !seen) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Global name is missing its authored declaration header.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
result.map_err(|error| lower::Failure { file, error })?;
|
||||
let globals = b.globals.len();
|
||||
let typed = b.typed.len();
|
||||
drop(b.globals);
|
||||
drop(b.typed);
|
||||
b.storage.release(Slots::entries(
|
||||
add(globals, typed, Span::default())
|
||||
.map_err(|error| lower::Failure { file: None, error })?,
|
||||
));
|
||||
Ok(b.ordinary)
|
||||
}
|
||||
impl Builder<'_, '_> {
|
||||
fn reserve(&mut self, at: Span) -> Result<(), TypedError> {
|
||||
self.storage.reserve(self.budget, Slots::entries(1), at)
|
||||
}
|
||||
fn header(&mut self, id: Id, alias: &str, at: Span) -> Result<(), TypedError> {
|
||||
let Some((target, seen)) = self.globals.get_mut(alias) else {
|
||||
return Err(error(at, "Ordinary header has no global target."));
|
||||
};
|
||||
if *target != id {
|
||||
return Err(error(
|
||||
at,
|
||||
"Ordinary header differs from its global identity.",
|
||||
));
|
||||
}
|
||||
if *seen {
|
||||
return Err(error(
|
||||
at,
|
||||
"Global declaration header appears more than once.",
|
||||
));
|
||||
}
|
||||
*seen = true;
|
||||
Ok(())
|
||||
}
|
||||
fn insert(
|
||||
&mut self,
|
||||
file: usize,
|
||||
alias: Span,
|
||||
occurrence: OrdinaryOccurrence,
|
||||
) -> Result<(), TypedError> {
|
||||
let key = (file, alias.start, alias.end);
|
||||
if self.typed.contains(&key) || self.ordinary.contains_key(&key) {
|
||||
return Err(error(
|
||||
alias,
|
||||
"Duplicate ordinary or typed physical name token.",
|
||||
));
|
||||
}
|
||||
self.reserve(alias)?;
|
||||
self.ordinary.insert(key, occurrence);
|
||||
Ok(())
|
||||
}
|
||||
fn fields(
|
||||
&mut self,
|
||||
file: usize,
|
||||
unit: &FileSyntax<'_>,
|
||||
owner: Id,
|
||||
declaration: Span,
|
||||
body: Span,
|
||||
fields: &BTreeMap<String, EditField>,
|
||||
) -> Result<(), TypedError> {
|
||||
let mut work = Vec::new();
|
||||
for field in fields.values() {
|
||||
self.push_field(unit, &mut work, field, body)?;
|
||||
}
|
||||
while let Some((value, parent)) = work.pop() {
|
||||
self.storage.release(Slots::entries(1));
|
||||
source(unit, value.span)?;
|
||||
contains(parent, value.span)?;
|
||||
contains(declaration, value.span)?;
|
||||
match &value.kind {
|
||||
EditKind::Reference(alias) => {
|
||||
let start = token_at(unit, value.span.start, value.span)?;
|
||||
let marker = unit.tokens[start];
|
||||
if marker.class != lex::Class::Punctuation || source(unit, marker.span)? != "@"
|
||||
{
|
||||
return Err(error(
|
||||
value.span,
|
||||
"Ordinary reference lacks its original marker token.",
|
||||
));
|
||||
}
|
||||
let name = *unit.tokens.get(start + 1).ok_or_else(|| {
|
||||
error(value.span, "Ordinary reference lacks its alias token.")
|
||||
})?;
|
||||
if name.span.end != value.span.end {
|
||||
return Err(error(
|
||||
value.span,
|
||||
"Ordinary reference extent differs from its complete alias token.",
|
||||
));
|
||||
}
|
||||
word(unit, name.span, alias)?;
|
||||
let target = self
|
||||
.globals
|
||||
.get(alias.as_str())
|
||||
.ok_or_else(|| {
|
||||
error(name.span, "Ordinary reference has no global target.")
|
||||
})?
|
||||
.0;
|
||||
self.insert(
|
||||
file,
|
||||
name.span,
|
||||
OrdinaryOccurrence {
|
||||
owner,
|
||||
target,
|
||||
role: OrdinaryRole::Reference,
|
||||
extent: value.span,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
EditKind::Record(fields) => {
|
||||
for field in fields.values() {
|
||||
self.push_field(unit, &mut work, field, value.span)?;
|
||||
}
|
||||
}
|
||||
EditKind::List(items) => {
|
||||
for item in items {
|
||||
self.reserve(item.span)?;
|
||||
work.push((item, value.span));
|
||||
}
|
||||
}
|
||||
EditKind::Atom | EditKind::HistoricalUuid(_) => {}
|
||||
}
|
||||
}
|
||||
drop(work);
|
||||
Ok(())
|
||||
}
|
||||
fn push_field<'a>(
|
||||
&mut self,
|
||||
unit: &FileSyntax<'_>,
|
||||
work: &mut Vec<(&'a EditValue, Span)>,
|
||||
field: &'a EditField,
|
||||
parent: Span,
|
||||
) -> Result<(), TypedError> {
|
||||
source(unit, field.span)?;
|
||||
contains(parent, field.span)?;
|
||||
contains(field.span, field.value.span)?;
|
||||
self.reserve(field.value.span)?;
|
||||
work.push((&field.value, field.span));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn source<'a>(unit: &'a FileSyntax<'_>, span: Span) -> Result<&'a str, TypedError> {
|
||||
unit.source
|
||||
.get(span.start..span.end)
|
||||
.ok_or_else(|| error(span, "Ordinary span is outside its original UTF-8 file."))
|
||||
}
|
||||
fn contains(parent: Span, child: Span) -> Result<(), TypedError> {
|
||||
if child.start < parent.start || child.end > parent.end || child.start > child.end {
|
||||
return Err(error(child, "Ordinary span crosses its containing syntax."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn token_at(unit: &FileSyntax<'_>, start: usize, at: Span) -> Result<usize, TypedError> {
|
||||
unit.tokens
|
||||
.binary_search_by_key(&start, |t| t.span.start)
|
||||
.map_err(|_| {
|
||||
error(
|
||||
at,
|
||||
"Ordinary occurrence does not start at an original token.",
|
||||
)
|
||||
})
|
||||
}
|
||||
fn word(unit: &FileSyntax<'_>, at: Span, expected: &str) -> Result<(), TypedError> {
|
||||
let token = unit.tokens[token_at(unit, at.start, at)?];
|
||||
if token.class != lex::Class::Word || token.span != at {
|
||||
return Err(error(
|
||||
at,
|
||||
"Ordinary alias is not an original whole Word token.",
|
||||
));
|
||||
}
|
||||
if source(unit, at)? != expected {
|
||||
return Err(error(
|
||||
at,
|
||||
"Ordinary alias differs from its retained spelling.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(in crate::language::analysis_source) mod rename;
|
||||
@@ -0,0 +1,492 @@
|
||||
//! 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;
|
||||
|
||||
pub(in crate::language::analysis_source) struct RenamePlan<'s, 't> {
|
||||
source: AuthoredMixedProject<'s>,
|
||||
desired: &'t Project,
|
||||
names: BTreeMap<Target, &'t str>,
|
||||
edits: BTreeMap<OrdinaryKey, Target>,
|
||||
}
|
||||
impl<'s> AuthoredMixedProject<'s> {
|
||||
pub fn plan_rename<'t>(mut self, desired: &'t Project) -> Result<RenamePlan<'s, 't>, Failure> {
|
||||
let mut failure_file = None;
|
||||
let result = (|| {
|
||||
let resolved = &mut self.indexed.resolved;
|
||||
let e = resolved.expressions.len();
|
||||
resolved.storage.reserve(
|
||||
&resolved.syntax.budget,
|
||||
Slots::entries(e),
|
||||
Span::default(),
|
||||
)?;
|
||||
let equal = same_project(&resolved.project, desired);
|
||||
resolved.storage.release(Slots::entries(e));
|
||||
if !equal {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename target changes non-name project semantics.",
|
||||
));
|
||||
}
|
||||
let mut names = BTreeMap::new();
|
||||
let mut bytes = 0;
|
||||
visit_names(desired, &mut |target, name| {
|
||||
let old = self.indexed.data.names.get(&target).ok_or_else(|| {
|
||||
error(
|
||||
Span::default(),
|
||||
"Rename target is absent from the original catalog.",
|
||||
)
|
||||
})?;
|
||||
if old != name {
|
||||
bytes = add(bytes, name.len(), Span::default())?;
|
||||
self.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.budget
|
||||
.index_bytes(bytes, Span::default())?;
|
||||
if !crate::model::valid_alias(name) {
|
||||
return Err(error(Span::default(), "Invalid rename alias."));
|
||||
}
|
||||
self.indexed.resolved.storage.reserve(
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
Slots::entries(1),
|
||||
Span::default(),
|
||||
)?;
|
||||
if names.insert(target, name).is_some() {
|
||||
return Err(error(Span::default(), "Duplicate rename target."));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if !names.is_empty() {
|
||||
let mut namespace = BTreeSet::new();
|
||||
for (target, old) in &self.indexed.data.names {
|
||||
let scope = match target {
|
||||
Target::Global(_) => None,
|
||||
Target::Parameter { calculation, .. } => Some(*calculation),
|
||||
};
|
||||
let name = names.get(target).copied().unwrap_or(old);
|
||||
self.indexed.resolved.storage.reserve(
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
Slots::entries(1),
|
||||
Span::default(),
|
||||
)?;
|
||||
if !namespace.insert((scope, name)) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename aliases collide in their final namespace.",
|
||||
));
|
||||
}
|
||||
}
|
||||
let q = namespace.len();
|
||||
drop(namespace);
|
||||
self.indexed.resolved.storage.release(Slots::entries(q));
|
||||
}
|
||||
let mut edits = BTreeMap::new();
|
||||
for (key, location) in &self.indexed.data.occurrences {
|
||||
if names.contains_key(&key.target) {
|
||||
failure_file = Some(location.file);
|
||||
insert_edit(
|
||||
&mut self.indexed.resolved.storage,
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
&mut edits,
|
||||
(location.file, location.span.start, location.span.end),
|
||||
key.target,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for (&key, occurrence) in &self.ordinary {
|
||||
let target = Target::Global(occurrence.target);
|
||||
if names.contains_key(&target) {
|
||||
failure_file = Some(key.0);
|
||||
insert_edit(
|
||||
&mut self.indexed.resolved.storage,
|
||||
&self.indexed.resolved.syntax.budget,
|
||||
&mut edits,
|
||||
key,
|
||||
target,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok((names, edits))
|
||||
})();
|
||||
let (names, edits) = result.map_err(|error| {
|
||||
Failure::index(
|
||||
&self.indexed.resolved.syntax,
|
||||
lower::Failure {
|
||||
file: failure_file,
|
||||
error,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let plan = RenamePlan {
|
||||
source: self,
|
||||
desired,
|
||||
names,
|
||||
edits,
|
||||
};
|
||||
plan.validate()?;
|
||||
Ok(plan)
|
||||
}
|
||||
}
|
||||
fn insert_edit(
|
||||
storage: &mut Ledger,
|
||||
budget: &Budget,
|
||||
edits: &mut BTreeMap<OrdinaryKey, Target>,
|
||||
key: OrdinaryKey,
|
||||
target: Target,
|
||||
) -> Result<(), TypedError> {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
if edits.contains_key(&key) {
|
||||
return Err(error(at, "Duplicate rename physical token."));
|
||||
}
|
||||
storage.reserve(budget, Slots::entries(1), at)?;
|
||||
edits.insert(key, target);
|
||||
Ok(())
|
||||
}
|
||||
impl<'s, 't> RenamePlan<'s, 't> {
|
||||
pub fn source(&self) -> &AuthoredMixedProject<'s> {
|
||||
&self.source
|
||||
}
|
||||
pub fn desired(&self) -> &'t Project {
|
||||
self.desired
|
||||
}
|
||||
pub fn changed_names(&self) -> &BTreeMap<Target, &'t str> {
|
||||
&self.names
|
||||
}
|
||||
pub fn edits(&self) -> &BTreeMap<OrdinaryKey, Target> {
|
||||
&self.edits
|
||||
}
|
||||
pub fn validate_current(&self, current: &SourceBundle) -> Result<(), Failure> {
|
||||
let old = self.source.bundle();
|
||||
if old.entry != current.entry || old.files != current.files {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(
|
||||
Span::default(),
|
||||
"Current complete bundle differs from the rename snapshot.",
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn failure(&self, file: Option<usize>, error: TypedError) -> Failure {
|
||||
Failure::index(
|
||||
&self.source.indexed.resolved.syntax,
|
||||
lower::Failure { file, error },
|
||||
)
|
||||
}
|
||||
fn validate(&self) -> Result<(), Failure> {
|
||||
let mut previous: Option<OrdinaryKey> = None;
|
||||
for (&key, target) in &self.edits {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
let checked = (|| {
|
||||
let unit = self
|
||||
.source
|
||||
.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.files
|
||||
.get(key.0)
|
||||
.ok_or_else(|| error(at, "Rename token has no original file."))?;
|
||||
let original = self
|
||||
.source
|
||||
.names()
|
||||
.get(target)
|
||||
.ok_or_else(|| error(at, "Rename token has no original target."))?;
|
||||
if !self.names.contains_key(target) {
|
||||
return Err(error(at, "Rename token targets an unchanged name."));
|
||||
}
|
||||
if at.start >= at.end {
|
||||
return Err(error(at, "Rename token must be nonempty."));
|
||||
}
|
||||
word(unit, at, original)?;
|
||||
if previous.is_some_and(|p| p.0 == key.0 && p.2 > key.1) {
|
||||
return Err(error(at, "Rename token ranges overlap."));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
checked.map_err(|e| self.failure(Some(key.0), e))?;
|
||||
previous = Some(key);
|
||||
}
|
||||
let mut required = 0;
|
||||
for (key, location) in self.source.typed_occurrences() {
|
||||
self.require(
|
||||
(location.file, location.span.start, location.span.end),
|
||||
key.target,
|
||||
&mut required,
|
||||
)?;
|
||||
}
|
||||
for (&key, occurrence) in self.source.ordinary_occurrences() {
|
||||
self.require(key, Target::Global(occurrence.target), &mut required)?;
|
||||
}
|
||||
if required != self.edits.len() {
|
||||
return Err(self.failure(
|
||||
None,
|
||||
error(Span::default(), "Rename edit set contains extra tokens."),
|
||||
));
|
||||
}
|
||||
self.output_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
fn require(
|
||||
&self,
|
||||
key: OrdinaryKey,
|
||||
target: Target,
|
||||
required: &mut usize,
|
||||
) -> Result<(), Failure> {
|
||||
if self.names.contains_key(&target) {
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
if self.edits.get(&key) != Some(&target) {
|
||||
return Err(self.failure(
|
||||
Some(key.0),
|
||||
error(at, "Rename edit set is missing a required target token."),
|
||||
));
|
||||
}
|
||||
*required = add(*required, 1, at).map_err(|e| self.failure(Some(key.0), e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn output_bytes(&self) -> Result<usize, Failure> {
|
||||
let result = (|| {
|
||||
let mut total = 0;
|
||||
let mut edits = self.edits.iter().peekable();
|
||||
for (file, _, original) in self.source.files() {
|
||||
let mut removed = 0;
|
||||
let mut replaced = 0;
|
||||
while let Some((&key, target)) = edits.peek().copied() {
|
||||
if key.0 != file {
|
||||
break;
|
||||
}
|
||||
let at = Span {
|
||||
start: key.1,
|
||||
end: key.2,
|
||||
};
|
||||
removed = add(
|
||||
removed,
|
||||
key.2
|
||||
.checked_sub(key.1)
|
||||
.ok_or_else(|| error(at, "Invalid rename token size."))?,
|
||||
at,
|
||||
)?;
|
||||
replaced = add(
|
||||
replaced,
|
||||
self.names
|
||||
.get(target)
|
||||
.ok_or_else(|| error(at, "Rename token has no replacement."))?
|
||||
.len(),
|
||||
at,
|
||||
)?;
|
||||
edits.next();
|
||||
}
|
||||
let size = original.len().checked_sub(removed).ok_or_else(|| {
|
||||
error(Span::default(), "Rename removals exceed original file.")
|
||||
})?;
|
||||
total = add(
|
||||
total,
|
||||
add(size, replaced, Span::default())?,
|
||||
Span::default(),
|
||||
)?;
|
||||
}
|
||||
if edits.next().is_some() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename edit file is outside original bundle.",
|
||||
));
|
||||
}
|
||||
self.source
|
||||
.indexed
|
||||
.resolved
|
||||
.syntax
|
||||
.budget
|
||||
.index_bytes(total, Span::default())?;
|
||||
Ok(total)
|
||||
})();
|
||||
result.map_err(|e| self.failure(None, e))
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustive exclusion patterns make every new field an explicit gate decision.
|
||||
macro_rules! fixed {
|
||||
($old:expr, $new:expr, $ty:path, [$($field:ident),*], [$($excluded:ident),*]) => {{
|
||||
let $ty { $($field,)* $($excluded: _,)* } = $old;
|
||||
true $(&& $field == &$new.$field)*
|
||||
}};
|
||||
}
|
||||
fn map_same<T>(
|
||||
old: &BTreeMap<Id, T>,
|
||||
new: &BTreeMap<Id, T>,
|
||||
same: impl Fn(&T, &T) -> bool,
|
||||
) -> bool {
|
||||
old.len() == new.len()
|
||||
&& old
|
||||
.iter()
|
||||
.zip(new)
|
||||
.all(|((a, x), (b, y))| a == b && same(x, y))
|
||||
}
|
||||
fn same_project(a: &Project, b: &Project) -> bool {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
Project,
|
||||
[schema_version, id, name, description],
|
||||
[
|
||||
alias,
|
||||
definitions,
|
||||
elements,
|
||||
relations,
|
||||
diagrams,
|
||||
artifact_links,
|
||||
traces
|
||||
]
|
||||
) && map_same(&a.definitions, &b.definitions, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
analysis::Definition,
|
||||
[id, name, description],
|
||||
[alias, body]
|
||||
) && match (&a.body, &b.body) {
|
||||
(
|
||||
analysis::DefinitionBody::Calculation(a),
|
||||
analysis::DefinitionBody::Calculation(b),
|
||||
) => {
|
||||
a.parameters.len() == b.parameters.len()
|
||||
&& a.parameters
|
||||
.iter()
|
||||
.zip(&b.parameters)
|
||||
.all(|(a, b)| fixed!(a, b, analysis::Parameter, [id, value_type], [name]))
|
||||
&& fixed!(
|
||||
a,
|
||||
b,
|
||||
analysis::CalculationDefinition,
|
||||
[result, body],
|
||||
[parameters]
|
||||
)
|
||||
}
|
||||
(
|
||||
analysis::DefinitionBody::QuantityKind(a),
|
||||
analysis::DefinitionBody::QuantityKind(b),
|
||||
) => a == b,
|
||||
(analysis::DefinitionBody::Unit(a), analysis::DefinitionBody::Unit(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}) && map_same(&a.elements, &b.elements, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Element,
|
||||
[id, name, description, parent, body],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.relations, &b.relations, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Relation,
|
||||
[id, name, description, body],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.diagrams, &b.diagrams, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Diagram,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
layer,
|
||||
projection,
|
||||
view_options,
|
||||
routes
|
||||
],
|
||||
[alias, nodes]
|
||||
) && map_same(&a.nodes, &b.nodes, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::NodePlacement,
|
||||
[id, element, position, size, collapsed, port_anchors],
|
||||
[alias]
|
||||
)
|
||||
})
|
||||
}) && map_same(&a.artifact_links, &b.artifact_links, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::ArtifactLink,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
owner,
|
||||
relation,
|
||||
uri,
|
||||
revision,
|
||||
fragment
|
||||
],
|
||||
[alias]
|
||||
)
|
||||
}) && map_same(&a.traces, &b.traces, |a, b| {
|
||||
fixed!(
|
||||
a,
|
||||
b,
|
||||
crate::Trace,
|
||||
[id, name, description, oa, sa, kind, baseline],
|
||||
[alias]
|
||||
)
|
||||
})
|
||||
}
|
||||
fn visit_names<'t>(
|
||||
project: &'t Project,
|
||||
f: &mut impl FnMut(Target, &'t str) -> Result<(), TypedError>,
|
||||
) -> Result<(), TypedError> {
|
||||
f(Target::Global(project.id), &project.alias)?;
|
||||
for d in project.definitions.values() {
|
||||
f(Target::Global(d.id), &d.alias)?;
|
||||
if let analysis::DefinitionBody::Calculation(c) = &d.body {
|
||||
for p in &c.parameters {
|
||||
f(
|
||||
Target::Parameter {
|
||||
calculation: d.id,
|
||||
parameter: p.id,
|
||||
},
|
||||
&p.name,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in project.elements.values() {
|
||||
f(Target::Global(e.id), &e.alias)?;
|
||||
}
|
||||
for r in project.relations.values() {
|
||||
f(Target::Global(r.id), &r.alias)?;
|
||||
}
|
||||
for d in project.diagrams.values() {
|
||||
f(Target::Global(d.id), &d.alias)?;
|
||||
for n in d.nodes.values() {
|
||||
f(Target::Global(n.id), &n.alias)?;
|
||||
}
|
||||
}
|
||||
for a in project.artifact_links.values() {
|
||||
f(Target::Global(a.id), &a.alias)?;
|
||||
}
|
||||
for t in project.traces.values() {
|
||||
f(Target::Global(t.id), &t.alias)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+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
|
||||
}
|
||||
);
|
||||
}
|
||||
+982
@@ -0,0 +1,982 @@
|
||||
use super::super::super::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
use super::*;
|
||||
use crate::language::analysis_source::budget::Limits;
|
||||
use crate::language::analysis_source::mixed::parse_with_profile;
|
||||
use crate::source_project::ProjectLimits;
|
||||
use crate::{Layer, analysis as a};
|
||||
|
||||
fn authored(bundle: &SourceBundle) -> AuthoredMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap().index_ordinary().unwrap()
|
||||
}
|
||||
fn calculation(p: &mut Project, n: usize) -> &mut a::CalculationDefinition {
|
||||
let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(n)).unwrap().body else {
|
||||
panic!("calculation")
|
||||
};
|
||||
c
|
||||
}
|
||||
fn renamed(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")] {
|
||||
p.definitions.get_mut(&id(n)).unwrap().alias = name.into();
|
||||
let c = calculation(&mut p, n);
|
||||
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
|
||||
}
|
||||
fn rejected(bundle: &SourceBundle, desired: &Project, contains: &str) {
|
||||
let e = match authored(bundle).plan_rename(desired) {
|
||||
Ok(_) => panic!("unexpected accepted target"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics.len(), 1);
|
||||
assert!(
|
||||
e.diagnostics[0].diagnostic.message.contains(contains),
|
||||
"{:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn noop_borrows_exact_target_and_preserves_source_usage_and_storage() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let source = authored(&bundle);
|
||||
let desired = rich_model(Layer::Oa);
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let numeric = source.indexed.resolved.syntax.budget.numeric_units();
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert!(std::ptr::eq(plan.desired(), &desired));
|
||||
assert!(std::ptr::eq(plan.source().bundle(), &bundle));
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(plan.changed_names().is_empty());
|
||||
assert_eq!(plan.source.indexed.resolved.storage.live, before);
|
||||
assert_eq!(plan.source.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(desired, rich_model(Layer::Oa));
|
||||
assert_eq!(plan.source.project(), &desired);
|
||||
assert_eq!(plan.output_bytes().unwrap(), 900);
|
||||
}
|
||||
#[test]
|
||||
fn final_names_reject_unchanged_collisions_invalid_names_and_scoped_collisions() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
for alias in ["w", "", "雨", "project", "bad-name"] {
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = alias.into();
|
||||
rejected(
|
||||
&bundle,
|
||||
&desired,
|
||||
if alias == "w" { "collide" } else { "Invalid" },
|
||||
);
|
||||
}
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
calculation(&mut desired, 10).parameters[0].name = "y".into();
|
||||
rejected(&bundle, &desired, "collide");
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
calculation(&mut desired, 10).parameters[0].name = "p".into();
|
||||
calculation(&mut desired, 11).parameters[0].name = "p".into();
|
||||
let plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
assert_eq!(plan.changed_names().len(), 2);
|
||||
}
|
||||
#[test]
|
||||
fn complete_model_gate_rejects_identity_collection_parameter_and_payload_changes() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
type Mutation = Box<dyn Fn(&mut Project)>;
|
||||
let mutations: Vec<Mutation> = vec![
|
||||
Box::new(|p| p.schema_version = 7),
|
||||
Box::new(|p| p.id = id(99)),
|
||||
Box::new(|p| p.name.push('!')),
|
||||
Box::new(|p| p.description.push('!')),
|
||||
Box::new(|p| {
|
||||
p.elements.remove(&id(2));
|
||||
}),
|
||||
Box::new(|p| {
|
||||
let e = p.elements.remove(&id(2)).unwrap();
|
||||
p.elements.insert(id(99), e);
|
||||
}),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.elements.get_mut(&id(2)).unwrap().parent = Some(id(3))),
|
||||
Box::new(|p| {
|
||||
p.elements.get_mut(&id(2)).unwrap().body =
|
||||
crate::ElementBody::Sa(crate::SaBody::Function)
|
||||
}),
|
||||
Box::new(|p| p.definitions.get_mut(&id(10)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.definitions.get_mut(&id(10)).unwrap().name.push('!')),
|
||||
Box::new(|p| {
|
||||
p.definitions
|
||||
.get_mut(&id(10))
|
||||
.unwrap()
|
||||
.description
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).parameters.swap(0, 1)),
|
||||
Box::new(|p| {
|
||||
calculation(p, 10).parameters.pop();
|
||||
}),
|
||||
Box::new(|p| {
|
||||
let q = calculation(p, 10).parameters[0].clone();
|
||||
calculation(p, 10).parameters.push(q);
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).parameters[0].id = a::ParameterId(id(99))),
|
||||
Box::new(|p| {
|
||||
calculation(p, 10).parameters[0].value_type =
|
||||
a::AnalysisType::Scalar(a::ScalarType::Boolean)
|
||||
}),
|
||||
Box::new(|p| calculation(p, 10).result = a::AnalysisType::Scalar(a::ScalarType::Boolean)),
|
||||
Box::new(|p| calculation(p, 10).body = a::Binding::Unbound),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(5)).unwrap().layer = Layer::Sa),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(id(99))
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams.get_mut(&id(5)).unwrap().nodes.clear();
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.id = id(99)
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.element = id(2)
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = 1.
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.size
|
||||
.width = 1.
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.collapsed = true
|
||||
}),
|
||||
];
|
||||
for mutate in mutations {
|
||||
let mut p = rich_model(Layer::Oa);
|
||||
mutate(&mut p);
|
||||
rejected(&bundle, &p, "non-name");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn complete_current_bundle_rejects_entry_paths_missing_extra_and_untouched_bytes() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "renamed".into();
|
||||
let plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.validate_current(&bundle).unwrap();
|
||||
for case in 0..6 {
|
||||
let mut current = bundle.clone();
|
||||
match case {
|
||||
0 => current.entry = "a.arcadia".into(),
|
||||
1 => {
|
||||
current.files.remove("a.arcadia");
|
||||
}
|
||||
2 => {
|
||||
current.files.insert("extra.arcadia".into(), String::new());
|
||||
}
|
||||
3 => current.files.get_mut("a.arcadia").unwrap().push(' '),
|
||||
4 => current.files.get_mut("main.arcadia").unwrap().push(' '),
|
||||
_ => {
|
||||
let s = current.files.remove("a.arcadia").unwrap();
|
||||
current.files.insert("renamed.arcadia".into(), s);
|
||||
}
|
||||
}
|
||||
assert!(plan.validate_current(¤t).is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn exact_merged_map_cap_passes_all_predecessors_and_retains_r_plus_m() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let desired = renamed(Layer::Oa);
|
||||
let baseline = authored(&bundle).indexed.resolved.storage.live;
|
||||
for (extra, fits) in [(37, false), (38, true)] {
|
||||
let profile = Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
assert_eq!(source.indexed.resolved.storage.live, baseline);
|
||||
let result = source.plan_rename(&desired);
|
||||
if fits {
|
||||
let plan = result.unwrap();
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
baseline.add(Slots::entries(38), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("cap accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 412,
|
||||
end: 413
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn damaged_complete_edits_fail_missing_extra_wrong_target_and_utf8_authority() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
for s in bundle.files.values_mut() {
|
||||
*s = format!("// 雨\r\n{s}");
|
||||
}
|
||||
let desired = renamed(Layer::Oa);
|
||||
for case in 0..7 {
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
let key = *plan.edits.keys().next().unwrap();
|
||||
match case {
|
||||
0 => {
|
||||
plan.edits.remove(&key);
|
||||
}
|
||||
1 => {
|
||||
plan.edits.insert((0, 0, 1), Target::Global(id(10)));
|
||||
}
|
||||
2 => {
|
||||
plan.edits.insert(key, Target::Global(id(11)));
|
||||
}
|
||||
3 => {
|
||||
plan.edits.insert((0, 4, 5), Target::Global(id(10)));
|
||||
}
|
||||
4 => {
|
||||
plan.edits
|
||||
.insert((99, key.1, key.2), Target::Global(id(10)));
|
||||
}
|
||||
5 => {
|
||||
plan.edits
|
||||
.insert((0, key.1, key.2 + 1), Target::Global(id(10)));
|
||||
}
|
||||
_ => {
|
||||
plan.edits.insert((0, key.1, key.1), Target::Global(id(10)));
|
||||
}
|
||||
}
|
||||
let failure = plan.validate().unwrap_err();
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
if case == 3 || case == 4 {
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].path,
|
||||
if case == 3 {
|
||||
"a.arcadia"
|
||||
} else {
|
||||
"main.arcadia"
|
||||
}
|
||||
);
|
||||
}
|
||||
if case == 0 {
|
||||
assert_eq!(failure.diagnostics[0].path, "a.arcadia");
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span,
|
||||
Span { start: 20, end: 21 }
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
"Rename edit set is missing a required target token."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn categories() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([(
|
||||
"main.arcadia".into(),
|
||||
include_str!("../tests/fixtures/categories.arcadia").into(),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn every_ordinary_record_field_group_is_fixed_including_historical_payloads() {
|
||||
let bundle = categories();
|
||||
let original = authored(&bundle).project().clone();
|
||||
type Mutation = Box<dyn Fn(&mut Project)>;
|
||||
let changes: Vec<Mutation> = vec![
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.relations.get_mut(&id(6)).unwrap().description.push('!')),
|
||||
Box::new(|p| {
|
||||
let body = p.relations[&id(15)].body.clone();
|
||||
p.relations.get_mut(&id(6)).unwrap().body = body;
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.relations.remove(&id(6));
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.diagrams.get_mut(&id(7)).unwrap().projection = crate::ProjectionKind::Allocation
|
||||
}),
|
||||
Box::new(|p| p.diagrams.get_mut(&id(7)).unwrap().routes.clear()),
|
||||
Box::new(|p| {
|
||||
p.diagrams.remove(&id(7));
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().name.push('!')),
|
||||
Box::new(|p| {
|
||||
p.artifact_links
|
||||
.get_mut(&id(8))
|
||||
.unwrap()
|
||||
.description
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().owner = id(3)),
|
||||
Box::new(|p| {
|
||||
p.artifact_links.get_mut(&id(8)).unwrap().relation = crate::ArtifactRelation::Verifies
|
||||
}),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().uri.push('!')),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().revision = Some("changed".into())),
|
||||
Box::new(|p| p.artifact_links.get_mut(&id(8)).unwrap().fragment = Some("changed".into())),
|
||||
Box::new(|p| {
|
||||
p.artifact_links.remove(&id(8));
|
||||
}),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().id = id(99)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().name.push('!')),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().description.push('!')),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().oa = id(4)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().sa = id(4)),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().kind = crate::TraceKind::Refinement),
|
||||
Box::new(|p| p.traces.get_mut(&id(9)).unwrap().baseline = None),
|
||||
Box::new(|p| {
|
||||
p.traces
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = None
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.traces
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.oa
|
||||
.name
|
||||
.push('!')
|
||||
}),
|
||||
Box::new(|p| {
|
||||
p.traces.remove(&id(9));
|
||||
}),
|
||||
];
|
||||
for change in changes {
|
||||
let mut p = original.clone();
|
||||
change(&mut p);
|
||||
rejected(&bundle, &p, "non-name");
|
||||
}
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
drop(source.plan_rename(&original).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn combined_shrink_offsets_typed_growth_and_counts_untouched_files() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main.replace("node n ", &format!("node {} ", "n".repeat(31)));
|
||||
assert_eq!(bundle.files.values().map(String::len).sum::<usize>(), 930);
|
||||
for (name, expected) in [("renamed_f", Some(924)), ("renamed_func", None)] {
|
||||
let profile = Limits {
|
||||
bytes: 930,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
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 = name.into();
|
||||
let result = source.plan_rename(&desired);
|
||||
if let Some(bytes) = expected {
|
||||
let plan = result.unwrap();
|
||||
assert_eq!(plan.output_bytes().unwrap(), bytes);
|
||||
assert_eq!(plan.edits().len(), 4);
|
||||
} else {
|
||||
let e = match result {
|
||||
Ok(_) => panic!("oversize accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn deep_and_wide_equality_and_late_refusal_drop_on_small_stack() {
|
||||
for (expression, result, nodes) in [
|
||||
(
|
||||
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
|
||||
"Number",
|
||||
128,
|
||||
),
|
||||
(
|
||||
format!("[{}]", vec!["1"; 128].join(", ")),
|
||||
"List(Number)",
|
||||
129,
|
||||
),
|
||||
] {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}",
|
||||
id(2)
|
||||
));
|
||||
for mode in 0..3 {
|
||||
// Admission/intent construction are prerequisites, outside the
|
||||
// bounded planner worker. No limits or live owner state are reset.
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
|
||||
let mut desired = source.project().clone();
|
||||
match mode {
|
||||
0 => {}
|
||||
1 => desired.definitions.get_mut(&id(2)).unwrap().alias = "p".into(),
|
||||
_ => calculation(&mut desired, 2).body = a::Binding::Unbound,
|
||||
}
|
||||
std::thread::scope(|scope| {
|
||||
let desired = &desired;
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn_scoped(scope, move || {
|
||||
// The complete owner moves into this worker; successful
|
||||
// plans and consumed failure owners die here. Nothing
|
||||
// carrying source/edit authority escapes the worker.
|
||||
match source.plan_rename(desired) {
|
||||
Ok(plan) => {
|
||||
assert_eq!(mode, 0, "mutation unexpectedly accepted");
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(std::ptr::eq(plan.desired(), desired));
|
||||
drop(plan);
|
||||
}
|
||||
Err(failure) => {
|
||||
assert_ne!(mode, 0, "unchanged target unexpectedly refused");
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.message,
|
||||
if mode == 1 {
|
||||
"Rename aliases collide in their final namespace."
|
||||
} else {
|
||||
"Rename target changes non-name project semantics."
|
||||
}
|
||||
);
|
||||
drop(failure);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
drop(desired);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn zero_expression_original_rejects_target_only_deep_and_wide_variants() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}",
|
||||
id(2)
|
||||
));
|
||||
let source = authored(&bundle);
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
let original = source.project().clone();
|
||||
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 mut desired = original.clone();
|
||||
calculation(&mut desired, 2).body = a::Binding::Expression(expression);
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn signed_zero_geometry_uses_existing_semantic_equality() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.;
|
||||
assert!(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.edits()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
fn parameter(c: usize, p: usize) -> Target {
|
||||
Target::Parameter {
|
||||
calculation: id(c),
|
||||
parameter: a::ParameterId(id(p)),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_entire_rich_maps_oa_sa_unicode_crlf_and_borrowed_names() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for crlf in [false, true] {
|
||||
let mut bundle = rich_bundle(layer);
|
||||
if crlf {
|
||||
for source in bundle.files.values_mut() {
|
||||
*source = format!("// 雨\r\n{}", source.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let desired = renamed(layer);
|
||||
let source = authored(&bundle);
|
||||
let before = source.indexed.resolved.storage.live;
|
||||
let usage = source.indexed.resolved.syntax.budget.usage;
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
let mut names = BTreeMap::from([
|
||||
(Target::Global(id(1)), "w"),
|
||||
(Target::Global(id(2)), "p"),
|
||||
(Target::Global(id(3)), "n"),
|
||||
(Target::Global(id(4)), "v"),
|
||||
(Target::Global(id(5)), "z"),
|
||||
(Target::Global(id(10)), "g"),
|
||||
(Target::Global(id(11)), "f"),
|
||||
(parameter(10, 1), "y"),
|
||||
(parameter(10, 12), "x"),
|
||||
(parameter(11, 1), "y"),
|
||||
(parameter(11, 12), "x"),
|
||||
]);
|
||||
if layer == Layer::Sa {
|
||||
names.insert(Target::Global(id(6)), "u");
|
||||
}
|
||||
let mut edits = BTreeMap::new();
|
||||
let offsets = if crlf {
|
||||
[20, 77, 138, 214, 219]
|
||||
} else {
|
||||
[12, 68, 128, 203, 208]
|
||||
};
|
||||
for (file, calculation) in [(0, 10), (1, 11)] {
|
||||
for (start, target) in offsets.into_iter().zip([
|
||||
Target::Global(id(calculation)),
|
||||
parameter(calculation, 1),
|
||||
parameter(calculation, 12),
|
||||
parameter(calculation, 1),
|
||||
parameter(calculation, 12),
|
||||
]) {
|
||||
edits.insert((file, start, start + 1), target);
|
||||
}
|
||||
}
|
||||
let offsets = if crlf {
|
||||
[
|
||||
28, 133, 188, 200, 218, 281, 303, 305, 311, 315, 317, 323, 333, 335, 341, 363,
|
||||
426,
|
||||
]
|
||||
} else {
|
||||
[
|
||||
19, 122, 177, 189, 206, 269, 291, 293, 299, 303, 305, 311, 321, 323, 329, 350,
|
||||
412,
|
||||
]
|
||||
};
|
||||
let g = |n| Target::Global(id(n));
|
||||
for (start, target) in offsets.into_iter().zip([
|
||||
g(1),
|
||||
g(4),
|
||||
g(5),
|
||||
g(3),
|
||||
g(3),
|
||||
g(2),
|
||||
g(10),
|
||||
parameter(10, 12),
|
||||
parameter(10, 1),
|
||||
g(11),
|
||||
parameter(11, 1),
|
||||
parameter(11, 12),
|
||||
g(10),
|
||||
parameter(10, 1),
|
||||
parameter(10, 12),
|
||||
g(2),
|
||||
g(5),
|
||||
]) {
|
||||
edits.insert((2, start, start + 1), target);
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
let start = if crlf { 505 } else { 490 };
|
||||
edits.insert((2, start, start + 1), g(6));
|
||||
}
|
||||
assert_eq!(plan.changed_names(), &names);
|
||||
assert_eq!(plan.edits(), &edits);
|
||||
let delta = if layer == Layer::Oa { 38 } else { 40 };
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(delta), Span::default()).unwrap()
|
||||
);
|
||||
assert_eq!(plan.source.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert!(std::ptr::eq(plan.desired(), &desired));
|
||||
assert_eq!(desired, renamed(layer));
|
||||
assert_eq!(plan.source.project(), &rich_model(layer));
|
||||
assert!(std::ptr::eq(
|
||||
plan.names[&g(1)].as_ptr(),
|
||||
desired.alias.as_ptr()
|
||||
));
|
||||
assert_eq!(
|
||||
plan.output_bytes().unwrap(),
|
||||
match (layer, crlf) {
|
||||
(Layer::Oa, false) => 900,
|
||||
(Layer::Oa, true) => 943,
|
||||
(Layer::Sa, false) => 974,
|
||||
(Layer::Sa, true) => 1017,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_entire_category_map_includes_comment_separated_refs_and_excludes_history() {
|
||||
let bundle = categories();
|
||||
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 = replacements[0].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 before = source.indexed.resolved.storage.live;
|
||||
assert_eq!(source.indexed.resolved.expressions.len(), 0);
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
let spans: &[(usize, &[(usize, usize)])] = &[
|
||||
(1, &[(19, 20)]),
|
||||
(2, &[(168, 169), (344, 345)]),
|
||||
(3, &[(249, 254), (625, 630), (992, 997), (1741, 1746)]),
|
||||
(4, &[(369, 377), (640, 648), (1882, 1890)]),
|
||||
(5, &[(487, 488), (1003, 1004)]),
|
||||
(6, &[(561, 565), (741, 745), (789, 793), (885, 889)]),
|
||||
(7, &[(664, 665)]),
|
||||
(8, &[(818, 822)]),
|
||||
(9, &[(925, 926)]),
|
||||
(
|
||||
10,
|
||||
&[(1328, 1329), (1577, 1578), (1855, 1856), (2004, 2005)],
|
||||
),
|
||||
(11, &[(1422, 1425), (1617, 1620), (1712, 1715)]),
|
||||
(12, &[(1391, 1394), (1514, 1517), (2029, 2032)]),
|
||||
(13, &[(1487, 1491), (1648, 1652), (2047, 2051)]),
|
||||
(14, &[(1397, 1402), (1789, 1794), (2092, 2097)]),
|
||||
(15, &[(1935, 1942)]),
|
||||
];
|
||||
let expected: BTreeMap<_, _> = spans
|
||||
.iter()
|
||||
.flat_map(|(n, spans)| {
|
||||
spans
|
||||
.iter()
|
||||
.map(move |&(start, end)| ((0, start, end), Target::Global(id(*n))))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(plan.edits(), &expected);
|
||||
let names: BTreeMap<_, _> = replacements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &name)| (Target::Global(id(i + 1)), name))
|
||||
.collect();
|
||||
assert_eq!(plan.changed_names(), &names);
|
||||
assert_eq!(plan.output_bytes().unwrap(), 2145);
|
||||
assert_eq!(
|
||||
plan.source.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(51), Span::default()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_zero_scalar_bounds_defaults_and_historical_facets_preserve_original_bytes() {
|
||||
let bundle = 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(&bundle);
|
||||
let mut desired = source.project().clone();
|
||||
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 real")
|
||||
};
|
||||
review.oa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
review.sa = crate::semantic::Observed::Value(Some(-0.0));
|
||||
let plan = source.plan_rename(&desired).unwrap();
|
||||
assert!(plan.edits().is_empty());
|
||||
assert!(std::ptr::eq(plan.source.bundle(), &bundle));
|
||||
}
|
||||
#[test]
|
||||
fn nonzero_expression_lists_calls_and_definition_payloads_reject_changes() {
|
||||
let bundle = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result List(Number); body [1,2]; }}",
|
||||
id(2)
|
||||
));
|
||||
let original = authored(&bundle).project().clone();
|
||||
for bigger in [false, true] {
|
||||
let mut desired = original.clone();
|
||||
let a::Binding::Expression(a::Expr::List(items)) = &mut calculation(&mut desired, 2).body
|
||||
else {
|
||||
panic!("list")
|
||||
};
|
||||
if bigger {
|
||||
items.push(a::Expr::Boolean(true));
|
||||
} else {
|
||||
items.pop();
|
||||
}
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
}
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
let crate::ElementBody::Oa(crate::OaBody::Shared(crate::SharedBody::EngineeringProperty {
|
||||
base: a::Binding::Expression(a::Expr::Binary { left, .. }),
|
||||
..
|
||||
})) = &mut desired.elements.get_mut(&id(3)).unwrap().body
|
||||
else {
|
||||
panic!("property")
|
||||
};
|
||||
let a::Expr::Call { arguments, .. } = left.as_mut() else {
|
||||
panic!("call")
|
||||
};
|
||||
arguments.remove(&a::ParameterId(id(1)));
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired
|
||||
.diagrams
|
||||
.get_mut(&id(5))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.port_anchors
|
||||
.push(crate::PortAnchor {
|
||||
port: id(99),
|
||||
side: crate::PortAnchorSide::Left,
|
||||
offset: 0.,
|
||||
});
|
||||
rejected(&bundle, &desired, "non-name");
|
||||
// Opaque quantity/unit payload comparison is tested directly, independently
|
||||
// of admission's semantic dimension rules; these are gate-level controls.
|
||||
let mut old = rich_model(Layer::Oa);
|
||||
old.definitions.get_mut(&id(10)).unwrap().body =
|
||||
a::DefinitionBody::QuantityKind(a::QuantityKindDefinition {
|
||||
dimension: crate::analysis_numeric::Dimension::ZERO,
|
||||
algebra: a::QuantityAlgebra::Linear,
|
||||
});
|
||||
let mut changed = old.clone();
|
||||
let a::DefinitionBody::QuantityKind(q) =
|
||||
&mut changed.definitions.get_mut(&id(10)).unwrap().body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
q.algebra = a::QuantityAlgebra::Affine {
|
||||
difference: a::DefinitionRef::Local(id(11)),
|
||||
};
|
||||
assert!(!same_project(&old, &changed));
|
||||
old.definitions.get_mut(&id(10)).unwrap().body = a::DefinitionBody::Unit(a::UnitDefinition {
|
||||
kind: a::DefinitionRef::Local(id(11)),
|
||||
scale: crate::analysis_numeric::Exact::from_integer(1),
|
||||
offset: crate::analysis_numeric::Exact::from_integer(0),
|
||||
symbol: "u".into(),
|
||||
});
|
||||
let mut changed = old.clone();
|
||||
let a::DefinitionBody::Unit(u) = &mut changed.definitions.get_mut(&id(10)).unwrap().body else {
|
||||
unreachable!()
|
||||
};
|
||||
u.symbol.push('x');
|
||||
assert!(!same_project(&old, &changed));
|
||||
assert!(!same_project(&old, &rich_model(Layer::Oa)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_spelling_different_callee_and_unchanged_target_corruptions_are_rejected() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let desired = renamed(Layer::Oa);
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.insert((2, 299, 300), parameter(11, 1));
|
||||
let e = plan.validate().unwrap_err();
|
||||
assert_eq!(e.diagnostics[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 299,
|
||||
end: 300
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Rename edit set is missing a required target token."
|
||||
);
|
||||
let mut desired = rich_model(Layer::Oa);
|
||||
desired.alias = "new_project".into();
|
||||
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
|
||||
plan.edits.insert((2, 122, 123), Target::Global(id(4)));
|
||||
let e = plan.validate().unwrap_err();
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 122,
|
||||
end: 123
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Rename token targets an unchanged name."
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn included_file_last_reservation_preserves_its_canonical_diagnostic() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let start = main.find("diagram d ").unwrap();
|
||||
let end = start + main[start..].find('\n').unwrap() + 1;
|
||||
let diagram = main[start..end].to_owned();
|
||||
main.replace_range(start..end, "include \"z.arcadia\";\n");
|
||||
bundle.files.insert("z.arcadia".into(), diagram);
|
||||
let baseline = authored(&bundle).indexed.resolved.storage.live;
|
||||
let desired = renamed(Layer::Oa);
|
||||
// The unchanged-profile pipeline must clear every predecessor. The final
|
||||
// ordinary header is now in the canonical last include, at d[8..9).
|
||||
let profile = Limits {
|
||||
entries: baseline.entries + 37,
|
||||
..Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let source = resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap()
|
||||
.index()
|
||||
.unwrap()
|
||||
.index_ordinary()
|
||||
.unwrap();
|
||||
let e = match source.plan_rename(&desired) {
|
||||
Ok(_) => panic!("cap accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(e.diagnostics[0].path, "z.arcadia");
|
||||
assert_eq!(e.diagnostics[0].diagnostic.span, Span { start: 8, end: 9 });
|
||||
assert_eq!(
|
||||
e.diagnostics[0].diagnostic.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
use super::super::tests::{accepted, id, rich_bundle, rich_model, single};
|
||||
use super::*;
|
||||
use crate::Layer;
|
||||
use crate::source_project::{ProjectLimits, SourceBundle};
|
||||
|
||||
fn indexed(bundle: &SourceBundle) -> IndexedMixedProject<'_> {
|
||||
accepted(bundle).index().unwrap()
|
||||
}
|
||||
fn span(start: usize, end: usize) -> Span {
|
||||
Span { start, end }
|
||||
}
|
||||
fn record(
|
||||
owner: usize,
|
||||
target: usize,
|
||||
role: OrdinaryRole,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> OrdinaryOccurrence {
|
||||
OrdinaryOccurrence {
|
||||
owner: id(owner),
|
||||
target: id(target),
|
||||
role,
|
||||
extent: span(start, end),
|
||||
}
|
||||
}
|
||||
fn fail(indexed: IndexedMixedProject<'_>, message: &str, path: &str) -> SourceDiagnostic {
|
||||
let failure = match indexed.index_ordinary() {
|
||||
Ok(_) => panic!("damaged ordinary index accepted"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics.len(), 1);
|
||||
let diagnostic = failure.diagnostics.into_iter().next().unwrap();
|
||||
assert_eq!(diagnostic.path, path);
|
||||
assert_eq!(diagnostic.diagnostic.message, message);
|
||||
diagnostic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entire_rich_oa_sa_and_unicode_crlf_maps_preserve_complete_authority() {
|
||||
use OrdinaryRole::{Header as H, Reference as R};
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for crlf in [false, true] {
|
||||
let mut bundle = rich_bundle(layer);
|
||||
if crlf {
|
||||
for source in bundle.files.values_mut() {
|
||||
*source = format!("// 雨\r\n{}", source.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let indexed = indexed(&bundle);
|
||||
let names = indexed.names().clone();
|
||||
let typed = indexed.occurrences().clone();
|
||||
let expressions = indexed.expressions().clone();
|
||||
let warnings = indexed.warnings().to_vec();
|
||||
let limits = indexed.resolved.static_limits;
|
||||
let static_usage = indexed.resolved.static_usage;
|
||||
let usage = indexed.resolved.syntax.budget.usage;
|
||||
let numeric = indexed.resolved.syntax.budget.numeric_units();
|
||||
let before = indexed.resolved.storage.live;
|
||||
let complete = indexed.index_ordinary().unwrap();
|
||||
let endpoints = if crlf {
|
||||
[28, 133, 188, 200, 363, 426, 505]
|
||||
} else {
|
||||
[19, 122, 177, 189, 350, 412, 490]
|
||||
};
|
||||
let mut expected = BTreeMap::new();
|
||||
for (at, owner, target, role) in [
|
||||
(endpoints[0], 1, 1, H),
|
||||
(endpoints[1], 4, 4, H),
|
||||
(endpoints[2], 4, 5, R),
|
||||
(endpoints[3], 4, 3, R),
|
||||
(endpoints[4], 2, 2, H),
|
||||
(endpoints[5], 5, 5, H),
|
||||
] {
|
||||
expected.insert(
|
||||
(2, at, at + 1),
|
||||
record(
|
||||
owner,
|
||||
target,
|
||||
role,
|
||||
if role == R { at - 1 } else { at },
|
||||
at + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
let at = endpoints[6];
|
||||
expected.insert((2, at, at + 1), record(6, 6, H, at, at + 1));
|
||||
}
|
||||
assert_eq!(complete.ordinary_occurrences(), &expected);
|
||||
assert_eq!(complete.project(), &rich_model(layer));
|
||||
assert!(std::ptr::eq(complete.bundle(), &bundle));
|
||||
assert_eq!(
|
||||
complete.files().collect::<Vec<_>>(),
|
||||
bundle
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (p, s))| (i, p.as_str(), s.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(complete.names(), &names);
|
||||
assert_eq!(complete.typed_occurrences(), &typed);
|
||||
assert_eq!(complete.expressions(), &expressions);
|
||||
assert_eq!(complete.warnings(), &warnings);
|
||||
assert_eq!(complete.indexed.resolved.static_limits, limits);
|
||||
assert_eq!(complete.indexed.resolved.static_usage, static_usage);
|
||||
assert_eq!(complete.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.storage.live,
|
||||
before
|
||||
.add(Slots::entries(expected.len()), Span::default())
|
||||
.unwrap()
|
||||
);
|
||||
let mut headers: BTreeSet<_> = complete
|
||||
.ordinary
|
||||
.values()
|
||||
.filter(|o| o.role == H)
|
||||
.map(|o| o.target)
|
||||
.collect();
|
||||
headers.extend(
|
||||
complete
|
||||
.typed_occurrences()
|
||||
.keys()
|
||||
.filter(|k| k.role == index::TokenRole::Declaration)
|
||||
.map(|k| k.owner),
|
||||
);
|
||||
assert_eq!(
|
||||
headers,
|
||||
complete
|
||||
.names()
|
||||
.keys()
|
||||
.filter_map(|t| if let lower::Target::Global(id) = t {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_file_keys_distinguish_equal_header_offsets_and_exclude_include_prose() {
|
||||
let mut bundle = single("include \"b.arcadia\"; include \"a.arcadia\";");
|
||||
for (path, alias, n) in [("a.arcadia", "aa", 2), ("b.arcadia", "bb", 3)] {
|
||||
bundle.files.insert(
|
||||
path.into(),
|
||||
format!(
|
||||
"oa_activity {alias} \"Same @aa @bb\" id \"{}\" {{ {} }}",
|
||||
id(n),
|
||||
if n == 3 { "parent @aa;" } else { "" }
|
||||
),
|
||||
);
|
||||
}
|
||||
let complete = indexed(&bundle).index_ordinary().unwrap();
|
||||
assert_eq!(
|
||||
complete.ordinary,
|
||||
BTreeMap::from([
|
||||
((0, 12, 14), record(2, 2, OrdinaryRole::Header, 12, 14)),
|
||||
((1, 12, 14), record(3, 3, OrdinaryRole::Header, 12, 14)),
|
||||
((1, 82, 84), record(3, 2, OrdinaryRole::Reference, 81, 84)),
|
||||
((2, 19, 20), record(1, 1, OrdinaryRole::Header, 19, 20)),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
fn rich_with_extra_headers() -> SourceBundle {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let source = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
let at = source.rfind('}').unwrap();
|
||||
let extra = (20..26)
|
||||
.map(|n| format!("oa_activity extra{n} \"Extra\" id \"{}\" {{}}\n", id(n)))
|
||||
.collect::<String>();
|
||||
source.insert_str(at, &extra);
|
||||
bundle
|
||||
}
|
||||
#[test]
|
||||
fn exact_ordinary_peak_after_typed_peak_preserves_the_inherited_ledger() {
|
||||
let bundle = rich_with_extra_headers();
|
||||
let baseline = indexed(&bundle).resolved.storage.live;
|
||||
// G13 + T21 + N12 =46, final ordinary header. Six extra leaf activities
|
||||
// add six retained warnings and eighteen lower-catalog slots. The prior
|
||||
// passing resolver cap97 therefore needs at most109; this stage needs111.
|
||||
for (extra, fits) in [(45, false), (46, true)] {
|
||||
let profile = super::super::super::super::budget::Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed =
|
||||
super::super::super::parse_with_profile(&bundle, ProjectLimits::default(), profile)
|
||||
.unwrap();
|
||||
let indexed = resolve(parsed).unwrap().index().unwrap();
|
||||
assert_eq!(indexed.resolved.storage.live, baseline);
|
||||
if fits {
|
||||
assert_eq!(
|
||||
indexed
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
.indexed
|
||||
.resolved
|
||||
.storage
|
||||
.live,
|
||||
baseline.add(Slots::entries(12), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let diagnostic = fail(
|
||||
indexed,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let at = bundle.files["main.arcadia"].find("extra25").unwrap();
|
||||
assert_eq!(diagnostic.diagnostic.span, span(at, at + 7));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn node_field<'a>(
|
||||
indexed: &'a mut IndexedMixedProject<'_>,
|
||||
name: &str,
|
||||
) -> &'a mut language::EditField {
|
||||
let EditKind::Record(fields) = &mut indexed.resolved.syntax.files[2].positions[0].1.fields.kind
|
||||
else {
|
||||
panic!("node fields")
|
||||
};
|
||||
fields.get_mut(name).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_corruptions_reach_owner_header_marker_extent_and_spelling_checks() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
for case in 0..12 {
|
||||
let mut indexed = indexed(&bundle);
|
||||
let message = match case {
|
||||
0 => {
|
||||
indexed.resolved.syntax.files[2].positions[0].0 = id(3);
|
||||
"Ordinary position differs from its declaration owner."
|
||||
}
|
||||
1 => {
|
||||
indexed.resolved.syntax.files[2].positions[0]
|
||||
.1
|
||||
.lexical_owner = Some(id(2));
|
||||
"Ordinary position differs from its declaration owner."
|
||||
}
|
||||
2 => {
|
||||
indexed.resolved.syntax.files[2].positions[0]
|
||||
.1
|
||||
.header
|
||||
.insert("alias", span(177, 178));
|
||||
"Ordinary alias is not its declaration header token."
|
||||
}
|
||||
3 => {
|
||||
indexed.resolved.syntax.files[2]
|
||||
.ordinary
|
||||
.header
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.project
|
||||
.id = id(2);
|
||||
"Ordinary header differs from its global identity."
|
||||
}
|
||||
4 => {
|
||||
indexed.resolved.syntax.files[2].positions[0]
|
||||
.1
|
||||
.fields
|
||||
.span
|
||||
.start += 1;
|
||||
"Ordinary synthetic root differs from its body braces."
|
||||
}
|
||||
5 => {
|
||||
node_field(&mut indexed, "diagram").value.span.start = 177;
|
||||
"Ordinary reference lacks its original marker token."
|
||||
}
|
||||
6 => {
|
||||
node_field(&mut indexed, "diagram").value.span.end = 177;
|
||||
"Ordinary reference extent differs from its complete alias token."
|
||||
}
|
||||
7 => {
|
||||
node_field(&mut indexed, "diagram").value.span.end = 179;
|
||||
"Ordinary reference extent differs from its complete alias token."
|
||||
}
|
||||
8 => {
|
||||
node_field(&mut indexed, "diagram").value.kind =
|
||||
EditKind::Reference("other".into());
|
||||
"Ordinary alias differs from its retained spelling."
|
||||
}
|
||||
9 => {
|
||||
let token = indexed.resolved.syntax.files[2]
|
||||
.tokens
|
||||
.iter_mut()
|
||||
.find(|t| t.span.start == 177)
|
||||
.unwrap();
|
||||
token.class = lex::Class::String;
|
||||
"Ordinary alias is not an original whole Word token."
|
||||
}
|
||||
10 => {
|
||||
node_field(&mut indexed, "diagram").span.start = 0;
|
||||
"Ordinary span crosses its containing syntax."
|
||||
}
|
||||
_ => {
|
||||
indexed.data.names.remove(&lower::Target::Global(id(5)));
|
||||
"Ordinary reference has no global target."
|
||||
}
|
||||
};
|
||||
fail(indexed, message, "main.arcadia");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_header_and_duplicate_physical_ownership_are_rejected() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut missing = indexed(&bundle);
|
||||
missing.resolved.syntax.files[2]
|
||||
.ordinary
|
||||
.declarations
|
||||
.remove(1);
|
||||
missing.resolved.syntax.files[2].positions.remove(1);
|
||||
let diagnostic = fail(
|
||||
missing,
|
||||
"Global name is missing its authored declaration header.",
|
||||
"main.arcadia",
|
||||
);
|
||||
assert_eq!(diagnostic.diagnostic.span, Span::default());
|
||||
let mut duplicate = indexed(&bundle);
|
||||
// A second field position points at the same full reference. Parser emission
|
||||
// cannot produce this; the control isolates ordinary physical ownership.
|
||||
let field = node_field(&mut duplicate, "element");
|
||||
field.span = span(168, 179);
|
||||
field.value.span = span(176, 178);
|
||||
field.value.kind = EditKind::Reference("d".into());
|
||||
fail(
|
||||
duplicate,
|
||||
"Duplicate ordinary or typed physical name token.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let mut cross = indexed(&bundle);
|
||||
let key = cross
|
||||
.data
|
||||
.occurrences
|
||||
.keys()
|
||||
.find(|k| k.role == index::TokenRole::Use(lower::Role::ParameterUse))
|
||||
.unwrap()
|
||||
.clone();
|
||||
cross.data.occurrences.insert(
|
||||
key,
|
||||
index::Location {
|
||||
file: 2,
|
||||
span: span(122, 123),
|
||||
},
|
||||
);
|
||||
fail(
|
||||
cross,
|
||||
"Duplicate ordinary or typed physical name token.",
|
||||
"main.arcadia",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file_and_utf8_damage_use_safe_explicit_diagnostic_locations() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("main.arcadia").unwrap() =
|
||||
bundle.files["main.arcadia"].replace("node n", "// 雨\nnode n");
|
||||
let mut bad = indexed(&bundle);
|
||||
let at = bad.data.occurrences.values_mut().next().unwrap();
|
||||
at.file = usize::MAX;
|
||||
assert_eq!(
|
||||
fail(bad, "Ordinary index has no canonical file.", "main.arcadia")
|
||||
.diagnostic
|
||||
.span,
|
||||
Span::default()
|
||||
);
|
||||
let mut bad = indexed(&bundle);
|
||||
let byte = bundle.files["main.arcadia"].find('雨').unwrap() + 1;
|
||||
bad.resolved.syntax.files[2].positions[0].1.fields.span = span(byte, byte + 1);
|
||||
assert_eq!(
|
||||
fail(
|
||||
bad,
|
||||
"Ordinary synthetic root differs from its body braces.",
|
||||
"main.arcadia"
|
||||
)
|
||||
.diagnostic
|
||||
.span,
|
||||
Span::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_resolution_ignores_same_spelled_scoped_parameter_names() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
let main = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*main = main
|
||||
.replace("oa_activity w", "oa_activity x")
|
||||
.replace("subject @w", "subject @x");
|
||||
let end = main.rfind('}').unwrap();
|
||||
main.insert_str(
|
||||
end,
|
||||
&format!(
|
||||
"artifact attach \"Attachment\" id \"{}\" {{ owner @x; uri \"x\"; }}\n",
|
||||
id(20)
|
||||
),
|
||||
);
|
||||
let complete = indexed(&bundle).index_ordinary().unwrap();
|
||||
let ordinary = complete
|
||||
.ordinary
|
||||
.values()
|
||||
.find(|r| r.owner == id(20) && r.role == OrdinaryRole::Reference)
|
||||
.unwrap();
|
||||
assert_eq!(ordinary.target, id(2));
|
||||
assert!(complete.names().contains_key(&lower::Target::Parameter {
|
||||
calculation: id(10),
|
||||
parameter: analysis::ParameterId(id(1))
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_stack_success_and_late_header_failure_leave_no_partial_owner() {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
drop(indexed(&bundle).index_ordinary().unwrap());
|
||||
let mut bad = indexed(&bundle);
|
||||
bad.data
|
||||
.names
|
||||
.insert(lower::Target::Global(id(999)), "unowned".into());
|
||||
fail(
|
||||
bad,
|
||||
"Global name is missing its authored declaration header.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let expression = format!("{}1{}", "abs(".repeat(127), ")".repeat(127));
|
||||
let deep = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body {expression}; }}",
|
||||
id(2)
|
||||
));
|
||||
drop(indexed(&deep).index_ordinary().unwrap());
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn duplicate_global_header_and_cross_file_projection_damage_are_distinct() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut duplicate = indexed(&bundle);
|
||||
let mut key = duplicate
|
||||
.data
|
||||
.occurrences
|
||||
.keys()
|
||||
.find(|k| k.role == index::TokenRole::Declaration && k.owner == id(10))
|
||||
.unwrap()
|
||||
.clone();
|
||||
key.field = Some(lower::Field::Body);
|
||||
// Reclassify an existing whole call token as a second header; remove its
|
||||
// former role so physical uniqueness cannot mask the header-coverage check.
|
||||
let call = duplicate
|
||||
.data
|
||||
.occurrences
|
||||
.iter()
|
||||
.find(|(_, at)| at.file == 2 && at.span == span(291, 292))
|
||||
.unwrap()
|
||||
.0
|
||||
.clone();
|
||||
duplicate.data.occurrences.remove(&call);
|
||||
duplicate.data.occurrences.insert(
|
||||
key,
|
||||
index::Location {
|
||||
file: 2,
|
||||
span: span(291, 292),
|
||||
},
|
||||
);
|
||||
fail(
|
||||
duplicate,
|
||||
"Global declaration header appears more than once.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let mut bundle = single("include \"a.arcadia\"; include \"b.arcadia\";");
|
||||
for (file, alias, n) in [("a.arcadia", "aa", 2), ("b.arcadia", "bb", 3)] {
|
||||
bundle.files.insert(
|
||||
file.into(),
|
||||
format!("oa_activity {alias} \"Same\" id \"{}\" {{}}", id(n)),
|
||||
);
|
||||
}
|
||||
let mut damaged = indexed(&bundle);
|
||||
let other = damaged.resolved.syntax.files[1].positions.remove(0);
|
||||
damaged.resolved.syntax.files[0].positions[0] = other;
|
||||
fail(
|
||||
damaged,
|
||||
"Ordinary position differs from its declaration owner.",
|
||||
"a.arcadia",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_ordinary_declarations_use_no_typed_frame_policy() {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
let mut body = String::new();
|
||||
for n in 2..34 {
|
||||
body.push_str(&format!("oa_activity a{n} \"A\" id \"{}\" {{\n", id(n)));
|
||||
}
|
||||
body.push_str(&"}\n".repeat(32));
|
||||
let bundle = single(&body);
|
||||
let indexed = indexed(&bundle);
|
||||
let usage = indexed.resolved.syntax.budget.usage;
|
||||
let before = indexed.resolved.storage.live;
|
||||
let complete = indexed.index_ordinary().unwrap();
|
||||
assert_eq!(complete.ordinary.len(), 33);
|
||||
assert!(
|
||||
complete
|
||||
.ordinary
|
||||
.values()
|
||||
.all(|o| o.role == OrdinaryRole::Header)
|
||||
);
|
||||
assert_eq!(complete.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(33), Span::default()).unwrap()
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn malformed_reference_utf8_start_is_reported_without_slicing_panic() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("main.arcadia").unwrap() =
|
||||
bundle.files["main.arcadia"].replace("diagram @d", "diagram @ // 雨\r\n d");
|
||||
let mut damaged = indexed(&bundle);
|
||||
let byte = bundle.files["main.arcadia"].find('雨').unwrap() + 1;
|
||||
node_field(&mut damaged, "diagram").value.span.start = byte;
|
||||
let diagnostic = fail(
|
||||
damaged,
|
||||
"Ordinary span is outside its original UTF-8 file.",
|
||||
"main.arcadia",
|
||||
);
|
||||
assert_eq!(diagnostic.diagnostic.span, Span::default());
|
||||
}
|
||||
const CATEGORIES: &str = include_str!("tests/fixtures/categories.arcadia");
|
||||
fn categories() -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), CATEGORIES.into())]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_authored_category_reference_excludes_trivia_prose_and_historical_uuid() {
|
||||
use OrdinaryRole::{Header as H, Reference as R};
|
||||
let bundle = categories();
|
||||
assert_eq!(CATEGORIES.len(), 2152);
|
||||
let indexed = indexed(&bundle);
|
||||
let model = indexed.project().clone();
|
||||
let warnings = indexed.warnings().to_vec();
|
||||
let before = indexed.resolved.storage.live;
|
||||
let usage = indexed.resolved.syntax.budget.usage;
|
||||
let numeric = indexed.resolved.syntax.budget.numeric_units();
|
||||
let complete = indexed.index_ordinary().unwrap();
|
||||
// Entire independently authored36-row map, checked by literal UTF-8 arithmetic.
|
||||
let rows = [
|
||||
(19, 20, 1, 1, H, 19),
|
||||
(168, 169, 2, 2, H, 168),
|
||||
(249, 254, 3, 3, H, 249),
|
||||
(344, 345, 3, 2, R, 323),
|
||||
(369, 377, 4, 4, H, 369),
|
||||
(487, 488, 5, 5, H, 487),
|
||||
(561, 565, 6, 6, H, 561),
|
||||
(625, 630, 6, 3, R, 624),
|
||||
(640, 648, 6, 4, R, 639),
|
||||
(664, 665, 7, 7, H, 664),
|
||||
(741, 745, 7, 6, R, 740),
|
||||
(789, 793, 7, 6, R, 788),
|
||||
(818, 822, 8, 8, H, 818),
|
||||
(885, 889, 8, 6, R, 884),
|
||||
(925, 926, 9, 9, H, 925),
|
||||
(992, 997, 9, 3, R, 991),
|
||||
(1003, 1004, 9, 5, R, 1002),
|
||||
(1328, 1329, 10, 10, H, 1328),
|
||||
(1391, 1394, 10, 12, R, 1390),
|
||||
(1397, 1402, 10, 14, R, 1396),
|
||||
(1422, 1425, 11, 11, H, 1422),
|
||||
(1487, 1491, 11, 13, R, 1486),
|
||||
(1514, 1517, 12, 12, H, 1514),
|
||||
(1577, 1578, 12, 10, R, 1576),
|
||||
(1617, 1620, 12, 11, R, 1616),
|
||||
(1648, 1652, 13, 13, H, 1648),
|
||||
(1712, 1715, 13, 11, R, 1711),
|
||||
(1741, 1746, 13, 3, R, 1740),
|
||||
(1789, 1794, 14, 14, H, 1789),
|
||||
(1855, 1856, 14, 10, R, 1854),
|
||||
(1882, 1890, 14, 4, R, 1881),
|
||||
(1935, 1942, 15, 15, H, 1935),
|
||||
(2004, 2005, 15, 10, R, 2003),
|
||||
(2029, 2032, 15, 12, R, 2028),
|
||||
(2047, 2051, 15, 13, R, 2046),
|
||||
(2092, 2097, 15, 14, R, 2091),
|
||||
];
|
||||
let expected = rows
|
||||
.into_iter()
|
||||
.map(|(start, end, owner, target, role, extent)| {
|
||||
((0, start, end), record(owner, target, role, extent, end))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(complete.ordinary_occurrences(), &expected);
|
||||
assert_eq!(complete.project(), &model);
|
||||
assert_eq!(complete.warnings(), warnings);
|
||||
assert!(std::ptr::eq(complete.bundle(), &bundle));
|
||||
assert!(complete.typed_occurrences().is_empty());
|
||||
assert!(complete.expressions().is_empty());
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.storage.live,
|
||||
before.add(Slots::entries(36), Span::default()).unwrap()
|
||||
);
|
||||
assert_eq!(complete.indexed.resolved.syntax.budget.usage, usage);
|
||||
assert_eq!(
|
||||
complete.indexed.resolved.syntax.budget.numeric_units(),
|
||||
numeric
|
||||
);
|
||||
assert_eq!(complete.project().elements[&id(3)].parent, Some(id(2)));
|
||||
assert_eq!(complete.project().elements[&id(4)].parent, Some(id(2)));
|
||||
assert!(
|
||||
!complete
|
||||
.ordinary
|
||||
.values()
|
||||
.any(|o| o.owner == id(4) && o.role == R),
|
||||
"implicit parent has no authored token"
|
||||
);
|
||||
assert_eq!(&CATEGORIES[323..345], "@ // @noise child\r\n a");
|
||||
let Some(semantic) = complete.project().traces[&id(9)]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
else {
|
||||
panic!("historical semantic baseline")
|
||||
};
|
||||
assert!(
|
||||
matches!(&semantic.facets[0],crate::semantic::FacetReview::Field(crate::semantic::FieldReview::Type(review)) if review.oa==crate::semantic::Observed::Value(crate::TypeRef::Named(id(3))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_scratch_peak_reserves_live_iterative_slots_before_growth() {
|
||||
let mut bundle = categories();
|
||||
let points = ["{ x 0; y 0; }"; 64].join(", ");
|
||||
let source = bundle.files.get_mut("main.arcadia").unwrap();
|
||||
*source = source.replace(
|
||||
"routes [{ edge @link; }];",
|
||||
&format!("routes [{{ edge @link; waypoints [{points}]; }}];"),
|
||||
);
|
||||
let baseline = indexed(&bundle).resolved.storage.live;
|
||||
// Unexpanded category peak52 is below the earlier resolver catalog/Vec
|
||||
// overlap. Sixty-four valid route points isolate this stage: N11 + G15,
|
||||
// pending edge1 + point values64, then pop one and push x/y =>W66.
|
||||
// Exactly92 slots are live; earlier catalog overlap is only74.
|
||||
for (extra, fits) in [(91, false), (92, true)] {
|
||||
let profile = super::super::super::super::budget::Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let parsed =
|
||||
super::super::super::parse_with_profile(&bundle, ProjectLimits::default(), profile)
|
||||
.unwrap();
|
||||
let indexed = resolve(parsed).unwrap().index().unwrap();
|
||||
assert_eq!(indexed.resolved.storage.live, baseline);
|
||||
if fits {
|
||||
assert_eq!(
|
||||
indexed
|
||||
.index_ordinary()
|
||||
.unwrap()
|
||||
.indexed
|
||||
.resolved
|
||||
.storage
|
||||
.live,
|
||||
baseline.add(Slots::entries(36), Span::default()).unwrap()
|
||||
);
|
||||
} else {
|
||||
let diagnostic = fail(
|
||||
indexed,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
"main.arcadia",
|
||||
);
|
||||
let start = bundle.files["main.arcadia"].rfind("y 0").unwrap() + 2;
|
||||
assert_eq!(diagnostic.diagnostic.span, span(start, start + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# This exact-byte source fixture deliberately retains its authored CRLF.
|
||||
categories.arcadia -text whitespace=cr-at-eol
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
arcadia 8;
|
||||
project p "Authored @p child" id "00000000-0000-0000-0000-000000000001" {
|
||||
description "Do not index @child, @link or uuid in prose 雨";
|
||||
oa_activity a "Parent child" id "00000000-0000-0000-0000-000000000002" {
|
||||
oa_activity child "Explicit child" id "00000000-0000-0000-0000-000000000003" { parent @ // @noise child
|
||||
a; }
|
||||
oa_activity implicit "Implicit child" id "00000000-0000-0000-0000-000000000004" { description "child @a"; }
|
||||
}
|
||||
sa_function s "Selected" id "00000000-0000-0000-0000-000000000005" {}
|
||||
interaction link "Link" id "00000000-0000-0000-0000-000000000006" { source @child; target @implicit; }
|
||||
diagram d "View" id "00000000-0000-0000-0000-000000000007" {
|
||||
routes [{ edge @link; }];
|
||||
view_options { hidden_edges [@link]; };
|
||||
}
|
||||
artifact note "Evidence" id "00000000-0000-0000-0000-000000000008" { owner @link; uri "docs/@child.md"; }
|
||||
trace t "Review" id "00000000-0000-0000-0000-000000000009" {
|
||||
oa @child; sa @s; 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 q "Caller" id "00000000-0000-0000-0000-000000000010" { steps [@use, @right]; }
|
||||
oa_process sub "Callee" id "00000000-0000-0000-0000-000000000011" { steps [@left]; }
|
||||
oa_occurrence use "Call" id "00000000-0000-0000-0000-000000000012" { parent @q; body { SequenceReference { sequence @sub; }; }; }
|
||||
oa_occurrence left "Left" id "00000000-0000-0000-0000-000000000013" { parent @sub; body { Action { action @child; lifeline none; }; }; }
|
||||
oa_occurrence right "Right" id "00000000-0000-0000-0000-000000000014" { parent @q; body { Action { action @implicit; lifeline none; }; }; }
|
||||
contextual_link context "Context" id "00000000-0000-0000-0000-000000000015" { owner @q; source { references [@use]; occurrence @left; }; target { references []; occurrence @right; }; purpose { Precedence { guard "@child"; }; }; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,483 @@
|
||||
use super::*;
|
||||
use crate::language::analysis_source::index::{Location, TokenRole};
|
||||
use lower::{Child as C, Field as F, Role as R, Target as T};
|
||||
|
||||
fn parameter(n: usize) -> a::ParameterId {
|
||||
a::ParameterId(id(n))
|
||||
}
|
||||
fn q(c: usize, p: usize) -> T {
|
||||
T::Parameter {
|
||||
calculation: id(c),
|
||||
parameter: parameter(p),
|
||||
}
|
||||
}
|
||||
fn arg(n: usize) -> C {
|
||||
C::Argument(parameter(n))
|
||||
}
|
||||
fn mapped(original: &str, byte: usize, transformed: bool) -> usize {
|
||||
if transformed {
|
||||
8 + byte
|
||||
+ original.as_bytes()[..byte]
|
||||
.iter()
|
||||
.filter(|&&b| b == b'\n')
|
||||
.count()
|
||||
} else {
|
||||
byte
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entire_authored_index_oa_sa_original_and_unicode_crlf() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for transformed in [false, true] {
|
||||
let original = rich_bundle(layer);
|
||||
let mut bundle = original.clone();
|
||||
if transformed {
|
||||
for source in bundle.files.values_mut() {
|
||||
*source = format!("// 雨\r\n{}", source.replace('\n', "\r\n"));
|
||||
}
|
||||
}
|
||||
let resolved = accepted(&bundle);
|
||||
let before = resolved.storage.live;
|
||||
let usage = resolved.syntax.budget.usage;
|
||||
let numeric = resolved.syntax.budget.numeric_units();
|
||||
let warnings = resolved.warnings.clone();
|
||||
let static_limits = resolved.static_limits;
|
||||
let static_usage = resolved.static_usage;
|
||||
let indexed = resolved.index().unwrap();
|
||||
assert_eq!(indexed.project(), &rich_model(layer));
|
||||
assert_eq!(indexed.warnings(), warnings);
|
||||
assert!(std::ptr::eq(indexed.bundle(), &bundle));
|
||||
// Root plus Left/Argument(1)/Argument(1) is four structural frames.
|
||||
// Index verification may raise this peak without reparsing input.
|
||||
let mut expected_usage = usage;
|
||||
expected_usage.deepest_frame = expected_usage.deepest_frame.max(4);
|
||||
assert_eq!(indexed.resolved.syntax.budget.usage, expected_usage);
|
||||
assert_eq!(indexed.resolved.syntax.budget.numeric_units(), numeric);
|
||||
assert_eq!(indexed.resolved.static_limits, static_limits);
|
||||
assert_eq!(indexed.resolved.static_usage, static_usage);
|
||||
assert_eq!(
|
||||
indexed.files().collect::<Vec<_>>(),
|
||||
bundle
|
||||
.files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (p, s))| (i, p.as_str(), s.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let mut names: BTreeMap<_, String> = [
|
||||
(1, "p"),
|
||||
(2, "w"),
|
||||
(3, "v"),
|
||||
(4, "n"),
|
||||
(5, "d"),
|
||||
(10, "f"),
|
||||
(11, "g"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(n, s)| (T::Global(id(n)), s.into()))
|
||||
.collect();
|
||||
for c in [10, 11] {
|
||||
for (p, s) in [(1, "x"), (12, "y")] {
|
||||
names.insert(q(c, p), s.into());
|
||||
}
|
||||
}
|
||||
if layer == Layer::Sa {
|
||||
names.insert(T::Global(id(6)), "s".into());
|
||||
}
|
||||
assert_eq!(indexed.names(), &names);
|
||||
assert_eq!(
|
||||
indexed.resolved.storage.live,
|
||||
before
|
||||
.add(
|
||||
Slots {
|
||||
entries: if layer == Layer::Sa { 60 } else { 59 },
|
||||
occurrences: 0,
|
||||
text: names.len()
|
||||
},
|
||||
Span::default()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
let at = |file: usize, start, end| {
|
||||
let source = original.files.values().nth(file).unwrap();
|
||||
Location {
|
||||
file,
|
||||
span: Span {
|
||||
start: mapped(source, start, transformed),
|
||||
end: mapped(source, end, transformed),
|
||||
},
|
||||
}
|
||||
};
|
||||
let mut expressions = BTreeMap::new();
|
||||
for (file, owner) in [(0, 10), (1, 11)] {
|
||||
for (path, start, end) in [
|
||||
(vec![], 202, 209),
|
||||
(vec![C::Left], 202, 204),
|
||||
(vec![C::Right], 207, 209),
|
||||
] {
|
||||
expressions.insert((id(owner), F::Body, path), at(file, start, end));
|
||||
}
|
||||
}
|
||||
for (path, start, end) in [
|
||||
(vec![], 290, 334),
|
||||
(vec![C::Left], 290, 317),
|
||||
(vec![C::Left, arg(12)], 296, 297),
|
||||
(vec![C::Left, arg(1)], 302, 316),
|
||||
(vec![C::Left, arg(1), arg(1)], 308, 309),
|
||||
(vec![C::Left, arg(1), arg(12)], 314, 315),
|
||||
(vec![C::Right], 320, 334),
|
||||
(vec![C::Right, arg(1)], 326, 327),
|
||||
(vec![C::Right, arg(12)], 332, 333),
|
||||
] {
|
||||
expressions.insert((id(3), F::Base, path), at(2, start, end));
|
||||
}
|
||||
assert_eq!(
|
||||
indexed
|
||||
.expressions()
|
||||
.iter()
|
||||
.map(|(k, v)| ((k.owner, k.field, k.path.edges()), *v))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
expressions
|
||||
);
|
||||
let mut occurrences = BTreeMap::new();
|
||||
for (file, owner, start) in [(0, 10, 12), (1, 11, 12), (2, 3, 206)] {
|
||||
occurrences.insert(
|
||||
(
|
||||
id(owner),
|
||||
None,
|
||||
None,
|
||||
TokenRole::Declaration,
|
||||
T::Global(id(owner)),
|
||||
),
|
||||
at(file, start, start + 1),
|
||||
);
|
||||
}
|
||||
for (file, owner) in [(0, 10), (1, 11)] {
|
||||
for (p, start) in [(1, 68), (12, 128)] {
|
||||
occurrences.insert(
|
||||
(
|
||||
id(owner),
|
||||
Some(F::Input(parameter(p))),
|
||||
None,
|
||||
TokenRole::Use(R::ParameterDeclaration),
|
||||
q(owner, p),
|
||||
),
|
||||
at(file, start, start + 1),
|
||||
);
|
||||
}
|
||||
for (p, edge, start) in [(1, C::Left, 203), (12, C::Right, 208)] {
|
||||
occurrences.insert(
|
||||
(
|
||||
id(owner),
|
||||
Some(F::Body),
|
||||
Some(vec![edge]),
|
||||
TokenRole::Use(R::ParameterUse),
|
||||
q(owner, p),
|
||||
),
|
||||
at(file, start, start + 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
occurrences.insert(
|
||||
(
|
||||
id(3),
|
||||
Some(F::Subject),
|
||||
None,
|
||||
TokenRole::Use(R::Reference),
|
||||
T::Global(id(2)),
|
||||
),
|
||||
at(2, 269, 270),
|
||||
);
|
||||
for (path, role, target, start) in [
|
||||
(vec![C::Left], R::CallTarget, T::Global(id(10)), 291),
|
||||
(vec![C::Left], R::ArgumentLabel, q(10, 12), 293),
|
||||
(vec![C::Left], R::ArgumentLabel, q(10, 1), 299),
|
||||
(vec![C::Left, arg(1)], R::CallTarget, T::Global(id(11)), 303),
|
||||
(vec![C::Left, arg(1)], R::ArgumentLabel, q(11, 1), 305),
|
||||
(vec![C::Left, arg(1)], R::ArgumentLabel, q(11, 12), 311),
|
||||
(vec![C::Right], R::CallTarget, T::Global(id(10)), 321),
|
||||
(vec![C::Right], R::ArgumentLabel, q(10, 1), 323),
|
||||
(vec![C::Right], R::ArgumentLabel, q(10, 12), 329),
|
||||
] {
|
||||
occurrences.insert(
|
||||
(
|
||||
id(3),
|
||||
Some(F::Base),
|
||||
Some(path),
|
||||
TokenRole::Use(role),
|
||||
target,
|
||||
),
|
||||
at(2, start, start + 1),
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
indexed
|
||||
.occurrences()
|
||||
.iter()
|
||||
.map(|(k, v)| (
|
||||
(
|
||||
k.owner,
|
||||
k.field,
|
||||
k.path.as_ref().map(|p| p.edges()),
|
||||
k.role,
|
||||
k.target
|
||||
),
|
||||
*v
|
||||
))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
occurrences
|
||||
);
|
||||
for (key, at) in indexed.occurrences() {
|
||||
let source = indexed.files().nth(at.file).unwrap().2;
|
||||
assert_eq!(&source[at.span.start..at.span.end], names[&key.target]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject(value: ResolvedMixedProject<'_>, message: &str, file: Option<&str>) -> Failure {
|
||||
let failure = match value.index() {
|
||||
Ok(_) => panic!("corrupt index accepted: {message}"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.message, message);
|
||||
if let Some(file) = file {
|
||||
assert_eq!(failure.diagnostics[0].path, file);
|
||||
}
|
||||
failure
|
||||
}
|
||||
#[test]
|
||||
fn damage_reaches_structural_token_and_call_checks() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
for case in 0..13 {
|
||||
let mut value = accepted(&bundle);
|
||||
let message = match case {
|
||||
0 => {
|
||||
value.expressions[1].file = 1;
|
||||
"Source span crosses its containing syntax."
|
||||
}
|
||||
1 => {
|
||||
value.expressions[1].owner = id(1);
|
||||
"Unknown expression owner."
|
||||
}
|
||||
2 => {
|
||||
value.expressions[1].field = F::Predicate;
|
||||
"Expression parent crosses its owner, field or file."
|
||||
}
|
||||
3 => {
|
||||
value.expressions[1].parent = Some((1, C::Left));
|
||||
"Expression parent must precede its child."
|
||||
}
|
||||
4 => {
|
||||
value.expressions[1].parent = Some((2, C::Left));
|
||||
"Expression parent must precede its child."
|
||||
}
|
||||
5 => {
|
||||
value.expressions[1].parent = Some((0, C::Operand));
|
||||
"Arena parent/child edge differs from original syntax."
|
||||
}
|
||||
6 => {
|
||||
value.occurrences[11].target = q(11, 1);
|
||||
"Argument label crosses its called calculation."
|
||||
}
|
||||
7 => {
|
||||
value.occurrences.remove(11);
|
||||
"Argument child has no qualified label."
|
||||
}
|
||||
8 => {
|
||||
value.expressions.pop();
|
||||
"Parsed expression is absent from the source arena."
|
||||
}
|
||||
9 => {
|
||||
value.occurrences[1].span = value.occurrences[0].span;
|
||||
value.occurrences[1].target = q(10, 1);
|
||||
value.occurrences[1].field = F::Input(parameter(1));
|
||||
"Duplicate qualified occurrence or token ownership."
|
||||
}
|
||||
10 => {
|
||||
value.occurrences[0].span = Span { start: 69, end: 70 };
|
||||
"Token differs from its qualified target name."
|
||||
}
|
||||
11 => {
|
||||
let token = value.syntax.files[0]
|
||||
.tokens
|
||||
.iter_mut()
|
||||
.find(|t| t.span.start == 68)
|
||||
.unwrap();
|
||||
token.class = super::super::super::super::lex::Class::String;
|
||||
"Occurrence is not an original whole name token."
|
||||
}
|
||||
_ => {
|
||||
value.expressions.push(lower::ExpressionSite {
|
||||
file: 0,
|
||||
owner: id(10),
|
||||
field: F::Base,
|
||||
parent: None,
|
||||
span: Span {
|
||||
start: 202,
|
||||
end: 204,
|
||||
},
|
||||
});
|
||||
"Arena contains expressions absent from parsed syntax."
|
||||
}
|
||||
};
|
||||
let file = if matches!(case, 6..=8) {
|
||||
Some("main.arcadia")
|
||||
} else if case == 0 {
|
||||
Some("b.arcadia")
|
||||
} else if case == 12 {
|
||||
None
|
||||
} else {
|
||||
Some("a.arcadia")
|
||||
};
|
||||
reject(value, message, file);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_maps_keep_baseline_and_scope_reservations_and_peak_scratch() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let baseline = accepted(&bundle).storage.live;
|
||||
// Final new maps59; at the last occurrence, owners3+physical21+paths15
|
||||
// coexist. Subsequent call peak77 and syntax peak90 are smaller than98.
|
||||
for (extra, fits) in [(97, false), (98, true)] {
|
||||
let profile = super::super::super::super::budget::Limits {
|
||||
entries: baseline.entries + extra,
|
||||
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let resolved =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(resolved.storage.live, baseline);
|
||||
if fits {
|
||||
let indexed = resolved.index().unwrap();
|
||||
assert_eq!(indexed.resolved.storage.live.entries, baseline.entries + 59);
|
||||
assert_eq!(
|
||||
indexed.resolved.storage.live.occurrences,
|
||||
baseline.occurrences
|
||||
);
|
||||
} else {
|
||||
let failure = reject(
|
||||
resolved,
|
||||
"Source8 lowering entry limit exceeded.",
|
||||
Some("main.arcadia"),
|
||||
);
|
||||
assert_eq!(
|
||||
failure.diagnostics[0].diagnostic.span,
|
||||
Span {
|
||||
start: 329,
|
||||
end: 330
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copied_name_bytes_coexist_with_retained_warning_bytes() {
|
||||
let mut bundle = single("");
|
||||
let source = bundle
|
||||
.files
|
||||
.remove("main.arcadia")
|
||||
.unwrap()
|
||||
.replace("\"Project\"", "\"\"");
|
||||
bundle.entry = format!("{}.arcadia", "a".repeat(180));
|
||||
bundle.files.insert(bundle.entry.clone(), source);
|
||||
for (bytes, fits) in [(236, false), (237, true)] {
|
||||
let profile = super::super::super::super::budget::Limits {
|
||||
bytes,
|
||||
..super::super::super::super::budget::Limits::source8(language::ParseLimits::UNLIMITED)
|
||||
};
|
||||
let resolved =
|
||||
resolve(parse_with_profile(&bundle, ProjectLimits::default(), profile).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(resolved.storage.live.text, 236);
|
||||
if fits {
|
||||
assert_eq!(resolved.index().unwrap().resolved.storage.live.text, 237);
|
||||
} else {
|
||||
reject(resolved, "Source8 index/output byte limit exceeded.", None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_and_late_failure_drop_on_small_stack() {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
drop(accepted(&bundle).index().unwrap());
|
||||
let deep = single(&format!(
|
||||
"calculation c \"C\" id \"{}\" {{ result Number; body {}1{}; }}",
|
||||
id(2),
|
||||
"abs(".repeat(127),
|
||||
")".repeat(127)
|
||||
));
|
||||
drop(accepted(&deep).index().unwrap());
|
||||
let mut damaged = accepted(&deep);
|
||||
damaged.expressions[127].parent = Some((126, C::Left));
|
||||
reject(
|
||||
damaged,
|
||||
"Arena parent/child edge differs from original syntax.",
|
||||
Some("main.arcadia"),
|
||||
);
|
||||
let mut value = accepted(&bundle);
|
||||
value.expressions[1].parent = Some((0, C::Operand));
|
||||
reject(
|
||||
value,
|
||||
"Arena parent/child edge differs from original syntax.",
|
||||
Some("a.arcadia"),
|
||||
);
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file_and_utf8_span_use_explicit_safe_fallback() {
|
||||
let mut bundle = rich_bundle(Layer::Oa);
|
||||
*bundle.files.get_mut("a.arcadia").unwrap() =
|
||||
bundle.files["a.arcadia"].replace("\"F\"", "\"雨\"");
|
||||
let mut value = accepted(&bundle);
|
||||
value.expressions[0].file = usize::MAX;
|
||||
let failure = reject(
|
||||
value,
|
||||
"Source span is outside its original UTF-8 file.",
|
||||
Some("main.arcadia"),
|
||||
);
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
|
||||
let mut value = accepted(&bundle);
|
||||
let byte = bundle.files["a.arcadia"].find('雨').unwrap() + 1;
|
||||
value.occurrences[0].span = Span {
|
||||
start: byte,
|
||||
end: byte + 1,
|
||||
};
|
||||
let failure = reject(
|
||||
value,
|
||||
"Source span is outside its original UTF-8 file.",
|
||||
Some("a.arcadia"),
|
||||
);
|
||||
assert_eq!(failure.diagnostics[0].diagnostic.span, Span::default());
|
||||
}
|
||||
#[test]
|
||||
fn distinct_qualified_keys_cannot_share_one_physical_token() {
|
||||
let bundle = rich_bundle(Layer::Oa);
|
||||
let mut value = accepted(&bundle);
|
||||
let original = &value.occurrences[2];
|
||||
value.occurrences.push(lower::Occurrence {
|
||||
file: original.file,
|
||||
owner: original.owner,
|
||||
field: original.field,
|
||||
expression: original.expression,
|
||||
role: R::ArgumentLabel,
|
||||
target: q(11, 1),
|
||||
span: original.span,
|
||||
});
|
||||
reject(
|
||||
value,
|
||||
"Duplicate qualified occurrence or token ownership.",
|
||||
Some("a.arcadia"),
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
@@ -349,6 +365,11 @@ impl Cursor<'_, '_> {
|
||||
_ => 0,
|
||||
};
|
||||
if child_depth >= crate::analysis::MAX_EXPRESSION_DEPTH {
|
||||
// The rejected parent still owns accepted, potentially depth-128
|
||||
// children. Recursive derived cleanup here runs beneath the parser
|
||||
// frames and can exhaust the bounded native stack before returning
|
||||
// the depth error. Consume those children with an explicit worklist.
|
||||
drop_rejected_kind(kind);
|
||||
return Err(error(span, "Expression logical depth exceeds 128."));
|
||||
}
|
||||
Ok(Expression {
|
||||
@@ -399,6 +420,42 @@ impl Cursor<'_, '_> {
|
||||
self.node(kind, span)
|
||||
}
|
||||
}
|
||||
fn drop_rejected_kind(kind: ExpressionKind) {
|
||||
let mut pending = vec![kind];
|
||||
while let Some(kind) = pending.pop() {
|
||||
match kind {
|
||||
ExpressionKind::Unary { operand, .. } => pending.push(operand.kind),
|
||||
ExpressionKind::Binary { left, right, .. } => {
|
||||
pending.push(left.kind);
|
||||
pending.push(right.kind);
|
||||
}
|
||||
ExpressionKind::If {
|
||||
condition,
|
||||
when_true,
|
||||
when_false,
|
||||
} => {
|
||||
pending.push(condition.kind);
|
||||
pending.push(when_true.kind);
|
||||
pending.push(when_false.kind);
|
||||
}
|
||||
ExpressionKind::List(values) => pending.extend(values.into_iter().map(|v| v.kind)),
|
||||
ExpressionKind::Call { arguments, .. } => {
|
||||
pending.extend(arguments.into_iter().map(|a| a.value.kind));
|
||||
}
|
||||
ExpressionKind::Sum { values, initial } => {
|
||||
pending.push(values.kind);
|
||||
pending.push(initial.kind);
|
||||
}
|
||||
ExpressionKind::Power { base, .. } => pending.push(base.kind),
|
||||
ExpressionKind::AsKind { value, .. } => pending.push(value.kind),
|
||||
ExpressionKind::Boolean(_)
|
||||
| ExpressionKind::Number(_)
|
||||
| ExpressionKind::Quantity { .. }
|
||||
| ExpressionKind::Property(_)
|
||||
| ExpressionKind::Parameter(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn comparison(op: BinaryOp) -> bool {
|
||||
matches!(
|
||||
op,
|
||||
|
||||
@@ -108,6 +108,49 @@ fn flat_binary_depth_and_malformed_constructor_retain_bounded_completed_children
|
||||
assert_eq!(number(&expression("1")), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_parent_shape_rejects_depth129_and_cleans_children_on_128k_stack() {
|
||||
std::thread::Builder::new()
|
||||
.name("analysis-syntax-rejection-128k".into())
|
||||
.stack_size(128 * 1024)
|
||||
.spawn(|| {
|
||||
// Rotate through every recursive syntax shape, so the rejected
|
||||
// parent's complete child tree also exercises every cleanup arm.
|
||||
let wrap = |shape, child: &str| match shape {
|
||||
0 => format!("abs({child})"),
|
||||
1 => format!("({child}) + 1"),
|
||||
2 => format!("if(true, {child}, 0)"),
|
||||
3 => format!("[{child}]"),
|
||||
4 => format!("@f(a: {child})"),
|
||||
5 => format!("sum({child}, 0)"),
|
||||
6 => format!("power({child}, 1)"),
|
||||
7 => format!("as_kind(@k, {child})"),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut deep = "1".to_owned();
|
||||
for layer in 0..127 {
|
||||
deep = wrap(layer % 8, &deep);
|
||||
}
|
||||
assert_eq!(expression(&deep).depth, 128);
|
||||
for shape in 0..8 {
|
||||
let source = wrap(shape, &deep);
|
||||
let failure = parse(&source, &mut budget()).unwrap_err();
|
||||
assert_eq!(failure.message, "Expression logical depth exceeds 128.");
|
||||
assert_eq!(
|
||||
failure.span,
|
||||
Span {
|
||||
start: 0,
|
||||
end: source.len()
|
||||
}
|
||||
);
|
||||
assert_eq!(number(&expression("1")), "1");
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn significant_ratio_and_quantity_trivia_preserve_original_spans() {
|
||||
let source = "ratio // explanation café\n ( - // sign\n 3, // denominator\n 4 ) // measurement\n [@millisecond]";
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//! Simultaneous typed token edits. This is a staged plan, not source acceptance:
|
||||
//! the bundle owner must merge ordinary edits, reparse/recompile the exact bytes,
|
||||
//! admit once and compare the complete intended Project before installation.
|
||||
use super::index::{Index, Location, OccurrenceKey, checked_source};
|
||||
use super::lower::Target;
|
||||
use super::{Budget, Error, Span, error};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Edit {
|
||||
key: OccurrenceKey,
|
||||
location: Location,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Plan<'i, 's> {
|
||||
index: &'i Index<'s>,
|
||||
names: BTreeMap<Target, String>,
|
||||
edits: Vec<Edit>,
|
||||
}
|
||||
impl<'i, 's> Plan<'i, 's> {
|
||||
/// All names are the desired final names. Validate the final namespaces,
|
||||
/// rather than applying one rename at a time: alias/parameter swaps are legal.
|
||||
pub fn renames(
|
||||
index: &'i Index<'s>,
|
||||
renames: &BTreeMap<Target, String>,
|
||||
budget: &Budget,
|
||||
) -> Result<Self, Error> {
|
||||
let plan = Self::prepare(index, renames, budget)?;
|
||||
plan.validate(index.sources(), budget)?;
|
||||
plan.output_lengths(index.sources(), budget)?;
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn validate_names(
|
||||
index: &Index<'_>,
|
||||
renames: &BTreeMap<Target, String>,
|
||||
budget: &Budget,
|
||||
) -> Result<(), Error> {
|
||||
budget.active()?;
|
||||
budget.lowering_entries(renames.len(), Span::default())?;
|
||||
let mut bytes = 0usize;
|
||||
for (target, name) in renames {
|
||||
if !index.names().contains_key(target) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename has no qualified source target.",
|
||||
));
|
||||
}
|
||||
if !crate::valid_alias(name) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Invalid alias or scoped parameter name.",
|
||||
));
|
||||
}
|
||||
bytes = bytes
|
||||
.checked_add(name.len())
|
||||
.ok_or_else(|| error(Span::default(), "Rename size overflow."))?;
|
||||
budget.index_bytes(bytes, Span::default())?;
|
||||
}
|
||||
let mut final_names = BTreeSet::new();
|
||||
for (target, original) in index.names() {
|
||||
let name = renames.get(target).unwrap_or(original);
|
||||
let scope = match target {
|
||||
Target::Global(_) => None,
|
||||
Target::Parameter { calculation, .. } => Some(*calculation),
|
||||
};
|
||||
budget.lowering_entries(final_names.len() + 1, Span::default())?;
|
||||
if !final_names.insert((scope, name.as_str())) {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Rename creates a duplicate name in its namespace.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(
|
||||
index: &'i Index<'s>,
|
||||
renames: &BTreeMap<Target, String>,
|
||||
budget: &Budget,
|
||||
) -> Result<Self, Error> {
|
||||
// Validate record/name storage before copying any owned final names.
|
||||
Self::validate_names(index, renames, budget)?;
|
||||
let mut edits = Vec::new();
|
||||
for (key, location) in index.occurrences() {
|
||||
if renames
|
||||
.get(&key.target)
|
||||
.is_some_and(|name| index.names().get(&key.target) != Some(name))
|
||||
{
|
||||
budget.lowering_occurrences(edits.len() + 1, location.span)?;
|
||||
edits.push(Edit {
|
||||
key: key.clone(),
|
||||
location: *location,
|
||||
});
|
||||
}
|
||||
}
|
||||
edits.sort_by_key(|edit| {
|
||||
(
|
||||
edit.location.file,
|
||||
edit.location.span.start,
|
||||
edit.location.span.end,
|
||||
)
|
||||
});
|
||||
Ok(Self {
|
||||
index,
|
||||
names: renames.clone(),
|
||||
edits,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn edit_count(&self) -> usize {
|
||||
self.edits.len()
|
||||
}
|
||||
|
||||
/// Recheck the complete original witness and complete rename intent.
|
||||
/// A changed unrelated file is stale too. This does not construct output.
|
||||
fn validate(&self, current: &BTreeMap<usize, String>, budget: &Budget) -> Result<(), Error> {
|
||||
budget.active()?;
|
||||
if current != self.index.sources() {
|
||||
return Err(error(
|
||||
Span::default(),
|
||||
"Source index is stale: original file set or bytes changed.",
|
||||
));
|
||||
}
|
||||
budget.lowering_entries(current.len(), Span::default())?;
|
||||
budget.lowering_occurrences(self.edits.len(), Span::default())?;
|
||||
let mut previous: Option<Location> = None;
|
||||
let mut seen = BTreeSet::new();
|
||||
for edit in &self.edits {
|
||||
let at = edit.location;
|
||||
if self.index.occurrences().get(&edit.key) != Some(&at) || !seen.insert(&edit.key) {
|
||||
return Err(error(at.span, "Forged or duplicate edit occurrence."));
|
||||
}
|
||||
let old = checked_source(current, at)?;
|
||||
if self
|
||||
.index
|
||||
.names()
|
||||
.get(&edit.key.target)
|
||||
.is_none_or(|name| name != old)
|
||||
{
|
||||
return Err(error(at.span, "Stale original token spelling."));
|
||||
}
|
||||
self.names
|
||||
.get(&edit.key.target)
|
||||
.ok_or_else(|| error(at.span, "Edit has no final target name."))?;
|
||||
if previous.is_some_and(|prior| {
|
||||
prior.file > at.file || (prior.file == at.file && prior.span.end > at.span.start)
|
||||
}) {
|
||||
return Err(error(at.span, "Overlapping or unordered source edits."));
|
||||
}
|
||||
previous = Some(at);
|
||||
}
|
||||
for (key, at) in self.index.occurrences() {
|
||||
let required = self
|
||||
.names
|
||||
.get(&key.target)
|
||||
.is_some_and(|name| self.index.names().get(&key.target) != Some(name));
|
||||
if required != seen.contains(key) {
|
||||
return Err(error(
|
||||
at.span,
|
||||
"Edit set differs from the complete qualified rename plan.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Standalone output preflight, after ownership validation. Composition
|
||||
/// defers this hypothetical typed-only size to its future combined emitter.
|
||||
fn output_lengths(
|
||||
&self,
|
||||
current: &BTreeMap<usize, String>,
|
||||
budget: &Budget,
|
||||
) -> Result<BTreeMap<usize, usize>, Error> {
|
||||
let mut lengths: BTreeMap<_, _> = current
|
||||
.iter()
|
||||
.map(|(&file, source)| (file, source.len()))
|
||||
.collect();
|
||||
for edit in &self.edits {
|
||||
let at = edit.location;
|
||||
let length = lengths.get_mut(&at.file).expect("validated edit file");
|
||||
*length = length
|
||||
.checked_sub(at.span.end - at.span.start)
|
||||
.and_then(|size| size.checked_add(self.names[&edit.key.target].len()))
|
||||
.ok_or_else(|| error(at.span, "Updated source size overflow."))?;
|
||||
}
|
||||
let mut total = 0usize;
|
||||
for length in lengths.values() {
|
||||
total = total
|
||||
.checked_add(*length)
|
||||
.ok_or_else(|| error(Span::default(), "Updated bundle size overflow."))?;
|
||||
budget.index_bytes(total, Span::default())?;
|
||||
}
|
||||
Ok(lengths)
|
||||
}
|
||||
|
||||
pub fn apply(
|
||||
&self,
|
||||
current: &BTreeMap<usize, String>,
|
||||
budget: &Budget,
|
||||
) -> Result<BTreeMap<usize, String>, Error> {
|
||||
self.validate(current, budget)?;
|
||||
let lengths = self.output_lengths(current, budget)?;
|
||||
let mut result = BTreeMap::new();
|
||||
let mut edits = self.edits.iter().peekable();
|
||||
for (&file, source) in current {
|
||||
let mut output = String::with_capacity(lengths[&file]);
|
||||
let mut start = 0;
|
||||
while edits.peek().is_some_and(|edit| edit.location.file == file) {
|
||||
let edit = edits.next().expect("peeked edit");
|
||||
output.push_str(&source[start..edit.location.span.start]);
|
||||
output.push_str(&self.names[&edit.key.target]);
|
||||
start = edit.location.span.end;
|
||||
}
|
||||
output.push_str(&source[start..]);
|
||||
result.insert(file, output);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Private typed-only composition planning. The caller must supply its inherited
|
||||
/// parse Budget; this type keeps that same immutable budget through export.
|
||||
/// It does not establish the future complete bundle/file-table/parse-session
|
||||
/// witness. In particular, an Index currently does not store its parse profile.
|
||||
pub(super) struct Composition<'b, 'i, 's> {
|
||||
plan: Plan<'i, 's>,
|
||||
budget: &'b Budget,
|
||||
}
|
||||
impl std::fmt::Debug for Composition<'_, '_, '_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Composition")
|
||||
.field("plan", &self.plan)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
impl<'b, 'i, 's> Composition<'b, 'i, 's> {
|
||||
pub fn renames(
|
||||
index: &'i Index<'s>,
|
||||
renames: &BTreeMap<Target, String>,
|
||||
inherited: &'b Budget,
|
||||
) -> Result<Self, Error> {
|
||||
let plan = Plan::prepare(index, renames, inherited)?;
|
||||
plan.validate(index.sources(), inherited)?;
|
||||
Ok(Self {
|
||||
plan,
|
||||
budget: inherited,
|
||||
})
|
||||
}
|
||||
|
||||
/// Export the entire validated plan against exact original numeric file keys
|
||||
/// and bytes. There is no output allocation, application or subset-accept API.
|
||||
/// A future collector must check this actual index witness and the complete
|
||||
/// combined output bounds before applying either producer's edits once.
|
||||
pub fn original_edits(
|
||||
&self,
|
||||
current: &BTreeMap<usize, String>,
|
||||
) -> Result<OriginalEdits<'_, 'i, 's>, Error> {
|
||||
Plan::validate_names(self.plan.index, &self.plan.names, self.budget)?;
|
||||
self.plan.validate(current, self.budget)?;
|
||||
Ok(OriginalEdits { plan: &self.plan })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct OriginalEdits<'p, 'i, 's> {
|
||||
plan: &'p Plan<'i, 's>,
|
||||
}
|
||||
impl<'s> OriginalEdits<'_, '_, 's> {
|
||||
pub fn index(&self) -> &Index<'s> {
|
||||
self.plan.index
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.plan.edits.len()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.plan.edits.is_empty()
|
||||
}
|
||||
pub fn iter(&self) -> impl ExactSizeIterator<Item = OriginalEdit<'_>> {
|
||||
self.plan.edits.iter().map(|edit| OriginalEdit {
|
||||
key: &edit.key,
|
||||
location: edit.location,
|
||||
original: &self.plan.index.sources()[&edit.location.file]
|
||||
[edit.location.span.start..edit.location.span.end],
|
||||
replacement: &self.plan.names[&edit.key.target],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A borrowed inspection record, never independent edit authority. Only an
|
||||
/// intact OriginalEdits batch can carry this producer's complete rename intent.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct OriginalEdit<'p> {
|
||||
key: &'p OccurrenceKey,
|
||||
location: Location,
|
||||
original: &'p str,
|
||||
replacement: &'p str,
|
||||
}
|
||||
impl OriginalEdit<'_> {
|
||||
pub fn key(&self) -> &OccurrenceKey {
|
||||
self.key
|
||||
}
|
||||
pub fn location(&self) -> Location {
|
||||
self.location
|
||||
}
|
||||
pub fn original(&self) -> &str {
|
||||
self.original
|
||||
}
|
||||
pub fn replacement(&self) -> &str {
|
||||
self.replacement
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,744 @@
|
||||
use super::super::budget::Limits;
|
||||
use super::*;
|
||||
use crate::analysis as a;
|
||||
use crate::analysis_numeric::Exact;
|
||||
use crate::language::ParseLimits;
|
||||
use crate::{Id, Layer, Project};
|
||||
|
||||
const DEFINITIONS: &str = "// 雪 @f(a:) $a: comment spellings stay exact\r\n\
|
||||
calculation f \"f a b\" id \"00000000-0000-0000-0000-000000000100\" {\r\n\
|
||||
input a id \"00000000-0000-0000-0000-000000000200\" : Number;\r\n\
|
||||
input b id \"00000000-0000-0000-0000-000000000201\" : Number;\r\n\
|
||||
result Number; body $a + $b;\r\n}\r\n\
|
||||
calculation g \"g a b\" id \"00000000-0000-0000-0000-000000000200\" {\r\n\
|
||||
input a id \"00000000-0000-0000-0000-000000000200\" : Number;\r\n\
|
||||
input b id \"00000000-0000-0000-0000-000000000201\" : Number;\r\n\
|
||||
result Number; body $a + $b;\r\n}\r\n";
|
||||
const CALLER: &str = "calculation caller \"Caller\" id \"00000000-0000-0000-0000-000000000300\" {\n\
|
||||
result Number; body @f(b: 2, a: 1) // first 雪 @f(a:)\n\
|
||||
+ @f(a: 3, b: 4) // second\n\
|
||||
+ @g(a: 5, b: 6);\n}\n";
|
||||
fn budget() -> Budget {
|
||||
Budget::new(Limits::source8(ParseLimits::UNLIMITED)).unwrap()
|
||||
}
|
||||
fn global(id: u128) -> Target {
|
||||
Target::Global(Id::from_u128(id))
|
||||
}
|
||||
fn parameter(calculation: u128, parameter: u128) -> Target {
|
||||
Target::Parameter {
|
||||
calculation: Id::from_u128(calculation),
|
||||
parameter: a::ParameterId(Id::from_u128(parameter)),
|
||||
}
|
||||
}
|
||||
fn sources() -> BTreeMap<usize, String> {
|
||||
BTreeMap::from([(11, DEFINITIONS.into()), (23, CALLER.into())])
|
||||
}
|
||||
fn skeleton() -> Project {
|
||||
let mut project = super::super::model_oracle::project(Layer::Oa);
|
||||
project.definitions.clear();
|
||||
project
|
||||
.elements
|
||||
.retain(|_, element| !element.kind().is_analysis());
|
||||
project
|
||||
}
|
||||
fn binary(left: a::Expr, right: a::Expr) -> a::Expr {
|
||||
a::Expr::Binary {
|
||||
op: a::BinaryOp::Add,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
}
|
||||
}
|
||||
fn expected(renamed: bool) -> Project {
|
||||
// Independent model construction: no parsed AST, source index, writer or
|
||||
// generated edit participates in this expected complete Project.
|
||||
let mut project = skeleton();
|
||||
let p = |id| a::ParameterId(Id::from_u128(id));
|
||||
for (id, alias, name) in [
|
||||
(0x100, if renamed { "g" } else { "f" }, "f a b"),
|
||||
(0x200, if renamed { "f" } else { "g" }, "g a b"),
|
||||
] {
|
||||
let names = if renamed && id == 0x100 {
|
||||
["b", "a"]
|
||||
} else {
|
||||
["a", "b"]
|
||||
};
|
||||
let body = a::CalculationDefinition {
|
||||
parameters: [(0x200, names[0]), (0x201, names[1])]
|
||||
.into_iter()
|
||||
.map(|(id, name)| a::Parameter {
|
||||
id: p(id),
|
||||
name: name.into(),
|
||||
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
})
|
||||
.collect(),
|
||||
result: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
body: a::Binding::Expression(binary(
|
||||
a::Expr::Parameter(p(0x200)),
|
||||
a::Expr::Parameter(p(0x201)),
|
||||
)),
|
||||
};
|
||||
let id = Id::from_u128(id);
|
||||
project.definitions.insert(
|
||||
id,
|
||||
a::Definition {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
body: a::DefinitionBody::Calculation(body),
|
||||
},
|
||||
);
|
||||
}
|
||||
let call = |id, x, y| a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(Id::from_u128(id)),
|
||||
arguments: BTreeMap::from([
|
||||
(p(0x200), a::Expr::Number(Exact::from_integer(x))),
|
||||
(p(0x201), a::Expr::Number(Exact::from_integer(y))),
|
||||
]),
|
||||
};
|
||||
let id = Id::from_u128(0x300);
|
||||
project.definitions.insert(
|
||||
id,
|
||||
a::Definition {
|
||||
id,
|
||||
alias: "caller".into(),
|
||||
name: "Caller".into(),
|
||||
description: String::new(),
|
||||
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
|
||||
parameters: Vec::new(),
|
||||
result: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
body: a::Binding::Expression(binary(
|
||||
binary(call(0x100, 1, 2), call(0x100, 3, 4)),
|
||||
call(0x200, 5, 6),
|
||||
)),
|
||||
}),
|
||||
},
|
||||
);
|
||||
project
|
||||
}
|
||||
fn merge(mut project: Project, lowered: super::super::lower::Lowered) -> Project {
|
||||
project.definitions.extend(lowered.definitions);
|
||||
project.elements.extend(lowered.elements);
|
||||
project
|
||||
}
|
||||
fn rename_map() -> BTreeMap<Target, String> {
|
||||
BTreeMap::from([
|
||||
(global(0x100), "g".into()),
|
||||
(global(0x200), "f".into()),
|
||||
(parameter(0x100, 0x200), "b".into()),
|
||||
(parameter(0x100, 0x201), "a".into()),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simultaneous_global_and_scoped_swaps_match_original_token_and_whole_model_oracles() {
|
||||
let sources = sources();
|
||||
let mut work = budget();
|
||||
let (index, lowered) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
assert_eq!(merge(skeleton(), lowered), expected(false));
|
||||
let plan = Plan::renames(&index, &rename_map(), &work).unwrap();
|
||||
// List expected edits against the original authored bytes independently of
|
||||
// the index. Full contexts deliberately exclude equal strings/comments.
|
||||
let mut edits = Vec::new();
|
||||
let mut add = |file, context: &str, token: &str, replacement: &str| {
|
||||
let source = &sources[&file];
|
||||
let context_start = source.find(context).unwrap();
|
||||
let start = context_start + context.find(token).unwrap();
|
||||
edits.push((file, start, start + token.len(), replacement.to_owned()));
|
||||
};
|
||||
add(11, "calculation f ", "f", "g");
|
||||
add(11, "calculation g ", "g", "f");
|
||||
add(11, "input a id", "a", "b");
|
||||
add(11, "input b id", "b", "a");
|
||||
add(11, "body $a + $b;", "a", "b");
|
||||
add(11, "body $a + $b;", "b;", "a;");
|
||||
// The last oracle edit above includes ';' to disambiguate 'b' in 'body';
|
||||
// restrict it back to the exact name token for the span oracle.
|
||||
edits.last_mut().unwrap().2 -= 1;
|
||||
edits.last_mut().unwrap().3.pop();
|
||||
for call in ["@f(b: 2, a: 1)", "@f(a: 3, b: 4)"] {
|
||||
add_call_edits(&sources, &mut edits, call);
|
||||
}
|
||||
let start = CALLER.find("@g(a: 5, b: 6)").unwrap() + 1;
|
||||
edits.push((23, start, start + 1, "f".into()));
|
||||
edits.sort_by_key(|edit| (edit.0, edit.1));
|
||||
assert_eq!(plan.edit_count(), 13);
|
||||
assert_eq!(
|
||||
plan.edits
|
||||
.iter()
|
||||
.map(|edit| (
|
||||
edit.location.file,
|
||||
edit.location.span.start,
|
||||
edit.location.span.end,
|
||||
plan.names[&edit.key.target].clone()
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
edits
|
||||
);
|
||||
let mut exact = BTreeMap::new();
|
||||
for (&file, source) in &sources {
|
||||
let mut output = source.clone();
|
||||
for (_, start, end, value) in edits.iter().filter(|edit| edit.0 == file).rev() {
|
||||
output.replace_range(*start..*end, value);
|
||||
}
|
||||
exact.insert(file, output);
|
||||
}
|
||||
let output = plan.apply(&sources, &work).unwrap();
|
||||
assert_eq!(output, exact);
|
||||
assert!(output[&11].contains("// 雪 @f(a:) $a: comment spellings stay exact\r\n"));
|
||||
assert!(output[&23].contains("// first 雪 @f(a:)\n"));
|
||||
assert!(output[&11].contains("\"f a b\""));
|
||||
let (_, lowered) = Index::parse(&skeleton(), &output, &mut budget()).unwrap();
|
||||
let actual = merge(skeleton(), lowered);
|
||||
assert_eq!(actual, expected(true));
|
||||
a::admit(&actual, a::AdmissionLimits::REFERENCE).unwrap();
|
||||
}
|
||||
fn add_call_edits(
|
||||
sources: &BTreeMap<usize, String>,
|
||||
edits: &mut Vec<(usize, usize, usize, String)>,
|
||||
call: &str,
|
||||
) {
|
||||
let start = sources[&23].find(call).unwrap();
|
||||
for (offset, replacement) in [
|
||||
(1, "g"),
|
||||
(call.find("a:").unwrap(), "b"),
|
||||
(call.find("b:").unwrap(), "a"),
|
||||
] {
|
||||
edits.push((23, start + offset, start + offset + 1, replacement.into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_edits_preserve_unit_type_override_tokens_and_unrelated_authored_bytes() {
|
||||
let source = include_str!("../lower/fixtures/typed-oa.arcadia")
|
||||
.replace("750 [@millisecond]", "750 // exact 雪\r\n [@millisecond]");
|
||||
let files = BTreeMap::from([(59, source.clone())]);
|
||||
let mut work = budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &files, &mut work).unwrap();
|
||||
let mut expected_project = super::super::model_oracle::project(Layer::Oa);
|
||||
let id = |name: &str| {
|
||||
index
|
||||
.names()
|
||||
.iter()
|
||||
.find(|(target, alias)| matches!(target, Target::Global(_)) && alias.as_str() == name)
|
||||
.map(|(target, _)| *target)
|
||||
.unwrap()
|
||||
};
|
||||
let renames = BTreeMap::from([
|
||||
(id("duration"), "time_kind".into()),
|
||||
(id("millisecond"), "ms_unit".into()),
|
||||
(id("receive_time"), "arrival".into()),
|
||||
]);
|
||||
let plan = Plan::renames(&index, &renames, &work).unwrap();
|
||||
let output = plan.apply(&files, &work).unwrap();
|
||||
let expected_source = source
|
||||
.replace("quantity_kind duration ", "quantity_kind time_kind ")
|
||||
.replace("@duration", "@time_kind")
|
||||
.replace("unit millisecond ", "unit ms_unit ")
|
||||
.replace("@millisecond", "@ms_unit")
|
||||
.replace("oa_property receive_time ", "oa_property arrival ")
|
||||
.replace("@receive_time", "@arrival");
|
||||
assert_eq!(output[&59], expected_source);
|
||||
for definition in expected_project.definitions.values_mut() {
|
||||
if let Some(name) = renames.get(&Target::Global(definition.id)) {
|
||||
definition.alias = name.clone();
|
||||
}
|
||||
}
|
||||
for element in expected_project.elements.values_mut() {
|
||||
if let Some(name) = renames.get(&Target::Global(element.id)) {
|
||||
element.alias = name.clone();
|
||||
}
|
||||
}
|
||||
let (_, lowered) = Index::parse(&skeleton(), &output, &mut budget()).unwrap();
|
||||
assert_eq!(merge(skeleton(), lowered), expected_project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_rejects_unknown_invalid_and_colliding_final_names_but_allows_noop() {
|
||||
let sources = sources();
|
||||
let work = &mut budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, work).unwrap();
|
||||
for renames in [
|
||||
BTreeMap::from([(global(999), "new".into())]),
|
||||
BTreeMap::from([(global(0x100), "bad name".into())]),
|
||||
BTreeMap::from([(global(0x100), "g".into())]),
|
||||
BTreeMap::from([(parameter(0x100, 0x200), "b".into())]),
|
||||
] {
|
||||
assert!(Plan::renames(&index, &renames, work).is_err());
|
||||
}
|
||||
let plan = Plan::renames(&index, &BTreeMap::from([(global(0x100), "f".into())]), work).unwrap();
|
||||
assert_eq!(plan.edit_count(), 0);
|
||||
assert_eq!(plan.apply(&sources, work).unwrap(), sources);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_file_sets_bytes_forged_spans_and_duplicate_edits_fail_before_output() {
|
||||
let sources = sources();
|
||||
let mut work = budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
for damage in 0..6 {
|
||||
let mut current = sources.clone();
|
||||
let mut plan = Plan::renames(&index, &rename_map(), &work).unwrap();
|
||||
match damage {
|
||||
0 => current
|
||||
.get_mut(&23)
|
||||
.unwrap()
|
||||
.push_str("// unrelated change"),
|
||||
1 => {
|
||||
current.remove(&23);
|
||||
}
|
||||
2 => {
|
||||
current.insert(99, String::new());
|
||||
}
|
||||
3 => plan.edits[0].location.span.start += 1,
|
||||
4 => plan.edits.insert(1, plan.edits[0].clone()),
|
||||
_ => {
|
||||
plan.edits.pop();
|
||||
}
|
||||
}
|
||||
assert!(plan.apply(¤t, &work).is_err(), "damage {damage}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_and_plan_limits_are_aggregate_and_do_not_reset_parse_work() {
|
||||
let sources = sources();
|
||||
let mut work = budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
let usage = work.usage;
|
||||
let numeric = work.numeric_units();
|
||||
let plan = Plan::renames(&index, &rename_map(), &work).unwrap();
|
||||
let output = plan.apply(&sources, &work).unwrap();
|
||||
assert_eq!(work.usage, usage);
|
||||
assert_eq!(work.numeric_units(), numeric);
|
||||
let bytes = output.values().map(String::len).sum();
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.bytes = bytes;
|
||||
assert!(plan.apply(&sources, &Budget::new(limits).unwrap()).is_ok());
|
||||
limits.bytes -= 1;
|
||||
assert!(plan.apply(&sources, &Budget::new(limits).unwrap()).is_err());
|
||||
limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.references = 0;
|
||||
limits.entries = 0;
|
||||
assert!(Plan::renames(&index, &rename_map(), &Budget::new(limits).unwrap()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_exports_complete_borrowed_swap_witnesses_and_empty_noops() {
|
||||
use super::super::index::TokenRole;
|
||||
use super::super::lower::{Child, Field, Role};
|
||||
type Key = (Id, Option<Field>, Option<Vec<Child>>, TokenRole, Target);
|
||||
type Row = (Location, String, String, Key);
|
||||
|
||||
let sources = sources();
|
||||
let mut work = budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
let usage = work.usage;
|
||||
let numeric = work.numeric_units();
|
||||
let composition = Composition::renames(&index, &rename_map(), &work).unwrap();
|
||||
assert!(std::ptr::eq(composition.budget, &work));
|
||||
// A byte-identical snapshot is valid; records still borrow the actual index.
|
||||
let batch = composition.original_edits(&sources.clone()).unwrap();
|
||||
assert!(std::ptr::eq(batch.index(), &index));
|
||||
assert_eq!(batch.len(), 13);
|
||||
assert!(!batch.is_empty());
|
||||
assert_eq!(batch.iter().len(), 13);
|
||||
|
||||
// Independently authored occurrence identities and full source contexts.
|
||||
// No index offset/path or generated output defines this oracle.
|
||||
let mut expected: Vec<Row> = Vec::new();
|
||||
let mut add = |file, context: &str, prefix: &str, old: &str, new: &str, key: Key| {
|
||||
let start = sources[&file].find(context).unwrap() + prefix.len();
|
||||
assert_eq!(&sources[&file][start..start + old.len()], old);
|
||||
expected.push((
|
||||
Location {
|
||||
file,
|
||||
span: Span {
|
||||
start,
|
||||
end: start + old.len(),
|
||||
},
|
||||
},
|
||||
old.into(),
|
||||
new.into(),
|
||||
key,
|
||||
));
|
||||
};
|
||||
for (id, old, new, context) in [
|
||||
(0x100, "f", "g", "calculation f "),
|
||||
(0x200, "g", "f", "calculation g "),
|
||||
] {
|
||||
add(
|
||||
11,
|
||||
context,
|
||||
"calculation ",
|
||||
old,
|
||||
new,
|
||||
(
|
||||
Id::from_u128(id),
|
||||
None,
|
||||
None,
|
||||
TokenRole::Declaration,
|
||||
global(id),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (id, old, new, context, edge, prefix) in [
|
||||
(0x200, "a", "b", "input a id", Child::Left, "body $"),
|
||||
(0x201, "b", "a", "input b id", Child::Right, "body $a + $"),
|
||||
] {
|
||||
let target = parameter(0x100, id);
|
||||
add(
|
||||
11,
|
||||
context,
|
||||
"input ",
|
||||
old,
|
||||
new,
|
||||
(
|
||||
Id::from_u128(0x100),
|
||||
Some(Field::Input(a::ParameterId(Id::from_u128(id)))),
|
||||
None,
|
||||
TokenRole::Use(Role::ParameterDeclaration),
|
||||
target,
|
||||
),
|
||||
);
|
||||
add(
|
||||
11,
|
||||
"body $a + $b;",
|
||||
prefix,
|
||||
old,
|
||||
new,
|
||||
(
|
||||
Id::from_u128(0x100),
|
||||
Some(Field::Body),
|
||||
Some(vec![edge]),
|
||||
TokenRole::Use(Role::ParameterUse),
|
||||
target,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (context, path) in [
|
||||
("@f(b: 2, a: 1)", vec![Child::Left, Child::Left]),
|
||||
("@f(a: 3, b: 4)", vec![Child::Left, Child::Right]),
|
||||
] {
|
||||
add(
|
||||
23,
|
||||
context,
|
||||
"@",
|
||||
"f",
|
||||
"g",
|
||||
(
|
||||
Id::from_u128(0x300),
|
||||
Some(Field::Body),
|
||||
Some(path.clone()),
|
||||
TokenRole::Use(Role::CallTarget),
|
||||
global(0x100),
|
||||
),
|
||||
);
|
||||
for (id, old, new) in [(0x200, "a", "b"), (0x201, "b", "a")] {
|
||||
let offset = context.find(&format!("{old}:")).unwrap();
|
||||
add(
|
||||
23,
|
||||
context,
|
||||
&context[..offset],
|
||||
old,
|
||||
new,
|
||||
(
|
||||
Id::from_u128(0x300),
|
||||
Some(Field::Body),
|
||||
Some(path.clone()),
|
||||
TokenRole::Use(Role::ArgumentLabel),
|
||||
parameter(0x100, id),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
add(
|
||||
23,
|
||||
"@g(a: 5, b: 6)",
|
||||
"@",
|
||||
"g",
|
||||
"f",
|
||||
(
|
||||
Id::from_u128(0x300),
|
||||
Some(Field::Body),
|
||||
Some(vec![Child::Right]),
|
||||
TokenRole::Use(Role::CallTarget),
|
||||
global(0x200),
|
||||
),
|
||||
);
|
||||
expected.sort_by_key(|row| (row.0.file, row.0.span.start, row.0.span.end));
|
||||
let actual: Vec<Row> = batch
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let key = edit.key();
|
||||
(
|
||||
edit.location(),
|
||||
edit.original().into(),
|
||||
edit.replacement().into(),
|
||||
(
|
||||
key.owner,
|
||||
key.field,
|
||||
key.path.as_ref().map(|path| path.edges()),
|
||||
key.role,
|
||||
key.target,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let noop = Composition::renames(
|
||||
&index,
|
||||
&BTreeMap::from([
|
||||
(global(0x100), "f".into()),
|
||||
(parameter(0x200, 0x200), "a".into()),
|
||||
]),
|
||||
&work,
|
||||
)
|
||||
.unwrap();
|
||||
let empty = noop.original_edits(&sources).unwrap();
|
||||
assert!(empty.is_empty());
|
||||
assert_eq!(empty.len(), 0);
|
||||
assert_eq!(empty.iter().count(), 0);
|
||||
assert_eq!(work.usage, usage);
|
||||
assert_eq!(work.numeric_units(), numeric);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_rejects_stale_or_damaged_complete_batches_before_export() {
|
||||
let mut sources = sources();
|
||||
sources.insert(99, "// unrelated 雪\r\n".into());
|
||||
let original = sources.clone();
|
||||
let mut work = budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
for damage in 0..10 {
|
||||
let mut current = sources.clone();
|
||||
let mut composition = Composition::renames(&index, &rename_map(), &work).unwrap();
|
||||
match damage {
|
||||
0 => current.get_mut(&99).unwrap().push_str("changed"),
|
||||
1 => {
|
||||
current.remove(&23);
|
||||
}
|
||||
2 => {
|
||||
current.insert(101, String::new());
|
||||
}
|
||||
3 => composition.plan.edits[0].location.span.start += 1,
|
||||
4 => composition
|
||||
.plan
|
||||
.edits
|
||||
.insert(1, composition.plan.edits[0].clone()),
|
||||
5 => {
|
||||
composition.plan.edits.pop();
|
||||
}
|
||||
6 => composition.plan.edits.swap(0, 1),
|
||||
7 => {
|
||||
composition
|
||||
.plan
|
||||
.names
|
||||
.insert(global(0x100), "bad name".into());
|
||||
}
|
||||
8 => {
|
||||
composition.plan.names.insert(global(0x100), "f".into());
|
||||
}
|
||||
_ => {
|
||||
composition.plan.names.insert(global(999), "missing".into());
|
||||
}
|
||||
}
|
||||
let before = current.clone();
|
||||
assert!(
|
||||
composition.original_edits(¤t).is_err(),
|
||||
"damage {damage}"
|
||||
);
|
||||
assert_eq!(current, before);
|
||||
}
|
||||
assert_eq!(sources, original);
|
||||
let failure = work.frame(257, Span::default()).unwrap_err();
|
||||
assert_eq!(
|
||||
Composition::renames(&index, &rename_map(), &work).unwrap_err(),
|
||||
failure
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_defers_only_typed_output_size_with_independent_shrink_arithmetic() {
|
||||
let sources = sources();
|
||||
let cap = sources.values().map(String::len).sum();
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.bytes = cap;
|
||||
let mut work = Budget::new(limits).unwrap();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
assert_eq!(work.usage.bytes, cap);
|
||||
let usage = work.usage;
|
||||
let numeric = work.numeric_units();
|
||||
let names = BTreeMap::from([(global(0x100), "ff".into())]);
|
||||
assert_eq!(
|
||||
Plan::renames(&index, &names, &work).unwrap_err().message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
let composition = Composition::renames(&index, &names, &work).unwrap();
|
||||
let batch = composition.original_edits(&sources).unwrap();
|
||||
assert_eq!(batch.len(), 3); // declaration and two distinct call targets
|
||||
let growth: usize = batch
|
||||
.iter()
|
||||
.map(|edit| edit.replacement().len() - edit.original().len())
|
||||
.sum();
|
||||
assert_eq!(growth, 3);
|
||||
assert_eq!(
|
||||
composition.plan.apply(&sources, &work).unwrap_err().message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
|
||||
// Independent future ordinary/scalar producer witness on untouched display
|
||||
// name bytes. This is size/offset arithmetic only: this typed-only harness
|
||||
// has no ordinary collector, mixed ownership proof or accepted output.
|
||||
let start = DEFINITIONS.find("\"f a b\"").unwrap() + 1;
|
||||
let shrink = Location {
|
||||
file: 11,
|
||||
span: Span {
|
||||
start,
|
||||
end: start + "f a b".len(),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
&sources[&shrink.file][shrink.span.start..shrink.span.end],
|
||||
"f a b"
|
||||
);
|
||||
for edit in batch.iter() {
|
||||
let at = edit.location();
|
||||
assert!(
|
||||
at.file != shrink.file
|
||||
|| at.span.end <= shrink.span.start
|
||||
|| shrink.span.end <= at.span.start
|
||||
);
|
||||
}
|
||||
let original_len = shrink.span.end - shrink.span.start;
|
||||
assert_eq!(cap + growth - original_len + "fa".len(), cap);
|
||||
assert_eq!(cap + growth - original_len + "fab".len(), cap + 1);
|
||||
assert_eq!(work.usage, usage);
|
||||
assert_eq!(work.numeric_units(), numeric);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_keeps_name_storage_and_rename_record_limits_before_allocation() {
|
||||
let sources = sources();
|
||||
let cap = sources.values().map(String::len).sum();
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.bytes = cap;
|
||||
let mut work = Budget::new(limits).unwrap();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
let exact = BTreeMap::from([(global(0x100), "x".repeat(cap))]);
|
||||
assert_eq!(
|
||||
Composition::renames(&index, &exact, &work)
|
||||
.unwrap()
|
||||
.original_edits(&sources)
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
let over = BTreeMap::from([(global(0x100), "x".repeat(cap + 1))]);
|
||||
assert_eq!(
|
||||
Composition::renames(&index, &over, &work)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
for invalid in [
|
||||
BTreeMap::from([(global(999), "unknown".into())]),
|
||||
BTreeMap::from([(global(0x100), "bad name".into())]),
|
||||
BTreeMap::from([(global(0x100), "g".into())]),
|
||||
BTreeMap::from([(parameter(0x100, 0x200), "b".into())]),
|
||||
] {
|
||||
assert!(Composition::renames(&index, &invalid, &work).is_err());
|
||||
}
|
||||
|
||||
// A typed-empty fixture isolates the rename-map record bound without
|
||||
// syntax entry usage masking it. No ordinary edits are authorized here.
|
||||
let ordinary = skeleton();
|
||||
let count = ordinary.identities().len();
|
||||
let files = BTreeMap::from([(1, "// empty typed unit\n".into())]);
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.entries = count;
|
||||
limits.references = 0;
|
||||
let mut inherited = Budget::new(limits).unwrap();
|
||||
let (index, _) = Index::parse(&ordinary, &files, &mut inherited).unwrap();
|
||||
let mut names: BTreeMap<_, _> = index
|
||||
.names()
|
||||
.keys()
|
||||
.enumerate()
|
||||
.map(|(i, target)| (*target, format!("renamed_{i}")))
|
||||
.collect();
|
||||
assert_eq!(names.len(), count);
|
||||
assert!(
|
||||
Composition::renames(&index, &names, &inherited)
|
||||
.unwrap()
|
||||
.original_edits(&files)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
names.insert(global(999), "additional".into());
|
||||
assert_eq!(
|
||||
Composition::renames(&index, &names, &inherited)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Source8 lowering entry limit exceeded."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_checks_edit_record_ceiling_before_duplicate_membership() {
|
||||
let sources = sources();
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.entries = 128;
|
||||
limits.references = 32;
|
||||
let ceiling = limits.entries + limits.references;
|
||||
let mut work = Budget::new(limits).unwrap();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut work).unwrap();
|
||||
let mut composition = Composition::renames(&index, &rename_map(), &work).unwrap();
|
||||
assert_eq!(composition.original_edits(&sources).unwrap().len(), 13);
|
||||
let duplicate = composition.plan.edits[0].clone();
|
||||
composition.plan.edits = vec![duplicate.clone(); ceiling];
|
||||
// At the exact count bound validation proceeds to the independent ownership
|
||||
// error. The damaged plan never becomes a successful/authoritative batch.
|
||||
assert_eq!(
|
||||
composition.original_edits(&sources).unwrap_err().message,
|
||||
"Forged or duplicate edit occurrence."
|
||||
);
|
||||
composition.plan.edits.push(duplicate);
|
||||
assert_eq!(
|
||||
composition.original_edits(&sources).unwrap_err().message,
|
||||
"Source8 lowering occurrence limit exceeded."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_noop_apply_keeps_tighter_file_only_budget_and_output_boundary() {
|
||||
let sources = sources();
|
||||
let mut inherited = budget();
|
||||
let (index, _) = Index::parse(&skeleton(), &sources, &mut inherited).unwrap();
|
||||
let plan = Plan::renames(
|
||||
&index,
|
||||
&BTreeMap::from([(global(0x100), "f".into())]),
|
||||
&inherited,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plan.edit_count(), 0);
|
||||
assert!(index.names().len() > sources.len());
|
||||
let mut limits = Limits::source8(ParseLimits::UNLIMITED);
|
||||
limits.entries = sources.len();
|
||||
limits.references = 0;
|
||||
limits.bytes = sources.values().map(String::len).sum();
|
||||
// Standalone apply checks file/edit records, not the already validated
|
||||
// catalog/name-storage allocation from construction. Rechecking the catalog
|
||||
// under this intentionally tighter probe budget would reject this no-op.
|
||||
assert_eq!(
|
||||
plan.apply(&sources, &Budget::new(limits).unwrap()).unwrap(),
|
||||
sources
|
||||
);
|
||||
limits.bytes -= 1;
|
||||
assert_eq!(
|
||||
plan.apply(&sources, &Budget::new(limits).unwrap())
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"Source8 index/output byte limit exceeded."
|
||||
);
|
||||
}
|
||||
@@ -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,6 +194,8 @@ 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()?;
|
||||
let facet = serde_json::from_value(raw(&ast).ok()?).ok()?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
mod source_migration;
|
||||
#[path = "source_project/source_structure.rs"]
|
||||
mod source_structure;
|
||||
pub(crate) mod traversal;
|
||||
|
||||
/// A portable slash-separated path, checked at every bundle boundary.
|
||||
pub type LogicalPath = String;
|
||||
@@ -304,71 +305,7 @@ fn compile_once(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<(CompiledSourceProject, Edits), Vec<SourceDiagnostic>> {
|
||||
let fail = |code, message| {
|
||||
vec![problem(
|
||||
bundle,
|
||||
&bundle.entry,
|
||||
Span::default(),
|
||||
code,
|
||||
message,
|
||||
)]
|
||||
};
|
||||
if bundle.entry.contains('/') {
|
||||
return Err(fail(
|
||||
"invalid_path",
|
||||
"The entry file must be at the source project root.".into(),
|
||||
));
|
||||
}
|
||||
if bundle.files.len() > limits.max_files {
|
||||
return Err(fail(
|
||||
"project_limit",
|
||||
format!("Source project exceeds {} files.", limits.max_files),
|
||||
));
|
||||
}
|
||||
let bytes = bundle
|
||||
.files
|
||||
.values()
|
||||
.fold(0usize, |sum, text| sum.saturating_add(text.len()));
|
||||
if bytes > limits.model.max_source_bytes {
|
||||
return Err(fail(
|
||||
"source_limit",
|
||||
format!(
|
||||
"Combined source exceeds {} bytes.",
|
||||
limits.model.max_source_bytes
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut folded = BTreeMap::new();
|
||||
for path in std::iter::once(&bundle.entry).chain(bundle.files.keys()) {
|
||||
if resolve_include("", path).as_ref() != Ok(path) {
|
||||
return Err(fail(
|
||||
"invalid_path",
|
||||
format!("Non-canonical source path '{path}'."),
|
||||
));
|
||||
}
|
||||
if let Some(previous) = folded.insert(path.to_lowercase(), path)
|
||||
&& previous != path
|
||||
{
|
||||
return Err(fail(
|
||||
"duplicate_path",
|
||||
format!("Case-colliding source paths '{previous}' and '{path}'."),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !bundle.files.contains_key(&bundle.entry) {
|
||||
return Err(fail(
|
||||
"missing_include",
|
||||
"The entry file is missing from the source bundle.".into(),
|
||||
));
|
||||
}
|
||||
|
||||
enum Visit {
|
||||
Enter(LogicalPath, usize, Option<SourceLocation>),
|
||||
Leave,
|
||||
}
|
||||
let mut stack = vec![Visit::Enter(bundle.entry.clone(), 0, None)];
|
||||
let mut active: Vec<LogicalPath> = Vec::new();
|
||||
let mut visited = BTreeSet::new();
|
||||
let mut traversal = traversal::Traversal::new(bundle, limits)?;
|
||||
let mut declarations = Vec::new();
|
||||
let mut declaration_paths = Vec::new();
|
||||
let mut aliases = BTreeMap::new();
|
||||
@@ -381,51 +318,8 @@ fn compile_once(
|
||||
let mut includes = Vec::new();
|
||||
let mut insertions: Edits = BTreeMap::new();
|
||||
let mut graph_count = 0usize;
|
||||
while let Some(visit) = stack.pop() {
|
||||
let Visit::Enter(path, depth, origin) = visit else {
|
||||
active.pop();
|
||||
continue;
|
||||
};
|
||||
let at = origin.unwrap_or(SourceLocation {
|
||||
path: path.clone(),
|
||||
span: Span::default(),
|
||||
});
|
||||
if let Some(start) = active.iter().position(|file| file == &path) {
|
||||
let chain = active[start..]
|
||||
.iter()
|
||||
.chain(std::iter::once(&path))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" -> ");
|
||||
return Err(vec![problem(
|
||||
bundle,
|
||||
&at.path,
|
||||
at.span,
|
||||
"include_cycle",
|
||||
format!("Include cycle: {chain}."),
|
||||
)]);
|
||||
}
|
||||
if depth > limits.max_include_depth {
|
||||
return Err(vec![problem(
|
||||
bundle,
|
||||
&at.path,
|
||||
at.span,
|
||||
"project_limit",
|
||||
format!("Include depth exceeds {}.", limits.max_include_depth),
|
||||
)]);
|
||||
}
|
||||
if !visited.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Some(source) = bundle.files.get(&path) else {
|
||||
return Err(vec![problem(
|
||||
bundle,
|
||||
&at.path,
|
||||
at.span,
|
||||
"missing_include",
|
||||
format!("Included file '{path}' is missing."),
|
||||
)]);
|
||||
};
|
||||
while let Some(path) = traversal.next()? {
|
||||
let source = &bundle.files[&path];
|
||||
let mut unit = language::parse_unit(source, path == bundle.entry, limits.model).map_err(
|
||||
|diagnostic| {
|
||||
vec![SourceDiagnostic {
|
||||
@@ -513,46 +407,12 @@ fn compile_once(
|
||||
declarations.push(declaration);
|
||||
declaration_paths.push(path.clone());
|
||||
}
|
||||
let mut children = Vec::new();
|
||||
for (raw, span) in unit.includes {
|
||||
let target = resolve_include(&path, &raw)
|
||||
.map_err(|message| vec![problem(bundle, &path, span, "invalid_path", message)])?;
|
||||
let location = SourceLocation {
|
||||
path: path.clone(),
|
||||
span,
|
||||
};
|
||||
includes.push(IncludeLocation {
|
||||
location: location.clone(),
|
||||
target: target.clone(),
|
||||
});
|
||||
if includes.len() > limits.max_include_edges {
|
||||
return Err(vec![problem(
|
||||
bundle,
|
||||
&path,
|
||||
span,
|
||||
"project_limit",
|
||||
format!("Include graph exceeds {} edges.", limits.max_include_edges),
|
||||
)]);
|
||||
}
|
||||
children.push(Visit::Enter(
|
||||
target,
|
||||
depth.saturating_add(1),
|
||||
Some(location),
|
||||
));
|
||||
includes.push(traversal.include(&path, &raw, span)?);
|
||||
}
|
||||
active.push(path);
|
||||
stack.push(Visit::Leave);
|
||||
stack.extend(children.into_iter().rev());
|
||||
}
|
||||
if let Some(path) = bundle.files.keys().find(|path| !visited.contains(*path)) {
|
||||
return Err(vec![problem(
|
||||
bundle,
|
||||
path,
|
||||
Span::default(),
|
||||
"unreachable_file",
|
||||
"Supplied source file is not reachable from the entry file.",
|
||||
)]);
|
||||
traversal.finish_file(path);
|
||||
}
|
||||
traversal.finish()?;
|
||||
let header = project_header.expect("validated entry header");
|
||||
let index = SourceIndex {
|
||||
declarations: declaration_locations,
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
//! Shared include traversal policy for ordinary compilation and private syntax.
|
||||
use super::*;
|
||||
|
||||
enum Visit {
|
||||
Enter(LogicalPath, usize, Option<SourceLocation>),
|
||||
Leave,
|
||||
}
|
||||
|
||||
pub(crate) struct Traversal<'a> {
|
||||
bundle: &'a SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
stack: Vec<Visit>,
|
||||
active: Vec<LogicalPath>,
|
||||
visited: BTreeSet<LogicalPath>,
|
||||
children: Vec<Visit>,
|
||||
depth: usize,
|
||||
edges: usize,
|
||||
}
|
||||
|
||||
impl<'a> Traversal<'a> {
|
||||
pub(crate) fn new(
|
||||
bundle: &'a SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
) -> Result<Self, Vec<SourceDiagnostic>> {
|
||||
let fail = |code, message| {
|
||||
vec![problem(
|
||||
bundle,
|
||||
&bundle.entry,
|
||||
Span::default(),
|
||||
code,
|
||||
message,
|
||||
)]
|
||||
};
|
||||
if bundle.entry.contains('/') {
|
||||
return Err(fail(
|
||||
"invalid_path",
|
||||
"The entry file must be at the source project root.".into(),
|
||||
));
|
||||
}
|
||||
if bundle.files.len() > limits.max_files {
|
||||
return Err(fail(
|
||||
"project_limit",
|
||||
format!("Source project exceeds {} files.", limits.max_files),
|
||||
));
|
||||
}
|
||||
let bytes = bundle
|
||||
.files
|
||||
.values()
|
||||
.fold(0usize, |sum, text| sum.saturating_add(text.len()));
|
||||
if bytes > limits.model.max_source_bytes {
|
||||
return Err(fail(
|
||||
"source_limit",
|
||||
format!(
|
||||
"Combined source exceeds {} bytes.",
|
||||
limits.model.max_source_bytes
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut folded = BTreeMap::new();
|
||||
for path in std::iter::once(&bundle.entry).chain(bundle.files.keys()) {
|
||||
if resolve_include("", path).as_ref() != Ok(path) {
|
||||
return Err(fail(
|
||||
"invalid_path",
|
||||
format!("Non-canonical source path '{path}'."),
|
||||
));
|
||||
}
|
||||
if let Some(previous) = folded.insert(path.to_lowercase(), path)
|
||||
&& previous != path
|
||||
{
|
||||
return Err(fail(
|
||||
"duplicate_path",
|
||||
format!("Case-colliding source paths '{previous}' and '{path}'."),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !bundle.files.contains_key(&bundle.entry) {
|
||||
return Err(fail(
|
||||
"missing_include",
|
||||
"The entry file is missing from the source bundle.".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
bundle,
|
||||
limits,
|
||||
stack: vec![Visit::Enter(bundle.entry.clone(), 0, None)],
|
||||
active: Vec::new(),
|
||||
visited: BTreeSet::new(),
|
||||
children: Vec::new(),
|
||||
depth: 0,
|
||||
edges: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn next(&mut self) -> Result<Option<LogicalPath>, Vec<SourceDiagnostic>> {
|
||||
while let Some(visit) = self.stack.pop() {
|
||||
let Visit::Enter(path, depth, origin) = visit else {
|
||||
self.active.pop();
|
||||
continue;
|
||||
};
|
||||
let at = origin.unwrap_or(SourceLocation {
|
||||
path: path.clone(),
|
||||
span: Span::default(),
|
||||
});
|
||||
if let Some(start) = self.active.iter().position(|file| file == &path) {
|
||||
let chain = self.active[start..]
|
||||
.iter()
|
||||
.chain(std::iter::once(&path))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" -> ");
|
||||
return Err(vec![problem(
|
||||
self.bundle,
|
||||
&at.path,
|
||||
at.span,
|
||||
"include_cycle",
|
||||
format!("Include cycle: {chain}."),
|
||||
)]);
|
||||
}
|
||||
if depth > self.limits.max_include_depth {
|
||||
return Err(vec![problem(
|
||||
self.bundle,
|
||||
&at.path,
|
||||
at.span,
|
||||
"project_limit",
|
||||
format!("Include depth exceeds {}.", self.limits.max_include_depth),
|
||||
)]);
|
||||
}
|
||||
if !self.visited.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
if !self.bundle.files.contains_key(&path) {
|
||||
return Err(vec![problem(
|
||||
self.bundle,
|
||||
&at.path,
|
||||
at.span,
|
||||
"missing_include",
|
||||
format!("Included file '{path}' is missing."),
|
||||
)]);
|
||||
}
|
||||
self.depth = depth;
|
||||
return Ok(Some(path));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn include(
|
||||
&mut self,
|
||||
path: &str,
|
||||
raw: &str,
|
||||
span: Span,
|
||||
) -> Result<IncludeLocation, Vec<SourceDiagnostic>> {
|
||||
let target = resolve_include(path, raw)
|
||||
.map_err(|message| vec![problem(self.bundle, path, span, "invalid_path", message)])?;
|
||||
let location = SourceLocation {
|
||||
path: path.into(),
|
||||
span,
|
||||
};
|
||||
if self.edges >= self.limits.max_include_edges {
|
||||
return Err(vec![problem(
|
||||
self.bundle,
|
||||
path,
|
||||
span,
|
||||
"project_limit",
|
||||
format!(
|
||||
"Include graph exceeds {} edges.",
|
||||
self.limits.max_include_edges
|
||||
),
|
||||
)]);
|
||||
}
|
||||
self.edges += 1;
|
||||
self.children.push(Visit::Enter(
|
||||
target.clone(),
|
||||
self.depth.saturating_add(1),
|
||||
Some(location.clone()),
|
||||
));
|
||||
Ok(IncludeLocation { location, target })
|
||||
}
|
||||
|
||||
pub(crate) fn finish_file(&mut self, path: LogicalPath) {
|
||||
self.active.push(path);
|
||||
self.stack.push(Visit::Leave);
|
||||
self.stack.extend(self.children.drain(..).rev());
|
||||
}
|
||||
|
||||
pub(crate) fn finish(self) -> Result<(), Vec<SourceDiagnostic>> {
|
||||
if let Some(path) = self
|
||||
.bundle
|
||||
.files
|
||||
.keys()
|
||||
.find(|path| !self.visited.contains(*path))
|
||||
{
|
||||
return Err(vec![problem(
|
||||
self.bundle,
|
||||
path,
|
||||
Span::default(),
|
||||
"unreachable_file",
|
||||
"Supplied source file is not reachable from the entry file.",
|
||||
)]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ordinary_compile_once_keeps_include_order_and_cycle_source_location() {
|
||||
let mut bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), "arcadia 7; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" { include \"dir/left.arcadia\"; include \"dir/right.arcadia\"; }".into()),
|
||||
("dir/left.arcadia".into(), "include \"../shared.arcadia\";".into()),
|
||||
("dir/right.arcadia".into(), "include \"../shared.arcadia\";".into()),
|
||||
("shared.arcadia".into(), "// one shared ordinary fragment\n".into()),
|
||||
]),
|
||||
};
|
||||
let original = bundle.clone();
|
||||
// This is the existing initial ordinary pass, before schema migration's
|
||||
// separate output/reparse. It makes no public source8 acceptance claim.
|
||||
let (compiled, insertions) =
|
||||
super::super::compile_once(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert!(insertions.is_empty());
|
||||
assert_eq!(compiled.bundle, original);
|
||||
assert_eq!(compiled.index.declarations.len(), 1);
|
||||
assert_eq!(
|
||||
compiled
|
||||
.index
|
||||
.includes
|
||||
.iter()
|
||||
.map(|i| (i.location.path.as_str(), i.target.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
("main.arcadia", "dir/left.arcadia"),
|
||||
("main.arcadia", "dir/right.arcadia"),
|
||||
("dir/left.arcadia", "shared.arcadia"),
|
||||
("dir/right.arcadia", "shared.arcadia"),
|
||||
]
|
||||
);
|
||||
let cycle = "// 雪\r\ninclude \"main.arcadia\";";
|
||||
bundle.files.insert("shared.arcadia".into(), cycle.into());
|
||||
let before = bundle.clone();
|
||||
let errors = super::super::compile_once(&bundle, ProjectLimits::default()).unwrap_err();
|
||||
assert_eq!(errors[0].diagnostic.code, "include_cycle");
|
||||
assert_eq!(errors[0].path, "shared.arcadia");
|
||||
assert_eq!(errors[0].diagnostic.line, 2);
|
||||
assert_eq!(
|
||||
&cycle[errors[0].diagnostic.span.start..errors[0].diagnostic.span.end],
|
||||
"include \"main.arcadia\";"
|
||||
);
|
||||
assert_eq!(
|
||||
errors[0].diagnostic.message,
|
||||
"Include cycle: main.arcadia -> dir/left.arcadia -> shared.arcadia -> main.arcadia."
|
||||
);
|
||||
assert_eq!(bundle, before);
|
||||
}
|
||||
}
|
||||
@@ -180,14 +180,96 @@ fn check_ids(out: &mut Vec<Issue>, owner: Id, field: &str, ids: &[Id]) {
|
||||
}
|
||||
|
||||
pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
validate_with_usage(p).0
|
||||
}
|
||||
|
||||
/// Runs the shared static/architectural route once and retains successful static
|
||||
/// metadata without retaining a Project-borrowing environment. Architectural
|
||||
/// issue production is outside the static meter and any caller auxiliary ledger;
|
||||
/// callers must reserve separately before mapping or retaining these issues.
|
||||
pub(crate) fn validate_with_usage(
|
||||
p: &Project,
|
||||
) -> (
|
||||
Vec<Issue>,
|
||||
Option<(
|
||||
crate::analysis::AdmissionLimits,
|
||||
crate::analysis::AdmissionUsage,
|
||||
)>,
|
||||
) {
|
||||
// Bound analysis trees before architectural visitors can clone an element.
|
||||
// Admission itself never enters this public validator recursively.
|
||||
if let Err(failure) = crate::analysis::admit_structure_checked(p, crate::analysis::AdmissionLimits::REFERENCE) {
|
||||
let mut out = Vec::new();
|
||||
error(&mut out, failure.object, &failure.code, &failure.field, failure.message);
|
||||
return out;
|
||||
let environment = match crate::analysis::admit_structure_checked(
|
||||
p,
|
||||
crate::analysis::AdmissionLimits::REFERENCE,
|
||||
) {
|
||||
Ok(environment) => environment,
|
||||
Err(failure) => {
|
||||
let mut out = Vec::new();
|
||||
error(
|
||||
&mut out,
|
||||
failure.object,
|
||||
&failure.code,
|
||||
&failure.field,
|
||||
failure.message,
|
||||
);
|
||||
return (out, None);
|
||||
}
|
||||
};
|
||||
let metadata = (
|
||||
environment.admission_limits(),
|
||||
environment.admission_usage(),
|
||||
);
|
||||
drop(environment);
|
||||
(validate_structure(p), Some(metadata))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod shared_admission_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn successful_static_metadata_survives_architectural_warnings() {
|
||||
let mut project = Project::empty();
|
||||
project.name.clear();
|
||||
let (issues, metadata) = validate_with_usage(&project);
|
||||
let (limits, usage) = metadata.expect("static admission succeeds");
|
||||
assert_eq!(limits, crate::analysis::AdmissionLimits::REFERENCE);
|
||||
assert!(usage.static_events > 0);
|
||||
assert_eq!(usage.stored_expression_nodes, 0);
|
||||
assert!(issues.iter().any(|issue| issue.code == "unnamed"
|
||||
&& issue.element == Some(project.id)
|
||||
&& issue.severity == Severity::Warning));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_failure_returns_no_metadata_and_skips_architectural_warnings() {
|
||||
use crate::analysis::{
|
||||
AnalysisType, Binding, CalculationDefinition, Definition, DefinitionBody, ScalarType,
|
||||
};
|
||||
let mut project = Project::empty();
|
||||
project.name.clear();
|
||||
let key = Id::from_u128(10);
|
||||
project.definitions.insert(
|
||||
key,
|
||||
Definition {
|
||||
id: Id::from_u128(11),
|
||||
alias: "f".into(),
|
||||
name: "F".into(),
|
||||
description: String::new(),
|
||||
body: DefinitionBody::Calculation(CalculationDefinition {
|
||||
parameters: Vec::new(),
|
||||
result: AnalysisType::Scalar(ScalarType::Number),
|
||||
body: Binding::Unbound,
|
||||
}),
|
||||
},
|
||||
);
|
||||
let (issues, metadata) = validate_with_usage(&project);
|
||||
assert!(metadata.is_none());
|
||||
assert_eq!(issues.len(), 1);
|
||||
assert_eq!(issues[0].severity, Severity::Error);
|
||||
assert_eq!(issues[0].element, Some(key));
|
||||
assert_eq!(issues[0].code, "identity_mismatch");
|
||||
}
|
||||
validate_structure(p)
|
||||
}
|
||||
|
||||
/// Architecture/header validation without recursively re-entering analysis admission.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Private carried-baseline phase-check implementation
|
||||
|
||||
2026-09-25. Based on `18073a6f2eabce83fc83997f094e331eec835a16`; implements the limited prerequisite selected in `harness/analysis-carried-phase-contract-20260925.md` (parent evidence commit `a2c781773454ee932a9e292e6dc93f551f9f86c5`). Source and tests are authored; compilation/runtime acceptance belong to parent gates and are not claimed here.
|
||||
|
||||
`mixed/carried.rs` introduces an opaque non-Clone input backed by either a fixed borrowed bundle with captured policy or a borrowed allocation-owning RetainedSnapshot. Every new constructor is `cfg(test)`. The retained fixture owns the actual complete SourceBundle and computes exactly F map slots, zero lower occurrence slots and all source/key/entry UTF-8 bytes. The parser cannot substitute a different bundle at invocation. There is no production emitter/old-owner transition, accepted-output result, public source8 activation or editing authority.
|
||||
|
||||
The shared parser implementation has explicit legacy versus carried mode. Existing entry functions choose legacy and preserve their separate metadata checkpoints. Carried mode checks C initially, C+file metadata before traversal, C+U+M before every position projection and at complete syntax return, even when C is zero. Checks use existing logical Slots arithmetic and inherited ceilings; they are not additional retained reservations. A narrow read-only Budget policy getter preserves all Limits values. Every snapshot still has one fresh Budget and normal independent Usage/Exact work, with exact caller ProjectLimits.
|
||||
|
||||
ParsedMixedSyntax retains the guard. Consumed resolution seeds its Ledger from that guard once, then runs the existing syntax accounting and subsequent reservation/release sequence unchanged. Successful ownership naturally retains the guard through syntax inside resolved/indexed owners. A cfg(test) immutable `phase_state` observer exposes syntax/Slots for sibling tests; it exposes no mutation or production constructor.
|
||||
|
||||
Temporal scope is deliberately limited: current-unit parsing and syntax string decoding/copying can precede aggregate phase refusal, and retained syntax text is counted during resolution. Existing grammar event checks remain before their declared allocations; this prerequisite does not add universal parser allocation admission. Model/serde/raw validator heap, allocator capacity/RSS and stack frames keep their established exclusions. Stronger before-growth obligations, output creation, whole actual-output equality and accepted edited-output ownership remain future requirements. This work neither closes nor waives them.
|
||||
|
||||
Twelve authored tests cover independent E/H bundle counts and whole Project oracles, real retained guard identity, exact new Usage, once-only resolver +C retention, parser 13/12 and 25/24 boundaries, resolver 17/16 and 29/28 entry peaks, 112/111 and 214/213 text peaks, canonical original diagnostic paths/spans, initial C/file checks, postdecode syntax text and ordinary position-key copy accounting, unchanged legacy versus explicit carried-zero timing, all captured Limits/ProjectLimits fields, real byte/token/reference/frame/Exact/model/file/depth/include policies, and OA/SA Unicode/CRLF compatibility through both index families. Nonzero occurrence and impossible arithmetic cases are explicitly labeled fault injection; the actual retained bundle owner has no occurrence storage.
|
||||
|
||||
E is 78 source bytes with C=(1,0,102), U 1 / M 11 and final (13,0,110). H is 132 source bytes with C=(3,0,180), U 3 / M 19 and final (25,0,212); its final comment contains actual é and is 6 UTF-8 bytes. These expectations come from the independent parent oracle, not application outputs. Syntax-only exact caps are not reported as full-resolve successes. Parser default resource spans remain 0..1; resolver restores 0..0 or the original header/include extent.
|
||||
|
||||
Zero carried mode intentionally refuses E cap 11 at preprojection, while legacy cap 11 still produces syntax and then refuses resolution at U+M 12. This distinction preserves the old API timing without numerically bypassing the new carried contract. Borrowed policy capture from a still-live fixture witness is test-only propagation evidence, not proof old auxiliary B was consumed or dropped.
|
||||
|
||||
No Cargo/build/test execution was performed by the implementation agent. Targeted rustfmt on owned files and raw-LF normalization are authoring checks only. Parent owns source review integration, exact-source commits, compiler/focused/full-suite gates and publication. Inherited full-suite failures and formatting issues are not addressed by this patch; parent gates will compare actual outcomes. The documented environment-sensitive upstream stack observation remains a limitation. Existing small-stack controls were not edited or waived.
|
||||
|
||||
Independent test-source review corrected two pre-execution fixture mistakes: use the accepted oa_property/sa_property keywords, and distinguish the parser include-edge error span 0..1 from the traversal include-depth span 0..23. These are source-review corrections, not recorded runtime failures.
|
||||
|
||||
## Recorded regression and slim-guard candidate
|
||||
|
||||
Parent's `d11b99bb` focused run passed all 12 carried tests, but its full run aborted at the unchanged `ordinary_index::tests::small_stack_success_and_late_header_failure_leave_no_partial_owner` with STATUS_STACK_OVERFLOW. The predecessor `18073a6f` full run had completed with 230 passing tests and 16 inherited failures, including that control. This is a failed current full gate, not an inherited-failure waiver or an accepted repair.
|
||||
|
||||
The exact-artifact LLDB trace reproduces the overflow during Windows heap allocation under formatting/Exact::parse_decimal, then exact_literal, syntax::binding, declarations::parse, State::parse_typed, Parser::unit, new parse_impl, parse_with_profile, parse, accepted/indexed helpers and the existing worker. The diagnostic child's killed exit is not a test pass. Parent measured individual prologues of parse_impl 11,144 bytes, parse_with_profile 312, accepted 1,752, indexed 1,704 and worker 7,752; these are not the complete active stack. Raw evidence remains in the parent gate records, including `carried-debugger-d11b99b-1.log` and `carried-full-d11b99b-1.stack-frames.json`.
|
||||
|
||||
The selected narrow candidate consumes CarriedInput before entering the shared parser. Exact captured policy transfers into Budget and syntax.limits; the retained CarriedGuard holds only Borrowed or Retained(&RetainedSnapshot). This removes the duplicate inline SnapshotPolicy payload from parsed/resolved/indexed/result carriers, while retained backing still borrows the actual allocation-owning bundle. Borrowed source identity remains bound through ParsedMixedSyntax.bundle. Explicit Some mode still distinguishes carried zero from legacy None, and all checks/reservations/Usage remain unchanged. No heap allocation or new logical storage was introduced. The parser wrapper remains uninlined and unchanged.
|
||||
|
||||
Only carried tests' guard-shape inspection and direct injected-arithmetic access follow the new internal guard type; all expected counts, limits, spans, outcomes and existing stack tests remain unchanged. The candidate is not claimed to establish a sole overflow cause or a successful repair before parent exact-source gates. All original failure evidence remains retained.
|
||||
@@ -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,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,63 @@
|
||||
# Private mixed typed occurrence index
|
||||
|
||||
Source checkpoint on `codex/analysis-mixed-index-956atf-20260924`, based on
|
||||
`aaec78fcc9b273d1485497126392afdd1fb2e54e`. This records implemented structure;
|
||||
compilation and runtime acceptance have not yet occurred at this checkpoint.
|
||||
The first Windows strict core all-target Clippy attempt at `a18b7966` returned
|
||||
101 for the test's `6 | 7 | 8` pattern (`manual_range_patterns`); all 545 source
|
||||
inputs remained unchanged. Its receipt/log are preserved. The successor uses
|
||||
`6..=8` with the same cases and no production change. No test body ran in that
|
||||
attempt. At `1d02206c`, strict lint passes and the actual listing has 216 names.
|
||||
The seven new tests initially yield six passed and one failed: the full fixture
|
||||
incorrectly required the old lexical frame peak (three) to remain unchanged.
|
||||
The contract explicitly permits structural depth checks to raise that peak;
|
||||
root plus Left/Argument(1)/Argument(1) has four frames. Independent review
|
||||
confirmed this test defect. The successor expects that exact maximum while
|
||||
still comparing every other usage field, numeric work and static metadata.
|
||||
Both original failures remain recorded; current-source runtime gates are pending.
|
||||
The accepted contract/review are published at
|
||||
`7cfdd13d3a1472e63fd34b0080b2cfc4b921b23e:harness/analysis-mixed-index-contract-20260924.md`.
|
||||
Independent exhaustive fixture data and its raw-byte checker are at
|
||||
`ae6df4fa1f103ff9b14963731b2edd84e38738b1:harness/analysis-mixed-index-20260924/`.
|
||||
|
||||
`ResolvedMixedProject::index` consumes the sealed complete-Project witness.
|
||||
`IndexedMixedProject` owns it and shared `IndexData`; read-only methods expose
|
||||
the Project, warnings, original bundle/canonical files, names and typed maps.
|
||||
It has no conversion to the legacy numeric-source Index or its updater. The
|
||||
constructor-only adapter keeps its file table private, so shared verification
|
||||
does not accept caller-fabricated mixed source/token ownership.
|
||||
|
||||
Shared verification preserves original token, owner, structural child-path,
|
||||
qualified call-label and physical occurrence checks. Mixed catalog construction
|
||||
streams complete Project globals and scoped parameters once. It does not
|
||||
reparse, relower, duplicate model records or repeat admission. Ordinary globals
|
||||
are names/targets here; ordinary source occurrences are not indexed by this view.
|
||||
Complete non-expression occurrence emission remains the sealed lowerer's
|
||||
invariant, checked by an independent exhaustive output oracle.
|
||||
|
||||
Mixed allocations reserve against the retained aggregate ledger before growth.
|
||||
New map/vector/set/Path entries charge entry slots; retained raw lowerer
|
||||
occurrences keep occurrence slots. New names charge copied UTF-8 bytes. Owner
|
||||
and physical-token scratch drop after occurrence checks; path/call scratch drop
|
||||
before syntax-edge traversal. Shared Path nodes remain retained while map keys
|
||||
own them. Legacy typed-only construction retains its original local policy.
|
||||
|
||||
Precompile independent review found stale diagnostic file attribution in two
|
||||
late verification loops and unsafe fallback for invalid file/UTF-8 spans. The
|
||||
source now updates attribution for each checked record and maps invalid
|
||||
locations explicitly to a safe start span, retaining a valid file when known.
|
||||
Regression cases are authored; no passing runtime result is implied.
|
||||
|
||||
Seven new tests cover complete OA/SA names/maps/model and Unicode/CRLF variants,
|
||||
qualified corruption cases, inherited-plus-new storage and copied-name byte
|
||||
boundaries, original provenance/admission metadata, and small-stack success and
|
||||
late rejection. Existing rich fixture/model construction is factored without
|
||||
removing its original full arena test. Actual test inventory, strict core lint,
|
||||
focused checks, full private-library dispositions and formatting still require
|
||||
current-source execution. Prior private source8 failures are not suppressed.
|
||||
|
||||
The working agreement is synchronized to current main blob
|
||||
`e76647f49913b314e1543c4464f6199533909bae`. This source remains separate from the
|
||||
Windows prerelease. Public source8, ordinary occurrence composition, source
|
||||
updates/output admission, migration, checked save/history, GUI and API parity
|
||||
remain required subsequent work.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Private mixed Project implementation
|
||||
|
||||
Source-only checkpoint; compilation and runtime acceptance are pending. Base is `a578ac98a06772a60bec00ae287e65650680d119`. The accepted contract is `aecee2aff3f4410648a501a70cb2e6160c81adc5:harness/analysis-mixed-project-contract-20260923.md`; this note records implementation choices, not a replacement specification.
|
||||
|
||||
`mixed::project::resolve` consumes the actual complete syntax witness. The sealed result retains Project, warnings, static admission metadata, original syntax/Budget/ProjectLimits/bundle, unchanged expression/occurrence vectors and their canonical file keys. Only Project/warnings are exposed read-only. The empty Project header is copied into model ownership; the original header remains in syntax. No declarations, typed models, tokens or Exact values are reparsed to establish authority.
|
||||
|
||||
The catalog seeds the entry first and merges ordinary/typed headers by original declaration start within canonical file order. It checks alias before global ID, retaining both occurrence owners on collision. Existing typed-only catalog precedence remains unchanged. Parameter names/IDs remain calculation scoped. Ordinary construction is the shared existing loop with a pre-field hook; its legacy wrapper preserves source3 scratch and all migration/admission tails. Typed lowering shares the existing body resolver over borrowed declarations and one complete catalog. Moved model records must be disjoint before insertion.
|
||||
|
||||
## Live auxiliary storage
|
||||
|
||||
Every reservation uses checked arithmetic and tests the aggregate through the inherited Budget's noncharging entry, occurrence and owned-byte checks. No new Budget or reset occurs. Failure aborts the phase. Logical slots are not allocator capacity/RSS.
|
||||
|
||||
| Representation | Reservation and end of lifetime |
|
||||
| --- | --- |
|
||||
| Original syntax | Retained mixed metadata subtotal plus parsed syntax entries. Includes the existing conservative dropped value-end subtotal. Tokens/positions are not counted a second time. An iterative walk counts separately owned strings, with each counting-stack push reserved first. Source bytes stay separately charged once. Baseline remains live. |
|
||||
| Complete catalog | Lower aliases+global ID set: two entries/global and owned alias bytes. Scoped parameter map: one entry/calculation plus one entry/name and copied name bytes. Per-signature temporary ID set reserves one entry/parameter and releases after `Tables::typed` returns. |
|
||||
| Alias projection and diagnostic owners | One entry+copied alias bytes/global for projection; one entry/global for owner registry. Projection drops after construction, owner registry after diagnostic mapping. Both overlap the lower catalog initially. |
|
||||
| Ordinary coordination | One borrowed `(file,declaration)` entry/ordinary occurrence. One deferred-node entry before that declaration's field cloning. Release after construction/attachment returns and the views drop. |
|
||||
| Typed lowering | Bounded syntax walk counts N expressions, A supplied call arguments, O emitted occurrences. Reserve `5N+2A` entries and O occurrences before lowering. N entries are retained expression sites. Temporaries are bounded by 2N Visit/Build slots, N borrowed child slots, N value/split slots, A call-parameter slots and A seen-set slots. Hold this conservative whole-phase allowance until all Resolver temporaries die; then release only the difference from actual retained global sites. |
|
||||
| Mapped diagnostics | Before retention, reserve one output slot, copied canonical path bytes and transferred Issue code/field/message String bytes. No line cache is allocated. Owner registry and syntax/sites remain live during mapping. Failure cannot produce truncated-warning success. |
|
||||
|
||||
The call signature is borrowed from the already accounted catalog; no lowerer allocation grows with unprovided callee parameters. Unknown/incomplete calls cannot exceed the supplied-argument bound. O includes parameter declarations, scalar-type references, non-null optional references, call targets/labels and expression reference forms; headers and display text count only as text. Failed lowering drops all partial output with its reservation.
|
||||
|
||||
Model/serde allocations and architectural validation's raw Issue vector/strings/indexes remain explicitly outside this preallocation ledger. Model limits are checked on the complete Project; static analysis retains its existing independent meter. Raw Issues are never retained uncharged in an accepted result. Rejection diagnostics are small error transport outside accepted-witness storage, so exhaustion itself remains reportable.
|
||||
|
||||
The ordinary hook selects exactly Process/FunctionalChain/Scenario steps, Occurrence Branch occurrences, and ContextualLink source then target reference arrays before field resolution/cloning. Repeated items count separately; scalar parent/owner, endpoint occurrence, unrelated fields and every Trace baseline snapshot are excluded. Recognizable malformed arrays are charged before the unchanged decoder reports their shape errors.
|
||||
|
||||
## Admission, mapping and verification
|
||||
|
||||
`validation::validate_with_usage` performs one static admission, copies owned usage/limits, drops the borrowing environment, then performs one architecture validation. `validate` uses the same route and returns its Issues. Static failure preserves the original short circuit. The mixed path maps all Issues and refuses any error. Known typed fields map precisely; unsupported semantic field paths, including the current aggregate `parameters`, explicitly fall back to their owner. Exact scoped-parameter lowerer failures retain their original file and full span; UUID substrings never manufacture precision.
|
||||
|
||||
Authored tests cover whole OA/SA models and exact warnings; a complete 15-expression/18-occurrence arena oracle with repeated/reordered calls, scoped UUID reuse and coincident file offsets; catalog collisions; ordinary reference selectors/cumulative limits; simultaneous storage and diagnostic-path copying; inherited nested records; legacy scratch/default/errors; static/architecture refusal; parameter diagnostic fallback; large-signature incomplete calls; and 128 KiB success/rejection cleanup. The full model/site oracle was independently designed before implementation. Tests are unexecuted at this checkpoint.
|
||||
|
||||
Required later gates: Windows listing, focused new Project tests, complete private typed tests (including lower/index/update and small-stack controls), existing public parser/traversal/migration regressions with preserved baseline disposition, and strict all-target core Clippy. The known inherited formatting/full-suite failures must remain explicit. Source formatting is a parsing check, not typechecking or runtime evidence.
|
||||
|
||||
Index construction/verification, ordinary occurrence indexing, old-updater access, public source8 activation, stabilization, mixed edit composition, output recompile/admission, intended-Project equality and root Scenario/editor reconciliation remain deferred. `index.rs` and `update.rs` are unchanged.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Private complete-target mixed rename planning
|
||||
|
||||
2026-09-24. Descendant implementation of base `ee500908e3d77182ddd54e12524f9b6c6eb43b60`, under accepted parent contract `dc87babf` (`harness/analysis-mixed-rename-plan-contract-20260924.md` on the evidence branch). This note describes source and authored tests, not completed runtime acceptance.
|
||||
|
||||
`AuthoredMixedProject::plan_rename` consumes the complete admitted source witness and borrows an immutable whole desired Project. `RenamePlan` retains that witness, desired borrow, changed qualified names and one original-coordinate physical edit map. Read-only methods expose source, desired, names, edits, full-current-bundle validation and combined complete output-byte sizing. There is no emitter, rewritten-output admission, public activation or legacy adapter.
|
||||
|
||||
The whole-target gate uses exhaustive record patterns, collection keys/counts and parameter position/identity/type checks. Existing PartialEq semantics govern all unchanged payloads, including signed floating zero. Its expression work vectors run under an E-entry reservation, where E is the retained raw expression-site count. Current expression roots are calculation bindings, engineering-property base, case overrides and check predicates; historical semantic fields have no analysis Expr. E scratch is released before changed names are collected; the final namespace set is dropped before edits are collected. Successful retained growth is R+M entries, zero occurrence slots and zero copied text. Parsing, numeric and frame Usage remain unchanged.
|
||||
|
||||
Both authenticated occurrence families feed one map. Validation checks original token spelling/UTF-8 authority, whole Word spans, physical nonoverlap and exact required key/qualified-target membership. Bundle validation includes entry, every path/file and untouched bytes. Sizing subtracts the sum of removed bytes before adding replacements per file, then includes all files in the inherited byte check. Emission/re-admission lifecycle and aggregate output storage remain required successors.
|
||||
|
||||
Independent source review found and prompted two pre-compiler repairs: preserve canonical file attribution for edit-reservation failures, and check ordered parameter shape before calculation-body equality. No runtime failure claim is attached to these source findings. Existing historical mixed/ordinary failures and prior baseline results remain unchanged; this work does not overwrite their records.
|
||||
|
||||
The 17 authored tests cover independently specified complete rich OA/SA and Unicode/CRLF maps and final-name sets; all36 category edits including real comment-separated references and historical UUID exclusion; identity/map/parameter/non-name field rejections; QuantityKind/Unit payload gate controls; simultaneous global/scoped swaps and collisions; immutable exact target/source borrowing; no-op; unchanged Usage and R+M retention; stale touched/untouched/path/entry/file-set controls; safe corrupted UTF-8/file authority and same-spelling wrong-callee identity; signed-zero geometry, scalar bounds/defaults and historical real facets; deep/wide old equality and target-only trees with small-stack success/drop and late refusal.
|
||||
|
||||
Storage oracle: rich OA B=resolved+65, E15/Q11/R11/M27; added peak38 and retention38. Inherited B+37/B+38 tests run the entire predecessor pipeline before planning, and the negative checks the last ordinary diagram header main[412..413). A separate diagram moved to canonical last include z.arcadia checks precise included-file allocation diagnostics, contingent on its complete predecessor pipeline fitting the unchanged profile. No test replaces/tightens a live owner's Budget to manufacture a later-phase failure. Wide/deep equality tests assert authored E128/E129; no unsupported isolated E-cap claim is made.
|
||||
|
||||
Combined-size oracle uses original930 bytes: ordinary31-byte node alias shrinks to1 while three typed f aliases grow to9; complete924 fits although typed-only intermediate954 would not. A12-byte replacement produces933 and must fail under the same inherited930-byte cap, including the untouched213-byte file. This tests planned sizing only, never output correctness.
|
||||
|
||||
All source/tests require parent compilation, focused runtime tests and existing broader gates. Direct rustfmt is the only executed Rust tool during authoring. Parent owns commit/publication and verification evidence; no Cargo/build/test execution was performed by this implementation agent.
|
||||
|
||||
## Recorded first runtime failure and narrow test repair
|
||||
|
||||
Parent's `9388fcf2` focused run compiled and passed four tests before the 128 KiB deep/wide worker terminated with Windows stack-overflow exit 0xc00000fd. Parent separately ran the remaining 16 tests from that exact original artifact: all passed, explicitly excluding the crashing test. This is not a 17-test acceptance result.
|
||||
|
||||
Diagnostic source `9e8e77e1` established that deep admission, whole original clone, rename success and successful plan drop completed; failure followed entry into the collision-rejection helper. Diagnostic source `8159a17d` further located the overflow inside that helper's `authored(bundle)` call: its flushed before-admission marker appeared, but its after-admission/before-plan marker did not. Thus that failing helper invocation had not entered rename planning. These observations do not prove a particular compiler frame size or establish model cloning as the cause.
|
||||
|
||||
The bounded repair removes diagnostic markers and restores the general helper. Only this small-stack test directly creates each fresh owner at worker scope and matches each planning result there. It still uses 128 KiB, deep 128 and wide 129 expression nodes, complete admission on the worker, success/plan-drop, late final-namespace collision/refusal-drop and non-name semantic refusal/drop for both shapes. Prior plan, diagnostic and desired owners are explicitly dropped before subsequent cases. No fixture depth/width or stack allowance is weakened; no production code changes. The repaired full 17-test focused run and broader gates remain parent-owned and pending.
|
||||
|
||||
The direct-call candidate at `70c20c3` also aborted the small-stack test after the same first four focused tests passed. That candidate is **not** an accepted repair. The next test-only candidate uses a compact mode loop: each shape runs no-op success, final-namespace collision and non-name refusal through one shared source local, one desired clone and one planning result expression, with explicit plan/failure/desired drops. Complete admission remains inside the 128 KiB worker for every case; depths and expected outcomes are unchanged. This removes repeated case-specific large debug temporaries as a bounded layout experiment; no exact compiler frame size or overflow cause is claimed. If it still fails, actual-frame diagnostics are required before further rearrangement. Parent-focused verification remains pending.
|
||||
|
||||
Parent's `f1c0956` debugger evidence supersedes the unlocated layout hypothesis: run3 backtrace reaches `mixed::parse_with_profile -> Parser::unit -> parse_typed -> declarations::parse -> syntax::binding -> Cursor::exact_literal -> Budget::exact -> Exact::parse_decimal -> BigInt::parse_bytes -> Windows heap allocation`, before any rename-plan call. The same artifact's debugger run2 passed, so the full admission-on-128-KiB observation remains an **UNRESOLVED environment-sensitive stack boundary**, not a demonstrated planner defect or a repaired production issue. Raw evidence is the parent-owned `rename-debugger-f1c0956-3.log` and receipt; all preceding failed full-pipeline observations remain valid and preserved.
|
||||
|
||||
The final narrowly scoped planner evaluator prepares each fresh complete admitted owner and immutable desired Project on the normal test thread. For all six deep128/wide129 × no-op/collision/non-name cases, a scoped128-KiB worker moves the complete owner, borrows desired, invokes planning, checks the exact expected outcome, and drops its successful plan or failure there. No owner/result escapes, policy resets or leaks occur. Desired is externally owned and destroyed outside the worker. Depth, width, worker stack allowance and all planning/drop cases are unchanged. This test now claims only planner comparison/ownership/drop behavior; it makes **no claim** that the complete upstream parse/admission pipeline reliably fits128KiB under every observed environment. Existing separate admission/index small-stack tests remain in the parent full-suite gates. Final focused/full runtime acceptance is still parent-owned and pending.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Private authored ordinary-name index implementation
|
||||
|
||||
Before compilation, source checkpoint `50a5918f` exposed Git normalization of
|
||||
the CRLF category fixture. A narrow fixture attribute now preserves its exact
|
||||
2,152 bytes and authored span oracle. Test-only resource variants were also
|
||||
strengthened to reach this new stage after prior phases fit; the initial source
|
||||
was not built or runtime-tested. Production bytes are unchanged by these repairs.
|
||||
|
||||
Local implementation on the `93eb6117f95353d25d594d59780018f197c51672` private lineage, following the root accepted ordinary-index contract (2026-09-24). Runtime acceptance is pending the parent-owned compiler/test gates; this note records design and authored evaluators, not passing results.
|
||||
|
||||
`IndexedMixedProject::index_ordinary(self)` consumes the admitted typed owner and returns `ordinary_index::AuthoredMixedProject`. It retains that owner and one map keyed by canonical file and whole alias-token byte range. Inline records carry owner/global target, Header/Reference role and full original extent. Read-only methods expose both occurrence families, expression keys, names, complete Project, warnings and original bundle/files. No adapter to legacy Index/Plan/Composition or independently replaceable source/profile is added.
|
||||
|
||||
Construction borrows original significant tokens and the sealed ordinary AST/position pairs. Headers are checked against declaration occurrences, not enclosing ranges; the synthetic root Record includes body braces, while real field/value extents are checked within their justified parents. Every Reference leaf is visited, including nested records/lists and arbitrary ordinary fields. A Reference must start at the significant `@` and end exactly at the immediately following whole Word. HistoricalUuid, Atom and prose/include/identity strings are excluded. No reparse, lower, model copy, name copy, numeric work, admission or frame call occurs. Completeness trusts the sealed projection plus exhaustive authored oracles; per-record checks do not establish arbitrary coordinated AST/projection omission rejection.
|
||||
|
||||
The inherited ledger remains live. New retention is exactly N=1+D+A entry slots, zero occurrence slots, zero text bytes. Scratch is G borrowed global-name entries with inline header-seen flags, T typed physical tokens and W currently queued borrowed values. Reserve before every map/set/work growth; a pop releases one logical slot; globals and typed lookup release after their containers drop. Work drains per declaration; the synthetic root is not queued. Actual rich OA/SA stage peaks are incoming baseline+34/+36 (retained+6/+7), but earlier typed peaks prevent those profiles from isolating this later stage. Rich plus six empty activities uses a later-stage46/45 boundary (retained12), clearing the preceding lower-catalog overlap including retained warnings; unexpanded category fixture has stage peak52 with W5, but that can be below preceding resolver overlap. The scratch-cap test adds64 finite route waypoints to a separate in-memory variant and uses92/91 with W66 (retained36). These hand-counts still require runtime verification, including earlier-stage admission.
|
||||
|
||||
Authored tests cover complete OA/SA maps with literal numeric spans, independent Unicode/CRLF spans, exact rich Project/model metadata retention, canonical coincident offsets and cross-file references, global versus calculation parameter spelling, nested explicit/implicit parents, comment-separated markers, relation/context/route/view/artifact/trace categories, historical UUID exclusion, precise corruption diagnostics and safe fallback, retained/scratch caps without Budget replacement, and bounded-stack success/late failure. The category literal initially contained an invalid duplicate route; the independent oracle critic caught it before compilation and the duplicate was removed. The corrected fixture has36 independently enumerated records (15 headers, 21 references). No inherited compatibility test or known failure disposition is removed.
|
||||
|
||||
Focused runtime at `147b247` reported 9 passing and 4 failing tests (parent-owned gate). Three failures were pre-index refusals from incorrectly authored C-style block comments: the shared raw scanner only treats `//` through newline as Comment. Category trivia now uses a byte-neutral line-comment replacement, preserving all36 numeric records and the full marker/alias extents; the UTF-8 corruption fixture likewise first admits a real line comment before damaging its retained start. The fourth control accidentally duplicated an already indexed call token before it could test header coverage; it now removes that token's former call role before assigning the forged second Header role. Intended later diagnostics remain unchanged. These are test/fixture corrections; production is unchanged, and the 147b247 failures remain evidence. Retest pending.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user