Experiment with nested source authoring against source3 models

This commit is contained in:
2026-09-05 19:22:42 +03:00
parent 06b33848ce
commit c5788fc15e
12 changed files with 1438 additions and 0 deletions
+364
View File
@@ -0,0 +1,364 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "oa-core"
version = "0.2.0"
dependencies = [
"serde",
"serde_json",
"tempfile",
"thiserror",
"uuid",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "source-authoring-experiment"
version = "0.0.0"
dependencies = [
"oa-core",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "uuid"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom",
"js-sys",
"serde_core",
"sha1_smol",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "source-authoring-experiment"
version = "0.0.0"
edition = "2024"
publish = false
[workspace]
[dependencies]
oa-core = { path = "../../crates/oa-core" }
+79
View File
@@ -0,0 +1,79 @@
# Source authoring A/B experiment
Status: bounded executable experiment, 2026-09-05. **Not production syntax or a release-completion claim.** Owns this directory only. The standalone Rust workspace depends on the real `oa-core` and never compiles Iced. Main checkpoint during verification: `06b33848cea5a0b1a6168d79a68e5185c190383e`.
## Question and competing expectations
Does semantic ownership nesting, typed function-port/exchange shorthand and ordered occurrence blocks reduce the coordination burden of text-first OA/SA modeling without introducing different semantics or unstable identities?
- **A:** source3 declarations organized by semantic aggregate. Expected advantage: simple exact mapping, easy reference edits, existing tooling. Possible failure: parent/endpoints/ordered-list bookkeeping remains dispersed even in small files.
- **B:** the same model with actual ownership nesting and three bounded syntactic conveniences. Expected advantage: fewer redundant references and local owner context. Possible failure: more lines, ambiguous ownership versus allocation, costly writeback, or loss of precise model control.
The comparison deliberately uses compact, well-organized A, not a UUID-sorted/full-default straw man. Both candidates retain full explicit UUIDs. This experiment does not compare human completion time or establish that fewer tokens means easier modeling.
## Method
Six independently authored fixtures define three complete model pairs:
1. `oa-*.arcadia`: two performers, an activity decomposition, a carried item, explicit leaf allocations and an operational interaction.
2. `sa-*.arcadia`: system/actor, function decomposition, typed function and component ports, a data field, functional/boundary exchanges, and explicit work/port/exchange allocations.
3. `scenario-*.arcadia`: an operational conversation with an initial Call, correlated reverse Reply, and repeated asynchronous report with a precondition.
`src/lib.rs` uses the existing tolerant lexer, parses the small B surface, lowers to ordinary source3 declarations and calls the **real source-bundle compiler and whole-model validator**. It supports includes only through an explicitly supplied `SourceBundle`. No filesystem, network, alias namespace or semantic model implementation is added to the compiler. `src/main.rs` provides a single-file CLI inspection harness.
B supports:
```text
sa_function transmit "Transmit report" id "00000000-0000-4000-8000-000000000204" {
output report_out "Report output" id "00000000-0000-4000-8000-000000000208" items [@report_data];
}
functional_exchange report_flow "Report flow" id "00000000-0000-4000-8000-000000000214"
from @report_out to @report_in items [@report_data];
```
The function owner of each explicit endpoint port supplies the redundant FE owner field. It creates no ports or allocations. Ordinary `sa_port` bodies remain available for fields beyond shorthand. All other existing explicit fields pass through to source3, including variant constructors, component contracts, diagram/node records and exact strings. A nested element supplies only its `parent`; an incompatible explicit parent is an error. `steps { occurrence declarations };` supplies both ownership and the ordered list. Existing `steps [@alias]` remains accepted. The prototype requires explicit identity clauses everywhere; optional-ID stabilization is intentionally outside this experiment.
## Observed results
Ten integration tests passed on Windows/Rust 1.98.1, plus strict all-target Clippy. The real CLI compiled `sa-b.arcadia` and printed the expected canonical source3 model.
| Task | Physical lines A/B | Significant tokens A/B | Explicit references A/B | Parent fields A/B | Explicit steps references A/B | Chosen owner/member line span A/B |
|---|---:|---:|---:|---:|---:|---:|
| OA | 12/13 | 111/103 | 9/7 | 2/0 | 0/0 | 2/2 |
| SA | 21/27 | 305/245 | 27/18 | 7/0 | 0/0 | 4/1 |
| Scenario | 28/29 | 290/262 | 29/21 | 5/0 | 3/0 | 11/11 |
Counts come from the real lexer excluding whitespace/comments; references count `@` tokens. Line spans select the OA parent/assessment child, SA transmitting function/output port and scenario/last occurrence. They measure only those concrete fixture locations, not screen navigation or user time. Each fixture fits one file, so baseline cross-file navigation is equal. B did **not** shorten the scenario span or any file's line count.
The tests establish:
- Every A/B pair produces an identical complete `Project`; canonical export parses back equally.
- Display-name edits change exactly one authored line in each candidate and preserve every identity and unrelated property. An alias rename likewise preserves model reference identities.
- Moving an entire aggregate into an included file, relocating it again, and separately moving the shorthand FE to another included file leave the complete model unchanged.
- Reordering the repeated message changes only the ordered list; occurrence and call/reply identities remain stable. A edits a list; B moves its existing declaration block. Neither requires new identities.
- Adding another report requires two authored locations in A (declaration plus membership list), one insertion in B's steps block. The resulting complete models are equal and existing identities survive.
- Contradictory parents/endpoints, duplicate identity, duplicate steps fields, unknown/reversed ports and inappropriate nested owners fail. Removing work allocations while retaining dependent port/exchange allocations is rejected; the shorthand never repairs them invisibly. Removing all three allocation categories leaves exactly the explicitly unallocated model.
- Explicit-field escape preserves a diagram, node geometry including negative-zero bits, and quoted Unicode text.
Two substantive initial test expectations were falsified. Removing work allocation while retaining dependent port allocation is invalid, so the test now checks that rejection and separately removes all dependent allocation relations. An occurrence omitted from its owner's steps is valid incomplete work with `unordered_occurrence` warning, not a structural error. The experiment records this existing semantic choice; it does not weaken or silently strengthen core validation. B's ordered block prevents that particular omission. Transient harness compile/CLI path mistakes were corrected and all final commands rerun.
## Conclusion and next gate
B has real, limited benefits: fewer redundant references, substantially less port constructor/endpoint boilerplate, and elimination of two-site scenario membership editing. It uses more lines and does not improve every locality measure. These results justify evolving the small B surface on top of A's pure bundle foundation; they do **not** justify replacing the language with a broad SysML-like grammar or declaring usability solved.
Before production adoption, test source-preserving graphical writeback against these same fixtures. Rename a nested owner, alter a port contract, reorder/add an occurrence, move a semantic child, and move a diagram node. Unrelated files and comments must survive; syntax must remain nested/concise; changed references must still resolve. The current experiment is **one-way lowering**: it discards comments in the lowered output, has no editable concrete syntax tree, and does not attempt B regeneration. A whole-declaration canonical replacement is insufficient for that gate. No GUI or HTTP service workflow was exercised here.
Revisit B if preserving its organization requires ambiguous parent inference or broad rewrite rules. Revisit global aliases only when a representative larger model demonstrates frequent naming collisions or unwieldy references. Revisit automatic aggregate partitioning when common tasks span more than three files. Include branches/fragments/state nesting and genuinely large models in the next trial before extending the ordered-block convenience beyond the tested sequence cases.
The language references informing the hypothesis are the [OMG SysML 2.0 language specification §7.5](https://www.omg.org/spec/SysML/2.0/Language/PDF), its [official vehicle example](https://github.com/Systems-Modeling/SysML-v2-Release/blob/master/sysml/src/examples/Vehicle%20Example/SysML%20v2%20Spec%20Annex%20A%20SimpleVehicleModel.sysml), and [Capella's distinction between functional dependency and control flow](https://mbse-capella.org/arcadia_capella_sysml_tool.html). These support examining ownership locality and keeping relation meanings explicit; they do not prove this experimental dialect conforms to either standard.
## Reproduce from the repository root
```powershell
$env:CARGO_TARGET_DIR='D:/CodexBuilds/Capella2-source-authoring-experiment'
cargo test --manifest-path experiments/source-authoring/Cargo.toml --locked -- --nocapture
cargo clippy --manifest-path experiments/source-authoring/Cargo.toml --locked --all-targets -- -D warnings
cargo run --manifest-path experiments/source-authoring/Cargo.toml --locked -- experiments/source-authoring/fixtures/sa-b.arcadia
```
The target-directory override is optional; choose another writable directory on other machines. Linux execution and minimum-supported-Rust execution have not been tested for this isolated experiment. Keep this parser isolated; production grammar/diagnostic/editor work needs independent review rather than copying it wholesale.
@@ -0,0 +1,12 @@
arcadia 3;
project coastal_ops "Coastal operations" id "00000000-0000-4000-8000-000000000001" {
oa_actor vessel "Vessel" id "00000000-0000-4000-8000-000000000101" {}
oa_entity center "Rescue center" id "00000000-0000-4000-8000-000000000102" {}
oa_activity respond "Respond to distress" id "00000000-0000-4000-8000-000000000103" {}
oa_activity report "Report distress" id "00000000-0000-4000-8000-000000000104" { parent @respond; }
oa_activity assess "Assess incident" id "00000000-0000-4000-8000-000000000105" { parent @respond; }
oa_exchange_item report_data "Distress report" id "00000000-0000-4000-8000-000000000106" {}
allocation vessel_reporting "Vessel reports" id "00000000-0000-4000-8000-000000000107" { source @vessel; target @report; }
allocation center_assessment "Center assesses" id "00000000-0000-4000-8000-000000000108" { source @center; target @assess; }
interaction distress_message "Distress report" id "00000000-0000-4000-8000-000000000109" { source @report; target @assess; items [@report_data]; }
}
@@ -0,0 +1,13 @@
arcadia 3;
project coastal_ops "Coastal operations" id "00000000-0000-4000-8000-000000000001" {
oa_actor vessel "Vessel" id "00000000-0000-4000-8000-000000000101" {}
oa_entity center "Rescue center" id "00000000-0000-4000-8000-000000000102" {}
oa_activity respond "Respond to distress" id "00000000-0000-4000-8000-000000000103" {
oa_activity report "Report distress" id "00000000-0000-4000-8000-000000000104" {}
oa_activity assess "Assess incident" id "00000000-0000-4000-8000-000000000105" {}
}
oa_exchange_item report_data "Distress report" id "00000000-0000-4000-8000-000000000106" {}
allocation vessel_reporting "Vessel reports" id "00000000-0000-4000-8000-000000000107" { source @vessel; target @report; }
allocation center_assessment "Center assesses" id "00000000-0000-4000-8000-000000000108" { source @center; target @assess; }
interaction distress_message "Distress report" id "00000000-0000-4000-8000-000000000109" { source @report; target @assess; items [@report_data]; }
}
@@ -0,0 +1,21 @@
arcadia 3;
project reporting_system "Reporting system" id "00000000-0000-4000-8000-000000000002" {
sa_system center "Rescue system" id "00000000-0000-4000-8000-000000000201" {}
sa_actor vessel "Vessel service" id "00000000-0000-4000-8000-000000000202" {}
sa_function manage "Manage reporting" id "00000000-0000-4000-8000-000000000203" {}
sa_function send_report "Transmit report" id "00000000-0000-4000-8000-000000000204" { parent @manage; }
sa_function assess "Assess report" id "00000000-0000-4000-8000-000000000205" { parent @manage; }
sa_exchange_item report_data "Report" id "00000000-0000-4000-8000-000000000206" {}
sa_data_field latitude "Latitude" id "00000000-0000-4000-8000-000000000207" { parent @report_data; type_ref Real; unit "degrees north"; }
sa_port report_out "Report output" id "00000000-0000-4000-8000-000000000208" { parent @send_report; contract { FunctionOutput { outgoing_items [@report_data]; }; }; }
sa_port report_in "Report input" id "00000000-0000-4000-8000-000000000209" { parent @assess; contract { FunctionInput { incoming_items [@report_data]; }; }; }
sa_port vessel_out "Vessel output" id "00000000-0000-4000-8000-000000000210" { parent @vessel; contract { Component { kind Flow; orientation Out; }; }; }
sa_port center_in "Center input" id "00000000-0000-4000-8000-000000000211" { parent @center; contract { Component { kind Flow; orientation In; }; }; }
allocation vessel_transmits "Vessel transmits" id "00000000-0000-4000-8000-000000000212" { source @vessel; target @send_report; }
allocation center_assesses "Center assesses" id "00000000-0000-4000-8000-000000000213" { source @center; target @assess; }
functional_exchange report_flow "Report flow" id "00000000-0000-4000-8000-000000000214" { source @send_report; target @assess; source_port @report_out; target_port @report_in; items [@report_data]; }
boundary_exchange reporting_connection "Reporting connection" id "00000000-0000-4000-8000-000000000215" { source @vessel_out; target @center_in; oriented true; items [@report_data]; }
port_allocation transmit_endpoint "Transmit endpoint" id "00000000-0000-4000-8000-000000000216" { source @vessel_out; target @report_out; }
port_allocation receive_endpoint "Receive endpoint" id "00000000-0000-4000-8000-000000000217" { source @center_in; target @report_in; }
exchange_allocation reports_carried "Reports carried" id "00000000-0000-4000-8000-000000000218" { source @reporting_connection; target @report_flow; }
}
@@ -0,0 +1,27 @@
arcadia 3;
project reporting_system "Reporting system" id "00000000-0000-4000-8000-000000000002" {
sa_system center "Rescue system" id "00000000-0000-4000-8000-000000000201" {
sa_port center_in "Center input" id "00000000-0000-4000-8000-000000000211" { contract { Component { kind Flow; orientation In; }; }; }
}
sa_actor vessel "Vessel service" id "00000000-0000-4000-8000-000000000202" {
sa_port vessel_out "Vessel output" id "00000000-0000-4000-8000-000000000210" { contract { Component { kind Flow; orientation Out; }; }; }
}
sa_function manage "Manage reporting" id "00000000-0000-4000-8000-000000000203" {
sa_function send_report "Transmit report" id "00000000-0000-4000-8000-000000000204" {
output report_out "Report output" id "00000000-0000-4000-8000-000000000208" items [@report_data];
}
sa_function assess "Assess report" id "00000000-0000-4000-8000-000000000205" {
input report_in "Report input" id "00000000-0000-4000-8000-000000000209" items [@report_data];
}
}
sa_exchange_item report_data "Report" id "00000000-0000-4000-8000-000000000206" {
sa_data_field latitude "Latitude" id "00000000-0000-4000-8000-000000000207" { type_ref Real; unit "degrees north"; }
}
allocation vessel_transmits "Vessel transmits" id "00000000-0000-4000-8000-000000000212" { source @vessel; target @send_report; }
allocation center_assesses "Center assesses" id "00000000-0000-4000-8000-000000000213" { source @center; target @assess; }
functional_exchange report_flow "Report flow" id "00000000-0000-4000-8000-000000000214" from @report_out to @report_in items [@report_data];
boundary_exchange reporting_connection "Reporting connection" id "00000000-0000-4000-8000-000000000215" { source @vessel_out; target @center_in; oriented true; items [@report_data]; }
port_allocation transmit_endpoint "Transmit endpoint" id "00000000-0000-4000-8000-000000000216" { source @vessel_out; target @report_out; }
port_allocation receive_endpoint "Receive endpoint" id "00000000-0000-4000-8000-000000000217" { source @center_in; target @report_in; }
exchange_allocation reports_carried "Reports carried" id "00000000-0000-4000-8000-000000000218" { source @reporting_connection; target @report_flow; }
}
@@ -0,0 +1,28 @@
arcadia 3;
project reporting_story "Repeated reporting" id "00000000-0000-4000-8000-000000000003" {
oa_actor vessel "Vessel" id "00000000-0000-4000-8000-000000000301" {}
oa_entity center "Center" id "00000000-0000-4000-8000-000000000302" {}
oa_activity report "Report distress" id "00000000-0000-4000-8000-000000000303" {}
oa_activity assess "Assess distress" id "00000000-0000-4000-8000-000000000304" {}
allocation reporting "Reporting" id "00000000-0000-4000-8000-000000000305" { source @vessel; target @report; }
allocation assessment "Assessment" id "00000000-0000-4000-8000-000000000306" { source @center; target @assess; }
oa_exchange_item report_data "Report" id "00000000-0000-4000-8000-000000000307" {}
interaction report_message "Report" id "00000000-0000-4000-8000-000000000308" { source @report; target @assess; items [@report_data]; }
interaction acknowledgement "Acknowledgement" id "00000000-0000-4000-8000-000000000309" { source @assess; target @report; }
oa_scenario reports "Repeated reports" id "00000000-0000-4000-8000-000000000310" { viewpoint OperationalActivity; steps [@initial_report, @accepted, @update_report]; }
oa_lifeline reporter_line "Reporter" id "00000000-0000-4000-8000-000000000311" { parent @reports; represented @report; }
oa_lifeline assessor_line "Assessor" id "00000000-0000-4000-8000-000000000312" { parent @reports; represented @assess; }
oa_occurrence initial_report "Initial report" id "00000000-0000-4000-8000-000000000313" {
parent @reports;
body { Exchange { exchange @report_message; sender @reporter_line; receiver @assessor_line; message_kind Call; reply_to none; values []; }; };
}
oa_occurrence accepted "Report accepted" id "00000000-0000-4000-8000-000000000314" {
parent @reports;
body { Exchange { exchange @acknowledgement; sender @assessor_line; receiver @reporter_line; message_kind Reply; reply_to @initial_report; values []; }; };
}
oa_occurrence update_report "Updated report" id "00000000-0000-4000-8000-000000000315" {
parent @reports;
precondition "A more recent position is available.";
body { Exchange { exchange @report_message; sender @reporter_line; receiver @assessor_line; message_kind Asynchronous; reply_to none; values []; }; };
}
}
@@ -0,0 +1,29 @@
arcadia 3;
project reporting_story "Repeated reporting" id "00000000-0000-4000-8000-000000000003" {
oa_actor vessel "Vessel" id "00000000-0000-4000-8000-000000000301" {}
oa_entity center "Center" id "00000000-0000-4000-8000-000000000302" {}
oa_activity report "Report distress" id "00000000-0000-4000-8000-000000000303" {}
oa_activity assess "Assess distress" id "00000000-0000-4000-8000-000000000304" {}
allocation reporting "Reporting" id "00000000-0000-4000-8000-000000000305" { source @vessel; target @report; }
allocation assessment "Assessment" id "00000000-0000-4000-8000-000000000306" { source @center; target @assess; }
oa_exchange_item report_data "Report" id "00000000-0000-4000-8000-000000000307" {}
interaction report_message "Report" id "00000000-0000-4000-8000-000000000308" { source @report; target @assess; items [@report_data]; }
interaction acknowledgement "Acknowledgement" id "00000000-0000-4000-8000-000000000309" { source @assess; target @report; }
oa_scenario reports "Repeated reports" id "00000000-0000-4000-8000-000000000310" {
viewpoint OperationalActivity;
oa_lifeline reporter_line "Reporter" id "00000000-0000-4000-8000-000000000311" { represented @report; }
oa_lifeline assessor_line "Assessor" id "00000000-0000-4000-8000-000000000312" { represented @assess; }
steps {
oa_occurrence initial_report "Initial report" id "00000000-0000-4000-8000-000000000313" {
body { Exchange { exchange @report_message; sender @reporter_line; receiver @assessor_line; message_kind Call; reply_to none; values []; }; };
}
oa_occurrence accepted "Report accepted" id "00000000-0000-4000-8000-000000000314" {
body { Exchange { exchange @acknowledgement; sender @assessor_line; receiver @reporter_line; message_kind Reply; reply_to @initial_report; values []; }; };
}
oa_occurrence update_report "Updated report" id "00000000-0000-4000-8000-000000000315" {
precondition "A more recent position is available.";
body { Exchange { exchange @report_message; sender @reporter_line; receiver @assessor_line; message_kind Asynchronous; reply_to none; values []; }; };
}
};
}
}
+349
View File
@@ -0,0 +1,349 @@
//! Disposable B-syntax experiment. This is not the production language.
//! All semantic checks are delegated to the real source3 compiler.
use oa_core::{
Project,
language::{self, TokenClass, TokenSpan},
source_project::{self, ProjectLimits, SourceBundle},
};
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
struct Record {
kind: String,
header: String,
alias: String,
fields: BTreeMap<String, String>,
endpoint_ports: Option<(String, String)>,
}
#[derive(Debug)]
enum Entry {
Text(String),
Record(Record),
}
/// Compile the complete B project after lowering its authored files to source3.
pub fn compile(bundle: &SourceBundle) -> Result<Project, String> {
let lowered = lower(bundle)?;
source_project::compile_bundle(&lowered, ProjectLimits::default())
.map(|result| result.decoded.project)
.map_err(|errors| format!("{errors:?}"))
}
pub fn lower(bundle: &SourceBundle) -> Result<SourceBundle, String> {
let mut units = BTreeMap::new();
let mut owners = BTreeMap::new();
for (path, source) in &bundle.files {
let entries = Parser::new(source)?
.unit(path == &bundle.entry)
.map_err(|error| format!("{path}: {error}"))?;
for entry in &entries {
if let Entry::Record(record) = entry
&& owners
.insert(record.alias.clone(), record.fields.get("parent").cloned())
.is_some()
{
return Err(format!("Duplicate declaration alias @{}", record.alias));
}
}
units.insert(path.clone(), entries);
}
let mut files = BTreeMap::new();
for (path, entries) in units {
let mut text = String::new();
for entry in entries {
match entry {
Entry::Text(raw) => {
text.push_str(&raw);
text.push('\n');
}
Entry::Record(mut record) => {
if let Some((source, target)) = &record.endpoint_ports {
for (field, port) in [("source", source), ("target", target)] {
let owner = owners
.get(port)
.and_then(|owner| owner.as_ref())
.ok_or_else(|| {
format!("Endpoint @{port} has no explicit function owner")
})?;
record.fields.insert(field.into(), owner.clone());
record
.fields
.insert(format!("{field}_port"), format!("@{port}"));
}
}
text.push_str(&record.header);
text.push_str(" {\n");
for (name, value) in record.fields {
text.push_str(&format!(" {name} {value};\n"));
}
text.push_str("}\n");
}
}
}
files.insert(path, text);
}
Ok(SourceBundle {
entry: bundle.entry.clone(),
files,
})
}
struct Parser<'a> {
source: &'a str,
tokens: Vec<TokenSpan>,
at: usize,
}
impl<'a> Parser<'a> {
fn new(source: &'a str) -> Result<Self, String> {
let tokens: Vec<_> = language::tokenize(source)
.into_iter()
.filter(|token| !matches!(token.class, TokenClass::Whitespace | TokenClass::Comment))
.collect();
if tokens.iter().any(|token| token.class == TokenClass::Error) {
return Err("Invalid lexical token".into());
}
Ok(Self {
source,
tokens,
at: 0,
})
}
fn peek(&self) -> &'a str {
self.tokens
.get(self.at)
.map_or("", |token| &self.source[token.span.start..token.span.end])
}
fn take(&mut self, expected: &str) -> Result<(), String> {
if self.peek() != expected {
return Err(format!(
"Expected {expected}, got {:?} at token {}",
self.peek(),
self.at
));
}
self.at += 1;
Ok(())
}
fn word(&mut self) -> Result<String, String> {
let token = self.tokens.get(self.at).ok_or("Unexpected end of input")?;
if !matches!(token.class, TokenClass::Identifier | TokenClass::Keyword) {
return Err(format!("Expected identifier, got {}", self.peek()));
}
let value = self.peek().to_owned();
self.at += 1;
Ok(value)
}
fn string(&mut self) -> Result<(), String> {
if self
.tokens
.get(self.at)
.is_none_or(|token| token.class != TokenClass::String)
{
return Err("Expected quoted string".into());
}
self.at += 1;
Ok(())
}
fn reference(&mut self) -> Result<String, String> {
self.take("@")?;
self.word()
}
fn slice(&self, start: usize, end: usize) -> String {
self.source[self.tokens[start].span.start..self.tokens[end - 1].span.end].to_owned()
}
fn value(&mut self) -> Result<String, String> {
let start = self.at;
let mut closers = Vec::new();
loop {
match self.peek() {
"" => return Err("Unterminated field".into()),
";" if closers.is_empty() => break,
"{" => closers.push("}"),
"[" => closers.push("]"),
"}" | "]" => {
closers
.pop()
.filter(|closing| *closing == self.peek())
.ok_or("Unbalanced field")?;
}
_ => {}
}
if closers.len() > 64 {
return Err("Field nesting exceeds experiment limit".into());
}
self.at += 1;
}
if self.at == start {
return Err("Missing field value".into());
}
let value = self.slice(start, self.at);
self.take(";")?;
Ok(value)
}
fn unit(mut self, entry: bool) -> Result<Vec<Entry>, String> {
let mut out = Vec::new();
if entry {
let start = self.at;
self.take("arcadia")?;
self.take("3")?;
self.take(";")?;
self.take("project")?;
self.word()?;
self.string()?;
self.take("id")?;
self.string()?;
self.take("{")?;
out.push(Entry::Text(self.slice(start, self.at)));
}
while !self.peek().is_empty() && !(entry && self.peek() == "}") {
if self.peek() == "include" || (entry && self.peek() == "description") {
let start = self.at;
self.at += 1;
self.value()?;
out.push(Entry::Text(self.slice(start, self.at)));
} else {
self.record(None, &mut out, 0)?;
}
}
if entry {
self.take("}")?;
out.push(Entry::Text("}".into()));
}
if !self.peek().is_empty() {
return Err("Unexpected trailing input".into());
}
Ok(out)
}
fn record(
&mut self,
owner: Option<(&str, &str)>,
out: &mut Vec<Entry>,
depth: usize,
) -> Result<String, String> {
if depth > 32 {
return Err("Ownership nesting exceeds experiment limit".into());
}
let start = self.at;
let kind = self.word()?;
let alias = self.word()?;
if kind != "node" {
self.string()?;
}
self.take("id")?;
self.string()?;
let header = self.slice(start, self.at);
let mut record = Record {
kind: kind.clone(),
header,
alias: alias.clone(),
fields: BTreeMap::new(),
endpoint_ports: None,
};
if matches!(kind.as_str(), "input" | "output") {
let Some((owner, "sa_function")) = owner else {
return Err("Typed function port shorthand requires an SA function owner".into());
};
self.take("items")?;
let items = self.value()?;
record.kind = "sa_port".into();
record.header = record.header.replacen(&kind, "sa_port", 1);
record.fields.insert("parent".into(), format!("@{owner}"));
let (variant, field) = if kind == "input" {
("FunctionInput", "incoming_items")
} else {
("FunctionOutput", "outgoing_items")
};
record.fields.insert(
"contract".into(),
format!("{{ {variant} {{ {field} {items}; }}; }}"),
);
} else if kind == "functional_exchange" && self.peek() == "from" {
self.take("from")?;
let source = self.reference()?;
self.take("to")?;
let target = self.reference()?;
self.take("items")?;
let items = self.value()?;
record.fields.insert("items".into(), items);
record.endpoint_ports = Some((source, target));
if self.peek() == "{" {
self.body(&mut record, out, depth)?;
}
for field in ["source", "target", "source_port", "target_port"] {
if record.fields.contains_key(field) {
return Err(format!(
"Contradictory shorthand/explicit endpoint field {field}"
));
}
}
} else {
self.body(&mut record, out, depth)?;
if let Some((owner, _)) = owner {
if !kind.starts_with("oa_") && !kind.starts_with("sa_") {
return Err("Only elements may use semantic ownership nesting".into());
}
let inferred = format!("@{owner}");
if record
.fields
.get("parent")
.is_some_and(|parent| parent != &inferred)
{
return Err(format!("Contradictory parent for @{alias}"));
}
record.fields.insert("parent".into(), inferred);
}
}
out.push(Entry::Record(record));
Ok(alias)
}
fn body(
&mut self,
record: &mut Record,
out: &mut Vec<Entry>,
depth: usize,
) -> Result<(), String> {
self.take("{")?;
while self.peek() != "}" {
if self.peek().is_empty() {
return Err("Unterminated declaration".into());
}
let nested = self.peek().starts_with("oa_")
|| self.peek().starts_with("sa_")
|| matches!(self.peek(), "input" | "output");
if nested {
self.record(Some((&record.alias, &record.kind)), out, depth + 1)?;
continue;
}
let field = self.word()?;
if record.fields.contains_key(&field) {
return Err(format!("Duplicate field {field} on @{}", record.alias));
}
let value = if field == "steps" && self.peek() == "{" {
if !matches!(
record.kind.as_str(),
"oa_scenario" | "sa_scenario" | "oa_process" | "sa_functional_chain"
) {
return Err("Ordered steps require a scenario/process/chain".into());
}
self.take("{")?;
let mut steps = Vec::new();
while self.peek() != "}" {
if !matches!(self.peek(), "oa_occurrence" | "sa_occurrence") {
return Err("Ordered steps contain occurrence declarations only".into());
}
steps.push(format!(
"@{}",
self.record(Some((&record.alias, &record.kind)), out, depth + 1)?
));
}
self.take("}")?;
self.take(";")?;
format!("[{}]", steps.join(", "))
} else {
self.value()?
};
record.fields.insert(field, value);
}
self.take("}")
}
}
+31
View File
@@ -0,0 +1,31 @@
//! One-file command-line harness for inspecting the actual lowering result.
use oa_core::{language, source_project::SourceBundle};
use std::{collections::BTreeMap, env, fs, process::ExitCode};
fn main() -> ExitCode {
let args: Vec<_> = env::args().skip(1).collect();
if args.len() != 1 {
eprintln!(
"Usage: source-authoring-experiment B_SOURCE_FILE\nIncludes require the library's explicit SourceBundle API."
);
return ExitCode::FAILURE;
}
let result = fs::read_to_string(&args[0])
.map_err(|error| error.to_string())
.and_then(|text| {
source_authoring_experiment::compile(&SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), text)]),
})
});
match result {
Ok(project) => {
print!("{}", language::format(&project));
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
@@ -0,0 +1,474 @@
use oa_core::{
Project,
language::{self, TokenClass},
source_project::{self, ProjectLimits, SourceBundle},
};
use source_authoring_experiment::compile;
use std::collections::BTreeMap;
const CASES: [(&str, &str, &str); 3] = [
(
"OA",
include_str!("../fixtures/oa-a.arcadia"),
include_str!("../fixtures/oa-b.arcadia"),
),
(
"SA",
include_str!("../fixtures/sa-a.arcadia"),
include_str!("../fixtures/sa-b.arcadia"),
),
(
"Scenario",
include_str!("../fixtures/scenario-a.arcadia"),
include_str!("../fixtures/scenario-b.arcadia"),
),
];
fn bundle(source: &str) -> SourceBundle {
SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), source.into())]),
}
}
fn source3(source: &str) -> Project {
source_project::compile_bundle(&bundle(source), ProjectLimits::default())
.unwrap()
.decoded
.project
}
fn b(source: &str) -> Project {
compile(&bundle(source)).unwrap()
}
fn ids(project: &Project) -> BTreeMap<String, oa_core::Id> {
project
.identities()
.into_iter()
.map(|(id, alias)| (alias.to_owned(), id))
.collect()
}
fn field_count(source: &str, field: &str) -> usize {
language::tokenize(source)
.iter()
.filter(|token| &source[token.span.start..token.span.end] == field)
.count()
}
#[test]
fn three_independently_authored_pairs_have_identical_complete_models() {
for (name, a, candidate) in CASES {
let a = source3(a);
let candidate = b(candidate);
assert_eq!(a, candidate, "{name}");
assert_eq!(
language::parse(&language::format(&candidate)).unwrap(),
a,
"canonical source parity: {name}"
);
assert_eq!(
ids(&a).len(),
a.elements.len() + a.relations.len() + 1,
"no accidental generated identities"
);
}
}
#[test]
fn rename_changes_one_name_without_replacing_ids_or_other_properties() {
for (name, a, candidate) in CASES {
let old = source3(a);
let (before, after) = if name == "SA" {
("Transmit report", "Transmit distress report")
} else {
("Report distress", "Report emergency")
};
let mut expected = old.clone();
let element = expected
.elements
.values_mut()
.find(|element| element.name == before)
.unwrap();
element.name = after.into();
let a_changed = a.replace(&format!("\"{before}\""), &format!("\"{after}\""));
let b_changed = candidate.replace(&format!("\"{before}\""), &format!("\"{after}\""));
assert_eq!(source3(&a_changed), expected, "{name}");
assert_eq!(b(&b_changed), expected, "{name}");
assert_eq!(ids(&old), ids(&expected));
for (original, changed) in [(a, a_changed.as_str()), (candidate, b_changed.as_str())] {
assert_eq!(
original
.lines()
.zip(changed.lines())
.filter(|(a, b)| a != b)
.count(),
1
);
}
}
}
fn moved_document(source: &str, path: &str) -> SourceBundle {
let start = source.find('{').unwrap() + 1;
let end = source.rfind('}').unwrap();
SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
(
"project.arcadia".into(),
format!("{}\n include \"{path}\";\n}}\n", &source[..start]),
),
(path.into(), source[start..end].into()),
]),
}
}
#[test]
fn document_relocation_and_forward_references_do_not_change_identity_or_semantics() {
for (name, a, candidate) in CASES {
let expected = source3(a);
for path in ["model.arcadia", "domain/subsystem/model.arcadia"] {
assert_eq!(
compile(&moved_document(candidate, path)).unwrap(),
expected,
"{name} B move"
);
assert_eq!(
source_project::compile_bundle(&moved_document(a, path), ProjectLimits::default())
.unwrap()
.decoded
.project,
expected,
"{name} A move"
);
}
}
}
// Fixture-only block movement, not part of the parser or production editing.
fn declaration_range(source: &str, prefix: &str) -> std::ops::Range<usize> {
let start = source.find(prefix).unwrap();
let open = start + source[start..].find('{').unwrap();
let mut depth = 0;
for (offset, ch) in source[open..].char_indices() {
if ch == '{' {
depth += 1;
}
if ch == '}' {
depth -= 1;
if depth == 0 {
return start..open + offset + 1;
}
}
}
panic!("Unclosed fixture declaration")
}
#[test]
fn occurrence_reordering_changes_only_order_and_retains_call_reply_identity() {
let (_, a, candidate) = CASES[2];
let old = source3(a);
let a = a.replace(
"steps [@initial_report, @accepted, @update_report]",
"steps [@update_report, @initial_report, @accepted]",
);
let moved = declaration_range(candidate, "oa_occurrence update_report");
let initial = candidate.find("oa_occurrence initial_report").unwrap();
let mut reordered = candidate.to_owned();
let block = reordered[moved.clone()].to_owned();
reordered.replace_range(moved, "");
reordered.insert_str(initial, &format!("{block}\n "));
let mut expected = old.clone();
expected
.elements
.values_mut()
.find(|element| element.alias == "reports")
.unwrap()
.steps_mut()
.unwrap()
.rotate_right(1);
assert_eq!(source3(&a), expected);
assert_eq!(b(&reordered), expected);
assert_eq!(ids(&old), ids(&expected));
}
#[test]
fn adding_a_repeat_needs_two_authored_sites_in_a_and_one_in_b() {
let (_, a, candidate) = CASES[2];
let new_body = "body { Exchange { exchange @report_message; sender @reporter_line; receiver @assessor_line; message_kind Asynchronous; reply_to none; values []; }; };";
let new_header = "oa_occurrence further_report \"Further report\" id \"00000000-0000-4000-8000-000000000316\"";
let mut a_added = a.replace(
"steps [@initial_report, @accepted, @update_report]",
"steps [@initial_report, @accepted, @update_report, @further_report]",
);
a_added.insert_str(
a_added.rfind('}').unwrap(),
&format!(" {new_header} {{ parent @reports; {new_body} }}\n"),
);
let mut b_added = candidate.to_owned();
let end_steps = b_added.rfind(" };").unwrap();
b_added.insert_str(
end_steps,
&format!(" {new_header} {{ {new_body} }}\n"),
);
let actual = b(&b_added);
assert_eq!(actual, source3(&a_added));
let old = source3(a);
for (alias, id) in ids(&old) {
assert_eq!(ids(&actual)[&alias], id);
}
assert_eq!(actual.elements.len(), old.elements.len() + 1);
assert_eq!(actual.relations, old.relations);
assert_eq!(
actual
.elements
.values()
.find(|element| element.alias == "reports")
.unwrap()
.steps()
.unwrap()
.len(),
4
);
// Current core semantics accept an incomplete A edit, with an explicit warning.
// The B steps block prevents this particular two-site omission by construction.
let a_missing_membership = a_added.replace(", @further_report]", "]");
let incomplete =
source_project::compile_bundle(&bundle(&a_missing_membership), ProjectLimits::default())
.unwrap();
assert!(
incomplete
.diagnostics
.iter()
.any(|issue| issue.diagnostic.code == "unordered_occurrence")
);
assert_ne!(incomplete.decoded.project, actual);
}
#[test]
fn explicit_field_escape_preserves_diagram_geometry_and_exact_strings() {
let (_, a, candidate) = CASES[1];
let explicit = r#"
diagram reporting_view "Reporting view" id "00000000-0000-4000-8000-000000000220" { layer Sa; projection FunctionalDataflow; description "Review \"quotation\" and @literal, {braces}, λ."; }
node send_placement id "00000000-0000-4000-8000-000000000221" { diagram @reporting_view; element @send_report; position { x -0.0; y 32.5; }; size { width 200.0; height 100.0; }; collapsed false; }
"#;
let add = |source: &str| {
let mut result = source.to_owned();
result.insert_str(result.rfind('}').unwrap(), explicit);
result
};
let actual = b(&add(candidate));
assert_eq!(actual, source3(&add(a)));
let node = actual
.diagrams
.values()
.next()
.unwrap()
.nodes
.values()
.next()
.unwrap();
assert_eq!(node.position.x.to_bits(), (-0.0_f32).to_bits());
}
#[test]
fn nesting_never_infers_allocations_or_crosses_semantic_ownership_rules() {
let (_, _, source) = CASES[1];
let missing_work = source
.lines()
.filter(|line| !line.trim_start().starts_with("allocation "))
.collect::<Vec<_>>()
.join("\n");
assert!(
compile(&bundle(&missing_work))
.unwrap_err()
.contains("actual work allocation"),
"The shorthand must not invent allocations to repair dependent port allocations"
);
let no_allocations = source
.lines()
.filter(|line| {
!["allocation ", "port_allocation ", "exchange_allocation "]
.iter()
.any(|prefix| line.trim_start().starts_with(prefix))
})
.collect::<Vec<_>>()
.join("\n");
let actual = b(&no_allocations);
let mut expected = b(source);
expected.relations.retain(|_, relation| {
!matches!(
relation.kind(),
oa_core::RelationKind::Allocation
| oa_core::RelationKind::PortAllocation
| oa_core::RelationKind::ExchangeAllocation
)
});
assert_eq!(actual, expected);
// A nested function under the system is not silently converted into an allocation.
let invalid = source.replace(
"sa_function manage \"Manage reporting\"",
"sa_system manage \"Manage reporting\"",
);
assert!(compile(&bundle(&invalid)).is_err());
let wrong_owner = source.replace(
"sa_function send_report \"Transmit report\"",
"sa_actor send_report \"Transmit report\"",
);
assert!(
compile(&bundle(&wrong_owner))
.unwrap_err()
.contains("SA function owner")
);
}
#[test]
fn alias_rename_keeps_all_identity_references_and_cross_file_ports_resolve() {
let (_, a, candidate) = CASES[1];
let mut expected = source3(a);
expected
.elements
.values_mut()
.find(|element| element.alias == "send_report")
.unwrap()
.alias = "transmit_report".into();
assert_eq!(
b(&candidate.replace("send_report", "transmit_report")),
expected
);
assert_eq!(
source3(&a.replace("send_report", "transmit_report")),
expected
);
let exchange = candidate
.lines()
.find(|line| line.trim_start().starts_with("functional_exchange "))
.unwrap();
let owner_source =
candidate.replace(exchange, " include \"connections/reporting.arcadia\";");
let mut split = bundle(&owner_source);
split
.files
.insert("connections/reporting.arcadia".into(), exchange.into());
assert_eq!(compile(&split).unwrap(), source3(a));
let other_path = "contracts/dataflow.arcadia";
let exchange_source = split.files.remove("connections/reporting.arcadia").unwrap();
split.files.insert(other_path.into(), exchange_source);
*split.files.get_mut("project.arcadia").unwrap() =
owner_source.replace("connections/reporting.arcadia", other_path);
assert_eq!(compile(&split).unwrap(), source3(a));
}
#[test]
fn conflicts_and_bad_semantic_shortcuts_fail_explicitly() {
let (_, _, oa) = CASES[0];
let parent = oa.replace("000000000104\" {}", "000000000104\" { parent @vessel; }");
assert!(
compile(&bundle(&parent))
.unwrap_err()
.contains("Contradictory parent")
);
let (_, _, sa) = CASES[1];
let endpoints = sa.replace(
"from @report_out to @report_in items [@report_data];",
"from @report_out to @report_in items [@report_data]; { source @assess; }",
);
assert!(
compile(&bundle(&endpoints))
.unwrap_err()
.contains("explicit endpoint")
);
let reversed = sa.replace(
"from @report_out to @report_in",
"from @report_in to @report_out",
);
assert!(
compile(&bundle(&reversed)).is_err(),
"Core must reject wrong function port directions"
);
let missing = sa.replace("from @report_out", "from @missing");
assert!(
compile(&bundle(&missing))
.unwrap_err()
.contains("no explicit function owner")
);
let duplicate = sa.replace("000000000209", "000000000208");
assert!(
compile(&bundle(&duplicate))
.unwrap_err()
.contains("Duplicate identity")
);
let (_, _, scenario) = CASES[2];
let conflict = scenario.replace("steps {", "steps []; steps {");
assert!(
compile(&bundle(&conflict))
.unwrap_err()
.contains("Duplicate field steps")
);
let incomplete = scenario.replace("oa_occurrence accepted", "oa_activity accepted");
assert!(
compile(&bundle(&incomplete))
.unwrap_err()
.contains("occurrence declarations only")
);
}
#[test]
fn objective_fixture_metrics_are_reported_without_claiming_human_timings() {
println!(
"Task | A/B lines | A/B significant tokens | A/B explicit refs | A/B parent fields | A/B steps references | A/B owner-to-member line span"
);
for (name, a, candidate) in CASES {
let count = |source| {
language::tokenize(source)
.iter()
.filter(|token| {
!matches!(token.class, TokenClass::Comment | TokenClass::Whitespace)
})
.count()
};
let steps = |source: &str| {
source
.lines()
.filter_map(|line| line.find("steps [").map(|index| &line[index..]))
.map(|tail| tail.split(']').next().unwrap().matches('@').count())
.sum::<usize>()
};
let (parent, child) = match name {
"OA" => ("oa_activity respond", "oa_activity assess"),
"SA" => (
"sa_function send_report",
if candidate.contains("output report_out") {
"report_out \"Report output\""
} else {
unreachable!()
},
),
_ => ("oa_scenario reports", "oa_occurrence update_report"),
};
let span = |source: &str| {
source
.lines()
.position(|line| line.contains(child))
.unwrap()
.abs_diff(
source
.lines()
.position(|line| line.contains(parent))
.unwrap(),
)
};
println!(
"{name} | {}/{} | {}/{} | {}/{} | {}/{} | {}/{} | {}/{}",
a.lines().count(),
candidate.lines().count(),
count(a),
count(candidate),
field_count(a, "@"),
field_count(candidate, "@"),
field_count(a, "parent"),
field_count(candidate, "parent"),
steps(a),
steps(candidate),
span(a),
span(candidate)
);
}
}