diff --git a/AGENTS.md b/AGENTS.md index fe111d7..fd4f879 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,18 +5,8 @@ the workspace-level `AGENTS.md` first, then this repository's local ownership, architecture, validation, and delivery rules. `opy-rs` is WrightKit's standalone Rust implementation of the OverPy `.opy` -language. It is not an internal Wright frontend repository. Wright is a -consumer that may integrate `opy-rs` through native APIs or LPP, while -`opy-rs` must remain independently usable as a library and CLI. - -Terminology: - -- **frontend** means the internal Workshop-independent source → syntax → - semantic/HIR stage inside `opy-rs`; -- **provider** means an integration role exposed through LPP or another - reviewed boundary; -- neither term replaces the repository's identity as an independent OverPy - implementation. +language. Wright is a consumer that may integrate `opy-rs` through native APIs +or LPP, while `opy-rs` must remain independently usable as a library and CLI. ## Ownership boundary diff --git a/Cargo.lock b/Cargo.lock index cde06dd..c6c8543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ version = "0.1.0" dependencies = [ "clap", "clap_complete", - "opy-frontend", + "opy-rs", "serde", "serde_json", ] @@ -379,25 +379,25 @@ dependencies = [ name = "opy-compiler" version = "0.1.0" dependencies = [ - "opy-frontend", + "opy-rs", "serde_json", "workshop-rs", ] [[package]] -name = "opy-frontend" +name = "opy-macro-js" version = "0.1.0" dependencies = [ - "opy-macro-js", - "serde", + "libquickjs-ng-sys", "serde_json", ] [[package]] -name = "opy-macro-js" +name = "opy-rs" version = "0.1.0" dependencies = [ - "libquickjs-ng-sys", + "opy-macro-js", + "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index b303716..2aad634 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "AGPL-3.0-or-later" repository = "https://github.com/wrightkit/opy-rs" [workspace.dependencies] -opy-frontend = { path = "crates/opy-frontend" } +opy-rs = { path = "crates/opy-rs" } serde = "1" serde_json = "1" workshop-rs = "=0.1.11" diff --git a/README.md b/README.md index bfe65b2..347ed06 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ reconstructing supported OverPy projects. Wright is a downstream consumer that integrates `opy-rs` with broader tooling such as linting, analysis, source editing, agent workflows, CI, and language -services. The term **frontend** in this repository refers only to the -Workshop-independent source-to-semantic stage inside the implementation; it is -not the product identity of the repository. Likewise, an LPP **provider** is an +services. An LPP **provider** in this repository is an integration role that `opy-rs` may expose to Wright and other tooling clients, not the reason this repository exists. @@ -23,7 +21,7 @@ and emission. ```text OPY source ↓ -opy-rs source frontend +opy-rs parsing, preprocessing, and semantic HIR ↓ OPY semantic model / HIR ↓ @@ -66,7 +64,7 @@ opy-cli completion bash opy-cli version ``` -The Rust library surface lives in `crates/opy-frontend`, while Workshop-dependent +The Rust library surface lives in `crates/opy-rs`, while Workshop-dependent compilation lives in `crates/opy-compiler`. See the [tooling API reference](docs/opy/tooling-api.md) and [implementation role](docs/opy/implementation-role.md) for the durable boundary. @@ -91,7 +89,7 @@ compatibility corpus and pinned OverPy reference evidence. | Builtin actions & values | 🟡 Partial | Declared semantic subset works; full catalog-backed breadth is still being closed | | Receiver/member functions | 🟡 Partial | Declared members work; full member breadth is not yet complete | | Enums & constants | 🟡 Partial | Declared domains resolve; full domain breadth is not yet complete | -| Advanced directives, translations & optimizer controls | 🟡 Partial | Frontend state exists; Workshop-dependent effects remain incomplete | +| Advanced directives, translations & optimizer controls | 🟡 Partial | Source state exists; Workshop-dependent effects remain incomplete | | OPY → Workshop compilation | 🟡 Partial | The compiler boundary exists and lowering is expanding through `workshop-rs`; full real-project compilation is not yet claimed | | Workshop → OPY reconstruction | ⏳ Not yet | Will consume canonical `workshop-rs` semantics and remain owned by `opy-rs` | diff --git a/compatibility/README.md b/compatibility/README.md index deaf503..7e1a157 100644 --- a/compatibility/README.md +++ b/compatibility/README.md @@ -3,7 +3,7 @@ This directory contains the reproducible compatibility harness described by [`docs/opy/tooling-notes.md`](../docs/opy/tooling-notes.md). It is evaluation tooling, not a dependency of the opy-rs core, and it is fully independent of -the Rust crates: no Node toolchain and no frontend crate is required to run +the Rust crates: no Node toolchain and no source implementation crate is required to run the harness tests or the fixture snapshot checks. ## Pinned oracle @@ -95,18 +95,18 @@ The harness tests (`compatibility/tests/`) run without the oracle installed: (the machine-readable support matrix structure and evidence paths). The wright-side npm-packaging tests are not ported (they test wright's release tooling). The manifest probe validator -(`crates/opy-frontend/src/manifest/probes/validate.py`) is frontend-owned and +(`crates/opy-rs/src/manifest/probes/validate.py`) is source implementation-owned and runs standalone against the pinned oracle (Node + pnpm required), like `run_oracle.py`; it is not part of the oracle-less harness suite. ## Native differential runner (issue #7) -The native frontend side of the differential contract lives in -`crates/opy-frontend/tests/differential.rs` and runs in `cargo test` with no +The native source implementation side of the differential contract lives in +`crates/opy-rs/tests/differential.rs` and runs in `cargo test` with no Node or OverPy installed: ```sh -cargo test -p opy-frontend --test differential +cargo test -p opy-rs --test differential ``` It compiles every fixture through the native pipeline (preprocess → parse → @@ -146,8 +146,8 @@ python3 compatibility/diff.py \ ``` `your-producer` is a placeholder: this repository ships no producer for -`diff.py`. The native frontend comparison runs inside `cargo test` -(`crates/opy-frontend/tests/differential.rs`) against the recorded oracle +`diff.py`. The native source implementation comparison runs inside `cargo test` +(`crates/opy-rs/tests/differential.rs`) against the recorded oracle snapshots directly. The producer must write a result with the same schema as the oracle's @@ -174,7 +174,7 @@ and exit 2 by default, so a CI job cannot silently pass without a producer. Use `--allow-inconclusive` only for local contract checks. The opy-rs producer side of the differential contract is the native Rust -suite (`crates/opy-frontend/tests/differential.rs`), which runs in `cargo test` +suite (`crates/opy-rs/tests/differential.rs`), which runs in `cargo test` with no Node or OverPy installed; `diff.py` remains the generic external-producer contract for other producers, exercised locally via `run_oracle.py` and the corpus snapshots. diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index 258672f..a1bf800 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -7,31 +7,31 @@ "classification": "match, known-gap, and unsupported are expected relationships. unexpected-divergence, regression, and inconclusive are produced by the runner." }, "cases": [ - {"fixture": "synthetic/basic-rule", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/basic-rule/oracle.json", "implementation-invariant:frontend-resolves-basic-rule"], "note": "Minimal rule is part of the Workshop-independent frontend contract."}, - {"fixture": "synthetic/control-flow", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/control-flow/oracle.json", "implementation-invariant:frontend-resolves-control-flow"], "note": "If/elif/else, for-in-range, while, and pass resolve in the OPY semantic model."}, + {"fixture": "synthetic/basic-rule", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/basic-rule/oracle.json", "implementation-invariant:source implementation-resolves-basic-rule"], "note": "Minimal rule is part of the Workshop-independent source implementation contract."}, + {"fixture": "synthetic/control-flow", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/control-flow/oracle.json", "implementation-invariant:source implementation-resolves-control-flow"], "note": "If/elif/else, for-in-range, while, and pass resolve in the OPY semantic model."}, {"fixture": "synthetic/issue-28-syntax", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-28-syntax/oracle.json", "implementation-invariant:issue-28-pure-syntax-hir"], "note": "Switch, do-while, hexadecimal literals, membership, dict indexing, comprehensions, lambda arguments, and f/w/b/c string modifiers resolve through the OPY HIR."}, - {"fixture": "synthetic/issue-28-string-modifiers", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-28-string-modifiers/oracle.json", "implementation-invariant:issue-28-string-modifier-hir"], "note": "The pinned oracle accepts the inventory-backed f/w/b/c modifier forms; l/t remain syntax-carried because translation content is outside this frontend."}, + {"fixture": "synthetic/issue-28-string-modifiers", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-28-string-modifiers/oracle.json", "implementation-invariant:issue-28-string-modifier-hir"], "note": "The pinned oracle accepts the inventory-backed f/w/b/c modifier forms; l/t remain syntax-carried because translation content is outside this source implementation."}, {"fixture": "synthetic/issue-33-switch-break", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-33-switch-break/oracle.json", "implementation-invariant:issue-33-fallthrough-break-hir"], "note": "The pinned oracle accepts source-order switch fallthrough and nested break; native HIR retains the same authored arm order and explicit break statements."}, {"fixture": "synthetic/issue-33-f-string", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-33-f-string/oracle.json", "implementation-invariant:issue-33-format-provenance-and-lambda-slots"], "note": "The pinned oracle accepts semantic f-string interpolation and sorted key lambda syntax; native HIR retains format arguments with source provenance."}, {"fixture": "synthetic/issue-33-lambda-negative", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-33-lambda-negative/oracle.json", "implementation-invariant:issue-33-lambda-context-diagnostic"], "note": "The pinned oracle rejects a standalone lambda argument; native emits the stable lambda-context diagnostic."}, {"fixture": "synthetic/issue-28-invalid-syntax", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-28-invalid-syntax/oracle.json", "implementation-invariant:issue-28-structured-negative-diagnostics"], "note": "Malformed do-while and dictionary syntax remain structured failures rather than silent acceptance."}, - {"fixture": "synthetic/declarations-numbers", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/declarations-numbers/oracle.json", "implementation-invariant:frontend-resolves-declarations"], "note": "Numeric literals and variable-index declarations resolve in the OPY semantic model."}, - {"fixture": "synthetic/declarations-rules", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/declarations-rules/oracle.json", "implementation-invariant:frontend-resolves-rule-declarations"], "note": "globalvar, playervar, subroutine, def, enum, and rule declarations resolve."}, - {"fixture": "synthetic/expressions-values", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/expressions-values/oracle.json", "implementation-invariant:frontend-resolves-expressions"], "note": "Expressions, arrays, strings, vectors, calls, and format expressions resolve."}, + {"fixture": "synthetic/declarations-numbers", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/declarations-numbers/oracle.json", "implementation-invariant:source implementation-resolves-declarations"], "note": "Numeric literals and variable-index declarations resolve in the OPY semantic model."}, + {"fixture": "synthetic/declarations-rules", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/declarations-rules/oracle.json", "implementation-invariant:source implementation-resolves-rule-declarations"], "note": "globalvar, playervar, subroutine, def, enum, and rule declarations resolve."}, + {"fixture": "synthetic/expressions-values", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/expressions-values/oracle.json", "implementation-invariant:source implementation-resolves-expressions"], "note": "Expressions, arrays, strings, vectors, calls, and format expressions resolve."}, {"fixture": "synthetic/preprocessing", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/preprocessing/oracle.json", "implementation-invariant:preprocessor-include-define-undef"], "note": "Include, object-like and function-like defines, and undef are preserved through preprocessing."}, {"fixture": "synthetic/issue-29-directives", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-29-directives/oracle.json", "implementation-invariant:issue-29-preprocessing-state"], "note": "Advanced directive state and source annotations resolve without executing Workshop optimizer, localization, or emission effects."}, {"fixture": "synthetic/issue-29-invalid", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-29-invalid/oracle.json", "implementation-invariant:issue-29-structured-directive-diagnostics"], "note": "Malformed preprocessing and annotation forms remain source-located structured diagnostics."}, - {"fixture": "synthetic/issue-29-main-file", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-29-main-file/oracle.json", "implementation-invariant:issue-29-main-file-scope"], "note": "mainFile redirects the frontend entry point and preserves child-include prefix state without copying catalog data."}, + {"fixture": "synthetic/issue-29-main-file", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-29-main-file/oracle.json", "implementation-invariant:issue-29-main-file-scope"], "note": "mainFile redirects the source implementation entry point and preserves child-include prefix state without copying catalog data."}, {"fixture": "synthetic/issue-31-positive", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-31-positive/oracle.json", "implementation-invariant:issue-31-directive-surfaces"], "note": "Pinned positive probe covers global rulePrefixTemplate, include prefix restoration, AST macro/enum redeclaration, and translation normalization."}, - {"fixture": "synthetic/issue-31-negative", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-31-negative/oracle.json", "implementation-invariant:issue-31-pinned-translation-set"], "note": "An outside-code translation is rejected by both the pinned oracle and native frontend."}, + {"fixture": "synthetic/issue-31-negative", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-31-negative/oracle.json", "implementation-invariant:issue-31-pinned-translation-set"], "note": "An outside-code translation is rejected by both the pinned oracle and native source implementation."}, {"fixture": "synthetic/issue-31-nested-scope", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-31-nested-scope/oracle.json", "implementation-invariant:issue-31-scoped-preprocessing"], "note": "Nested include optimization directives remain observable as scoped preprocessing events; optimizer execution is not claimed by opy-rs."}, {"fixture": "synthetic/settings", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/settings/oracle.json", "implementation-invariant:settings-structure-only"], "note": "Settings are structurally represented; Workshop key and leaf validation remains lowering-dependent."}, {"fixture": "synthetic/receiver-calls", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/receiver-calls/oracle.json", "implementation-invariant:receiver-call-resolution"], "note": "The exercised receiver/member forms resolve through the OPY semantic model."}, {"fixture": "synthetic/receiver-playervar", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/receiver-playervar/oracle.json", "implementation-invariant:receiver-playervar-member-expression"], "note": "A bare variable member expression is retained as an OPY HIR member node; canonical Workshop member validation remains lowering-dependent."}, {"fixture": "synthetic/chase-enums", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-enums/oracle.json", "implementation-invariant:opaque-workshop-enum-identities"], "note": "Declared chase enum identities resolve without duplicating Workshop catalog validation."}, - {"fixture": "synthetic/chase-condition-agentlab", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-condition-agentlab/oracle.json", "regression:agent-lab-chase-condition-shape"], "note": "The agent-lab chase condition shape is retained as an OPY frontend regression."}, + {"fixture": "synthetic/chase-condition-agentlab", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-condition-agentlab/oracle.json", "regression:agent-lab-chase-condition-shape"], "note": "The agent-lab chase condition shape is retained as an OPY source implementation regression."}, {"fixture": "synthetic/chase-keywords", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-keywords/oracle.json", "implementation-invariant:keyword-binding"], "note": "Generic keyword binding and the chase contextual form resolve."}, - {"fixture": "synthetic/for-range-agentlab", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/for-range-agentlab/oracle.json", "regression:agent-lab-for-range-binder"], "note": "Implicit for-range binders are retained as an OPY frontend regression."}, + {"fixture": "synthetic/for-range-agentlab", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/for-range-agentlab/oracle.json", "regression:agent-lab-for-range-binder"], "note": "Implicit for-range binders are retained as an OPY source implementation regression."}, {"fixture": "synthetic/diagnostics", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/diagnostics/oracle.json", "implementation-invariant:parse-error-code"], "note": "Malformed source must produce the stable parse-error diagnostic."}, {"fixture": "real-world/overpy-cake", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:real-world/overpy-cake/oracle.json", "provenance:real-world/overpy-cake/fixture.json"], "note": "Pinned OverPy example resolves through the native OPY semantic model."}, {"fixture": "real-world/overpy-pixelart", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/overpy-pixelart/oracle.json", "provenance:real-world/overpy-pixelart/fixture.json"], "note": "Pinned OverPy example resolves; emitted rule shape is not the contract."}, @@ -46,17 +46,17 @@ {"fixture": "real-world/overpy-zencopter", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/overpy-zencopter/oracle.json", "provenance:real-world/overpy-zencopter/fixture.json"], "note": "Both sides reject the pinned upstream example; native diagnostic wording is not used as semantic evidence."}, {"fixture": "real-world/ow1-emulator", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/ow1-emulator/oracle.json", "provenance:real-world/ow1-emulator/fixture.json"], "note": "The full project remains preserved as a failure corpus case with recorded provenance."}, {"fixture": "real-world/6v6-adjustments", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/6v6-adjustments/oracle.json", "provenance:real-world/6v6-adjustments/fixture.json"], "note": "The full project remains preserved as a failure corpus case with recorded provenance."}, - {"fixture": "synthetic/issue-35-integration", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-35-integration/oracle.json", "implementation-invariant:opy-compiler-vertical-slice"], "note": "The OPY frontend resolves the source fixture; the dedicated opy-compiler test independently lowers it through canonical WIR validation and deterministic workshop-rs emission."}, + {"fixture": "synthetic/issue-35-integration", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-35-integration/oracle.json", "implementation-invariant:opy-compiler-vertical-slice"], "note": "The OPY source implementation resolves the source fixture; the dedicated opy-compiler test independently lowers it through canonical WIR validation and deterministic workshop-rs emission."}, {"fixture": "synthetic/issue-40-structural", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-40-structural/oracle.json", "implementation-invariant:opy-compiler-structural-lowering"], "note": "The pinned oracle records subroutine source identity, deterministic explicit/implicit variable allocation, and player event filters; the dedicated opy-compiler test independently asserts those structures in canonical WIR."}, {"fixture": "synthetic/issue-46-primitives", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-46-primitives/oracle.json", "implementation-invariant:opy-compiler-primitive-lowering", "test:opy-compiler-issue-46-oracle-equivalence"], "note": "The pinned oracle records the evidenced assignment and modification lowering (including **= and single-level indexed forms), value expressions, array indexing (including the firstOf index-0 read normalization), not-comparison negation, implicit default variables at fixed Workshop slots, and non-null variable initializers; null-default initializer semantics and deeper indexed writes remain explicit follow-ups. The opy-compiler test suite reparses both the native output and the oracle Workshop text through the canonical workshop-rs parser and requires structural equivalence."}, - {"fixture": "synthetic/issue-46-unsupported", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-46-unsupported/oracle.json", "implementation-invariant:opy-compiler-primitive-lowering-limits"], "note": "Negative #46 probe: the frontend resolves and the pinned oracle compiles the dict-indexed assignment, while the native compiler rejects the dict primitive with the stable source-attributed unsupported-integration-surface diagnostic."}, - {"fixture": "synthetic/issue-47-control-flow", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-control-flow/oracle.json", "test:opy-compiler-issue-47-oracle-equivalence"], "note": "The #47 control-flow slice resolves in the frontend and lowers to canonical WIR with structural equivalence to the pinned oracle; direct loop/switch break and the nested switch shape are independently covered by opy-compiler tests."}, - {"fixture": "synthetic/issue-47-unsupported", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-unsupported/oracle.json", "test:opy-compiler-issue-47-nested-negative"], "note": "Negative #47 probe: the frontend and pinned oracle accept the nested conditional switch-break HIR, while the compiler rejects the form because canonical WIR has no equivalent structured break carrier."}, - {"fixture": "synthetic/issue-47-switch-order", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-switch-order/oracle.json", "test:opy-compiler-issue-47-switch-order"], "note": "The #47 default-before-case probe preserves authored arm order and fallthrough in the frontend; native and pinned Workshop both reparse to equivalent canonical WIR."}, + {"fixture": "synthetic/issue-46-unsupported", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-46-unsupported/oracle.json", "implementation-invariant:opy-compiler-primitive-lowering-limits"], "note": "Negative #46 probe: the source implementation resolves and the pinned oracle compiles the dict-indexed assignment, while the native compiler rejects the dict primitive with the stable source-attributed unsupported-integration-surface diagnostic."}, + {"fixture": "synthetic/issue-47-control-flow", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-control-flow/oracle.json", "test:opy-compiler-issue-47-oracle-equivalence"], "note": "The #47 control-flow slice resolves in the source implementation and lowers to canonical WIR with structural equivalence to the pinned oracle; direct loop/switch break and the nested switch shape are independently covered by opy-compiler tests."}, + {"fixture": "synthetic/issue-47-unsupported", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-unsupported/oracle.json", "test:opy-compiler-issue-47-nested-negative"], "note": "Negative #47 probe: the source implementation and pinned oracle accept the nested conditional switch-break HIR, while the compiler rejects the form because canonical WIR has no equivalent structured break carrier."}, + {"fixture": "synthetic/issue-47-switch-order", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-switch-order/oracle.json", "test:opy-compiler-issue-47-switch-order"], "note": "The #47 default-before-case probe preserves authored arm order and fallthrough in the source implementation; native and pinned Workshop both reparse to equivalent canonical WIR."}, {"fixture": "synthetic/issue-47-switch-structured-target", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-switch-structured-target/oracle.json", "test:opy-compiler-issue-47-structured-switch-target"], "note": "The #47 structured switch probe places nested if/while actions in an earlier arm and verifies later case/default targets against canonical emitted action widths through native/oracle WIR equivalence."}, - {"fixture": "synthetic/issue-47-switch-multiple-break", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-switch-multiple-break/oracle.json", "test:opy-compiler-issue-47-multiple-switch-break"], "note": "The frontend preserves the multi-break source and the pinned oracle accepts it; the compiler rejects the later-reachable multi-target shape explicitly because workshop-rs v0.1.11 has one canonical else carrier."}, + {"fixture": "synthetic/issue-47-switch-multiple-break", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-switch-multiple-break/oracle.json", "test:opy-compiler-issue-47-multiple-switch-break"], "note": "The source implementation preserves the multi-break source and the pinned oracle accepts it; the compiler rejects the later-reachable multi-target shape explicitly because workshop-rs v0.1.11 has one canonical else carrier."}, {"fixture": "synthetic/issue-47-do-while-shapes", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-do-while-shapes/oracle.json", "test:opy-compiler-issue-47-do-while-break-shapes"], "note": "Direct, conditional, and nested do-while break shapes resolve and match the pinned Workshop after native and oracle reparsing through workshop-rs."}, - {"fixture": "synthetic/issue-47-do-while-invalid-placement", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-do-while-invalid-placement/oracle.json", "test:opy-compiler-issue-47-invalid-do-while-placement"], "note": "The frontend reports the stable source-attributed do-while-placement diagnostic for a non-prefix do-while."}, + {"fixture": "synthetic/issue-47-do-while-invalid-placement", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-do-while-invalid-placement/oracle.json", "test:opy-compiler-issue-47-invalid-do-while-placement"], "note": "The source implementation reports the stable source-attributed do-while-placement diagnostic for a non-prefix do-while."}, {"fixture": "census/workshop-feature-census", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:census/workshop-feature-census/oracle.json", "contract:workshop-rs#10-feature-census"], "note": "OPy source records opaque Workshop feature identities for the future workshop-rs lowering boundary."} ] } diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index 8811ad7..736ca50 100644 --- a/compatibility/fixtures/README.md +++ b/compatibility/fixtures/README.md @@ -57,10 +57,10 @@ repository: | `issue-46-primitives` | #46 oracle-backed primitive lowering probe: assignments and modifications (including `**=`), expressions, indexing, format, initializers, implicit default variables at fixed slots; the snapshot constrains the native compiler through structural equivalence | | `issue-46-unsupported` | #46 negative probe: a dict-indexed assignment the compiler rejects with the stable source-attributed diagnostic while the oracle compiles it | | `issue-47-control-flow` | #47 pinned oracle-backed control-flow lowering probe: if/elif/else, while, range-for, do-while expansion, switch fallthrough/default, and direct break | -| `issue-47-unsupported` | #47 negative probe: a break hidden inside a conditional switch arm is accepted by the frontend/oracle but rejected by the compiler with a stable source-attributed diagnostic | +| `issue-47-unsupported` | #47 negative probe: a break hidden inside a conditional switch arm is accepted by the source implementation/oracle but rejected by the compiler with a stable source-attributed diagnostic | | `issue-47-switch-order` | #47 pinned oracle probe for a default arm before later case arms and source-order fallthrough | | `issue-47-switch-structured-target` | #47 pinned oracle probe for nested if/while structure in an earlier arm and later case/default targets | -| `issue-47-switch-multiple-break` | #47 pinned oracle probe for multiple direct breaks; frontend preserves the source while the compiler reports the canonical multi-target WIR gap | +| `issue-47-switch-multiple-break` | #47 pinned oracle probe for multiple direct breaks; the source implementation preserves the source while the compiler reports the canonical multi-target WIR gap | | `issue-47-do-while-shapes` | #47 pinned oracle probe for direct, conditional, and nested do-while break lowering | | `issue-47-do-while-invalid-placement` | #47 pinned negative probe for the stable do-while placement diagnostic | | `issue-29-*` | directive/include/main-file preprocessing probes | diff --git a/compatibility/fixtures/synthetic/chase-condition-agentlab/fixture.json b/compatibility/fixtures/synthetic/chase-condition-agentlab/fixture.json index 024173f..babb07c 100644 --- a/compatibility/fixtures/synthetic/chase-condition-agentlab/fixture.json +++ b/compatibility/fixtures/synthetic/chase-condition-agentlab/fixture.json @@ -15,5 +15,5 @@ "license": "AGPL-3.0-or-later", "redistributable": true }, - "provenanceNote": "Synthetic-original source authored by Wright for the corpus, reproducing the rule shape of the ChaseTimeReeval.NONE discovery fixture wrightkit/agent-lab tools/test/fixtures/analyzer/chase-condition.opy (blob SHA 2938baff2bac77f8818309ac1e356cf5b9e15d56); agent-lab declares no redistribution license, so its source stays out of the corpus. The pinned oracle compiles this shape (expectedStatus success, with its w_ow2_rule_condition_chase warning recorded in oracle.json). Since #109, Wright's native frontend resolves chaseOverTime (action) and isGameInProgress (value) through the OPY semantic compatibility manifest (crates/opy-frontend/src/manifest) and compiles this fixture end-to-end via the Workshop emission catalog; the manifest entries and their emission spellings are validated by the synthetic manifest probes (crates/opy-frontend/src/manifest/probes, #109)." + "provenanceNote": "Synthetic-original source authored by Wright for the corpus, reproducing the rule shape of the ChaseTimeReeval.NONE discovery fixture wrightkit/agent-lab tools/test/fixtures/analyzer/chase-condition.opy (blob SHA 2938baff2bac77f8818309ac1e356cf5b9e15d56); agent-lab declares no redistribution license, so its source stays out of the corpus. The pinned oracle compiles this shape (expectedStatus success, with its w_ow2_rule_condition_chase warning recorded in oracle.json). Since #109, Wright's native source implementation resolves chaseOverTime (action) and isGameInProgress (value) through the OPY semantic compatibility manifest (crates/opy-rs/src/manifest) and compiles this fixture end-to-end via the Workshop emission catalog; the manifest entries and their emission spellings are validated by the synthetic manifest probes (crates/opy-rs/src/manifest/probes, #109)." } diff --git a/compatibility/fixtures/synthetic/chase-enums/fixture.json b/compatibility/fixtures/synthetic/chase-enums/fixture.json index 999df11..ae48239 100644 --- a/compatibility/fixtures/synthetic/chase-enums/fixture.json +++ b/compatibility/fixtures/synthetic/chase-enums/fixture.json @@ -11,5 +11,5 @@ "license": "AGPL-3.0-or-later", "redistributable": true }, - "provenanceNote": "Synthetic-original source authored by Wright for the corpus. It reproduces the ChaseTimeReeval.NONE usage discovered in wrightkit/agent-lab tools/test/fixtures/analyzer/chase-condition.opy (blob SHA 2938baff2bac77f8818309ac1e356cf5b9e15d56) in a fully supported native-frontend context (assignments), because agent-lab declares no redistribution license and its source stays out of the corpus. Member spellings and emitted Workshop text were reference-validated against the pinned OverPy 9.7.10 oracle (see docs/opy/support-matrix.md and crates/wright-workshop/src/catalog/data/catalog.json provenance)." + "provenanceNote": "Synthetic-original source authored by Wright for the corpus. It reproduces the ChaseTimeReeval.NONE usage discovered in wrightkit/agent-lab tools/test/fixtures/analyzer/chase-condition.opy (blob SHA 2938baff2bac77f8818309ac1e356cf5b9e15d56) in a fully supported native-source implementation context (assignments), because agent-lab declares no redistribution license and its source stays out of the corpus. Member spellings and emitted Workshop text were reference-validated against the pinned OverPy 9.7.10 oracle (see docs/opy/support-matrix.md and crates/wright-workshop/src/catalog/data/catalog.json provenance)." } diff --git a/compatibility/fixtures/synthetic/chase-keywords/fixture.json b/compatibility/fixtures/synthetic/chase-keywords/fixture.json index d51b04d..67c0286 100644 --- a/compatibility/fixtures/synthetic/chase-keywords/fixture.json +++ b/compatibility/fixtures/synthetic/chase-keywords/fixture.json @@ -11,5 +11,5 @@ "license": "AGPL-3.0-or-later", "redistributable": true }, - "provenanceNote": "Synthetic-original source authored by Wright for the corpus. It reproduces the chase(...) keyword forms discovered in the real-world overpy-meipocalypse corpus (chase(variable, destination, rate=..., ChaseReeval.NONE), e.g. mei_types.opy and fightforyourlife.opy) in a fully supported native-frontend context, and exercises the generic keyword-argument binding for wait/vect/len/print/getPlayersInRadius/setStatusEffect. Reference-validated against the pinned OverPy 9.7.10 oracle (issue #110); the meipocalypse fixture itself stays out of the corpus because its broader surface (list comprehensions, Team.1, script hooks) is not native-supported." + "provenanceNote": "Synthetic-original source authored by Wright for the corpus. It reproduces the chase(...) keyword forms discovered in the real-world overpy-meipocalypse corpus (chase(variable, destination, rate=..., ChaseReeval.NONE), e.g. mei_types.opy and fightforyourlife.opy) in a fully supported native-source implementation context, and exercises the generic keyword-argument binding for wait/vect/len/print/getPlayersInRadius/setStatusEffect. Reference-validated against the pinned OverPy 9.7.10 oracle (issue #110); the meipocalypse fixture itself stays out of the corpus because its broader surface (list comprehensions, Team.1, script hooks) is not native-supported." } diff --git a/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json b/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json index 9215f5a..29a133d 100644 --- a/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json +++ b/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json @@ -9,7 +9,7 @@ "expectedStatus": "success", "provenance": { "kind": "original", - "origin": "opy-rs Issue #46 negative primitive-lowering probe (frontend resolves; compiler rejects the dict primitive with a stable source-attributed diagnostic)", + "origin": "opy-rs Issue #46 negative primitive-lowering probe (source implementation resolves; compiler rejects the dict primitive with a stable source-attributed diagnostic)", "license": "AGPL-3.0-or-later", "redistributable": true } diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 247030e..41ff4a1 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -10,12 +10,12 @@ }, "snapshot": { "date": "2026-08-24", - "note": "Readiness baseline for #7 through the #28/#33 Draft PR series plus the bounded #35, #40, #46, and #47 integration slices. Frontend-supported rows include the pinned OPY syntax, directives, preprocessing, macro statements, rule directives/model, JavaScript macros, and runtime hooks. Semantic-supported rows include declaration resolution, for-loop binders, modules, keyword arguments, the declared alias surface, and the OPY-owned manifest overlay for builtin/member/enum semantics. Canonical Workshop builtin/member/enum breadth and emission remain separate lowering-dependent rows; no Workshop catalog data is copied into opy-rs. The differential corpus currently reports 0 unexpected divergences and 0 inconclusive results; its exact match and known-gap counts are generated evidence, not a support claim. #!postCompileHook is parsed/validated/recorded by the frontend; execution against final Workshop text is lowering-dependent (#8).", + "note": "Readiness baseline for #7 through the #28/#33 Draft PR series plus the bounded #35, #40, #46, and #47 integration slices. Source implementation-supported rows include the pinned OPY syntax, directives, preprocessing, macro statements, rule directives/model, JavaScript macros, and runtime hooks. Semantic-supported rows include declaration resolution, for-loop binders, modules, keyword arguments, the declared alias surface, and the OPY-owned manifest overlay for builtin/member/enum semantics. Canonical Workshop builtin/member/enum breadth and emission remain separate lowering-dependent rows; no Workshop catalog data is copied into opy-rs. The differential corpus currently reports 0 unexpected divergences and 0 inconclusive results; its exact match and known-gap counts are generated evidence, not a support claim. #!postCompileHook is parsed/validated/recorded by the source implementation; execution against final Workshop text is lowering-dependent (#8).", "asOfCommit": "c5714a4028464f19f236f54d390e7b7237e434bd" }, "states": { "planned": "Declared surface, not yet implemented in opy-rs; evidence/provenance recorded for the implementing issue.", - "frontend-supported": "Frontend behavior (lex/parse/preprocess/macro expansion) implemented in opy-rs and corpus-evidenced.", + "source-supported": "Source implementation behavior (lex/parse/preprocess/macro expansion) implemented in opy-rs and corpus-evidenced.", "semantic-supported": "Semantic resolution implemented in opy-rs (names, members, enums, call semantics covered by the semantic model).", "lowering-dependent": "Completion requires canonical Workshop semantics/catalog/emission owned by workshop-rs; inventory-only until the integration stage.", "end-to-end-supported": "A declared feature or bounded integration slice has explicit OPY -> HIR -> WIR -> validated Workshop emission evidence for that scope; this state does not imply full-language OPY-to-Workshop parity." @@ -37,7 +37,7 @@ "id": "syntax/lexing", "name": "Lexer: identifiers, literals, strings, comments, #! directives, operators", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/basic-rule", "fixtures:synthetic/control-flow", @@ -55,7 +55,7 @@ "id": "syntax/expressions", "name": "Expression/postfix/member/call grammar, precedence, indexing", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/expressions-values", "fixtures:synthetic/receiver-calls", @@ -70,7 +70,7 @@ "id": "syntax/declarations", "name": "Declarations: globalvar/playervar (index + initializer), subroutine, def, enum, macro constants", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/declarations-rules", "fixtures:synthetic/declarations-numbers", @@ -86,7 +86,7 @@ "id": "syntax/assignments-control-flow", "name": "Assignments and control flow: =, +=, -=, *=, /=, %=, **=, if/elif/else, for-in-range, while, pass", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/control-flow", "fixtures:synthetic/for-range-agentlab", @@ -100,7 +100,7 @@ "id": "syntax/switch", "name": "switch/case/default", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "fixtures:synthetic/issue-33-switch-break", "upstream:src/tests/switches.opy"], "notes": "Issue #28/#33: switch/case/default preserve source-order arms and fall through into subsequent arms; explicit break exits the innermost switch or loop. Workshop lowering remains out of scope." }, @@ -108,7 +108,7 @@ "id": "syntax/break", "name": "break statements in switch and loops", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-33-switch-break", "upstream:src/tests/switches.opy", "upstream:src/tests/loops.opy"], "notes": "Issue #33: break is a real HIR statement, validates its enclosing switch/loop context, and is retained without implicit arm exits." }, @@ -116,7 +116,7 @@ "id": "syntax/do-while", "name": "do … while", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "fixtures:real-world/overpy-santa/regressions/do-while.opy", "upstream:src/tests/loops.opy"], "notes": "Issue #28: the body executes before the condition and the condition is retained as an OPY HIR expression; malformed/truncated conditions remain structured parse errors." }, @@ -124,7 +124,7 @@ "id": "syntax/hex-literals", "name": "Hexadecimal numeric literals (0x/0X)", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "upstream:src/tests/operators.opy"], "notes": "Issue #28: hexadecimal source spelling is preserved in the numeric HIR text while the numeric value is decoded independently." }, @@ -132,7 +132,7 @@ "id": "syntax/membership", "name": "Expression-level in/not in membership", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "upstream:src/tests/operators.opy"], "notes": "Issue #28: in/not in are represented as source-semantic binary operators; no Workshop Array Contains catalog or lowering is copied here." }, @@ -140,7 +140,7 @@ "id": "syntax/string-modifiers", "name": "String modifiers (f/w/l/b/c/t)", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-string-modifiers", "fixtures:synthetic/issue-28-syntax", "fixtures:synthetic/issue-33-f-string", "upstream:src/tests/strings.opy"], "notes": "Issue #28/#33: f-string interpolation lowers to a semantic format node with source-spanned arguments; w/b/c remain semantic string modifiers and l/t remain syntax-carried without duplicating translation data." }, @@ -148,7 +148,7 @@ "id": "syntax/dicts", "name": "Dictionary literals and keyed access", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "upstream:src/tests/dicts.opy"], "notes": "Issue #28: dictionary entries and keyed access are represented in OPY HIR; bare dictionaries and malformed entries produce structured diagnostics." }, @@ -156,7 +156,7 @@ "id": "syntax/comprehensions", "name": "List comprehensions (mapping/filtering, element and index binders)", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "upstream:src/tests/dicts.opy"], "notes": "Issue #28: one-for-clause comprehensions with an optional filter and optional element/index binders preserve local scope in HIR." }, @@ -164,7 +164,7 @@ "id": "syntax/lambda", "name": "Lambda expressions for array operations", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": ["fixtures:synthetic/issue-28-syntax", "fixtures:synthetic/issue-33-f-string", "fixtures:synthetic/issue-33-lambda-negative", "upstream:src/tests/dicts.opy"], "notes": "Issue #28/#33: lambda x: expr is accepted only in signature-approved positions (sorted index 1/key and array map/filter/all/any index 0); standalone and other argument positions produce a structured diagnostic." }, @@ -172,7 +172,7 @@ "id": "syntax/settings-blocks", "name": "settings { ... } custom-game-settings blocks: JSONC parse into the typed HIR payload", "category": "syntax", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/settings", "fixtures:real-world/overpy-meipocalypse/settings.opy", @@ -264,7 +264,7 @@ "probes:receiver-calls", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #30/#8. Canonical receiver member lists, member existence, content-specific receiver/domain validation, localized spellings, and emission are Workshop-owned. The frontend preserves unknown/member diagnostics that can be decided from OPY metadata and defers catalog checks to integration." + "notes": "Issue #30/#8. Canonical receiver member lists, member existence, content-specific receiver/domain validation, localized spellings, and emission are Workshop-owned. The source implementation preserves unknown/member diagnostics that can be decided from OPY metadata and defers catalog checks to integration." }, { "id": "semantics/receiver-playervar", @@ -276,7 +276,7 @@ "upstream:src/tests/variables.opy", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #30 repair. The pinned oracle accepts this OPY-owned receiver form and the frontend preserves its variable receiver and member identity in HIR with source provenance. Canonical member existence and emission remain Workshop-owned and lowering-dependent; #8 must not be used to claim the OPY expression incomplete." + "notes": "Issue #30 repair. The pinned oracle accepts this OPY-owned receiver form and the source implementation preserves its variable receiver and member identity in HIR with source provenance. Canonical member existence and emission remain Workshop-owned and lowering-dependent; #8 must not be used to claim the OPY expression incomplete." }, { "id": "semantics/enum-domains", @@ -350,15 +350,15 @@ }, { "id": "semantics/diagnostics", - "name": "Source identity and diagnostics: structured, source-located frontend errors, wright-result/v1 envelope", + "name": "Source identity and diagnostics: structured, source-located source implementation errors, wright-result/v1 envelope", "category": "semantics", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/diagnostics", "fixtures:synthetic/declarations-rules", "upstream:runTests.mjs" ], - "notes": "Structured stable-code diagnostics with 1-based spans; the stable-code contract is documented in docs/opy/tooling-api.md and tested in opy-frontend tooling tests." + "notes": "Structured stable-code diagnostics with 1-based spans; the stable-code contract is documented in docs/opy/tooling-api.md and tested in opy-rs tooling tests." }, { "id": "semantics/settings-emission", @@ -370,13 +370,13 @@ "fixtures:real-world/6v6-adjustments/lobby/lobby.opy", "upstream:src/tests/customGameSettings.opy" ], - "notes": "The typed settings payload is frontend-owned; the emission table and its domains are Workshop data owned by workshop-rs. Key-existence and leaf-kind settings validation is Workshop schema content and lowering-dependent (#8) — the core validates structure only. Emitted settings are deliberately not reparseable (round-trip boundary)." + "notes": "The typed settings payload is source implementation-owned; the emission table and its domains are Workshop data owned by workshop-rs. Key-existence and leaf-kind settings validation is Workshop schema content and lowering-dependent (#8) — the core validates structure only. Emitted settings are deliberately not reparseable (round-trip boundary)." }, { "id": "preprocessing/include", "name": "#!include: root-relative resolution, cycle detection, missing-file diagnostics, file registry", "category": "preprocessing", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/preprocessing", "fixtures:real-world/ow1-emulator", @@ -391,7 +391,7 @@ "id": "preprocessing/define-undef", "name": "#!define (object- and function-like), #!undef, recursive expansion, recursion guard", "category": "preprocessing", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/preprocessing", "fixtures:real-world/overpy-cake", @@ -412,7 +412,7 @@ "upstream:src/tests/compression.opy", "upstream:src/data/opy/preprocessing.ts" ], - "notes": "Issue #29: frontend records directive state, macro-redeclaration policy, main-file provenance, and rule-prefix application. Optimizer and generated Workshop effects remain separate." + "notes": "Issue #29: source implementation records directive state, macro-redeclaration policy, main-file provenance, and rule-prefix application. Optimizer and generated Workshop effects remain separate." }, { "id": "preprocessing/directive-effects", @@ -429,7 +429,7 @@ "id": "macros/definitions", "name": "macro name(params): statement bodies with MacroParam references, macro constants", "category": "macros", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/preprocessing", "fixtures:real-world/overpy-cake", @@ -445,7 +445,7 @@ "id": "macros/javascript", "name": "__script__ JavaScript macros (QuickJS runtime); #!postCompileHook parsed and recorded", "category": "macros", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "upstream:runTests.mjs", "upstream:src/tests/postCompileHook.opy", @@ -453,12 +453,12 @@ "upstream:src/tests/quickjs-invalid-return.js", "upstream:src/tests/quickjs-runaway.js", "upstream:src/quickjs.ts", - "test:opy-frontend-macro-integration", - "test-fixture:crates/opy-frontend/tests/fixtures/macros", - "test:opy-frontend-differential", + "test:opy-rs-macro-integration", + "test-fixture:crates/opy-rs/tests/fixtures/macros", + "test:opy-rs-differential", "fixtures:real-world/overpy-meipocalypse" ], - "notes": "Issue #6 + #7 part B. `#!define name(args) __script__(\"path.js\")` parsed (root-relative script resolution, script-not-found at the define site like the reference's ENOENT), expanded through opy_macro_js::MacroRuntime with the reference's var-injection ABI, indentation rule, and string-only completion contract — compile-time expansion executes and is frontend-supported. `#!postCompileHook` is parsed, validated, and recorded (duplicate rejection); the frontend never executes the hook, so no Workshop payload is fabricated. Structured script-* diagnostics carry script provenance. Catalog constants and post-compile-hook Workshop-output execution stay lowering-dependent (see hooks/post-compile-workshop, issue #8); `#!require` does not exist in the pinned reference." + "notes": "Issue #6 + #7 part B. `#!define name(args) __script__(\"path.js\")` parsed (root-relative script resolution, script-not-found at the define site like the reference's ENOENT), expanded through opy_macro_js::MacroRuntime with the reference's var-injection ABI, indentation rule, and string-only completion contract — compile-time expansion executes and is source-supported. `#!postCompileHook` is parsed, validated, and recorded (duplicate rejection); the source implementation never executes the hook, so no Workshop payload is fabricated. Structured script-* diagnostics carry script provenance. Catalog constants and post-compile-hook Workshop-output execution stay lowering-dependent (see hooks/post-compile-workshop, issue #8); `#!require` does not exist in the pinned reference." }, { "id": "hooks/post-compile-workshop", @@ -469,15 +469,15 @@ "upstream:src/tests/postCompileHook.opy", "upstream:src/quickjs.ts", "test:opy-macro-js-hooks", - "test:opy-frontend-macro-integration" + "test:opy-rs-macro-integration" ], - "notes": "Issue #6 + #8. The runtime's hook ABI (content injection, console capture, result/error semantics, 2000 ms budget) is implemented and tested on synthetic content in opy-macro-js; the frontend parses, validates, and records #!postCompileHook but never executes it. Real hook execution receives the final Workshop text from lowering and is inventory-only until the workshop-rs integration stage (#8); the frontend never fabricates a Workshop payload." + "notes": "Issue #6 + #8. The runtime's hook ABI (content injection, console capture, result/error semantics, 2000 ms budget) is implemented and tested on synthetic content in opy-macro-js; the source implementation parses, validates, and records #!postCompileHook but never executes it. Real hook execution receives the final Workshop text from lowering and is inventory-only until the workshop-rs integration stage (#8); the source implementation never fabricates a Workshop payload." }, { "id": "directives/rule-annotations", "name": "Rule annotations: @Event, @Condition, bare @Team/@Slot", "category": "directives", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/basic-rule", "fixtures:synthetic/declarations-rules", @@ -485,7 +485,7 @@ "upstream:src/tests/rules.opy", "upstream:README.md" ], - "notes": "Core rule annotations and event/condition structure are frontend-owned." + "notes": "Core rule annotations and event/condition structure are source implementation-owned." }, { "id": "directives/advanced-rule-annotations", @@ -503,7 +503,7 @@ "id": "directives/rule-model", "name": "Rule model: rule \"name\": with events (global, eachPlayer), conditions list, statements", "category": "directives", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic/basic-rule", "fixtures:synthetic/chase-condition-agentlab", @@ -526,7 +526,7 @@ "upstream:src/tests/translations2.fr.po", "upstream:src/compiler/translations.ts" ], - "notes": "Issue #29: language-tag syntax, uniqueness/conflict validation, provenance, and translation state are frontend-owned; locale availability, .po content, and generated Workshop helpers remain lowering-dependent." + "notes": "Issue #29: language-tag syntax, uniqueness/conflict validation, provenance, and translation state are source implementation-owned; locale availability, .po content, and generated Workshop helpers remain lowering-dependent." }, { "id": "translations/locale-emission", @@ -549,7 +549,7 @@ "upstream:src/tests/compression.opy", "upstream:src/data/opy/preprocessing.ts" ], - "notes": "Issue #29: optimizer/replacement controls are parsed, validated, ordered, and exposed as frontend state; transformation execution is not part of opy-rs." + "notes": "Issue #29: optimizer/replacement controls are parsed, validated, ordered, and exposed as source implementation state; transformation execution is not part of opy-rs." }, { "id": "optimization/backend-effects", @@ -577,7 +577,7 @@ "id": "runtime/js-hooks", "name": "QuickJS runtime for __script__ macro and hook execution (synthetic-content hook ABI)", "category": "runtime", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "upstream:runTests.mjs", "upstream:src/quickjs.ts", @@ -585,20 +585,20 @@ "test:opy-macro-js-abi", "test:opy-macro-js-hooks", "test:opy-macro-js-limits", - "test:opy-frontend-macro-integration" + "test:opy-rs-macro-integration" ], - "notes": "Issue #6 + #7 part B. Bounded runtime surface wired into the frontend: macro execution with argument injection, `vect` helper, empty constant objects, console capture, reference-compatible accept/reject and result semantics, resource limits defaulting to the pinned reference constants (1000 ms macro / 2000 ms hook budgets, 64 MiB memory, 512 KiB stack). The hook ABI runs against synthetic content in opy-macro-js; hook execution against the Workshop output is lowering-dependent (see hooks/post-compile-workshop, issue #8). Browser/WASM execution remains unsupported (native embedding only), matching the declared boundary." + "notes": "Issue #6 + #7 part B. Bounded runtime surface wired into the source implementation: macro execution with argument injection, `vect` helper, empty constant objects, console capture, reference-compatible accept/reject and result semantics, resource limits defaulting to the pinned reference constants (1000 ms macro / 2000 ms hook budgets, 64 MiB memory, 512 KiB stack). The hook ABI runs against synthetic content in opy-macro-js; hook execution against the Workshop output is lowering-dependent (see hooks/post-compile-workshop, issue #8). Browser/WASM execution remains unsupported (native embedding only), matching the declared boundary." }, { - "id": "compilation/frontend-pipeline", - "name": "Frontend pipeline: lexer -> preprocess -> CST/parser -> semantic resolution -> Opy HIR v2", + "id": "compilation/source implementation-pipeline", + "name": "Source implementation pipeline: lexer -> preprocess -> CST/parser -> semantic resolution -> Opy HIR v2", "category": "compilation", - "state": "frontend-supported", + "state": "source-supported", "evidence": [ "fixtures:synthetic", "fixtures:real-world", "docs:docs/hir/opy-hir-v2.md", - "test:opy-frontend-differential" + "test:opy-rs-differential" ], "notes": "Issues #3-#7, #25, and the #28/#29/#30/#33 readiness tracks. Fully Workshop-independent; the pinned differential corpus is the acceptance corpus. Differential harness runs every fixture through the native pipeline in cargo test with structural self-checks (HIR validation, wire round-trip, deterministic dump), status/rule-name parity against recorded oracle.json snapshots, explicit native evidence expectations, and a machine-readable report (target/opy-differential-report.json). The generated report records per-fixture matches and known gaps; no unexpected divergence or inconclusive result is a support claim." }, @@ -650,7 +650,7 @@ "test:opy-compiler-implicit-default-variables", "contract:workshop-rs-v0.1.11" ], - "notes": "Issue #46. Undeclared A-Z, AA-AZ, ..., DA-DX global references and eventPlayer. player references resolve through the frontend default_var_index contract and lower to separate Workshop variable namespaces at their fixed reference slots (A=0 ... DX=127); used implicit slots are reserved independently for declared global/player allocation, an explicit declared index colliding with a used implicit slot fails with the stable source-attributed index-collision diagnostic, and a declared name wins in its own namespace. The pinned oracle snapshot for the issue-46 fixture constrains global and player slots through structural equivalence." + "notes": "Issue #46. Undeclared A-Z, AA-AZ, ..., DA-DX global references and eventPlayer. player references resolve through the source implementation default_var_index contract and lower to separate Workshop variable namespaces at their fixed reference slots (A=0 ... DX=127); used implicit slots are reserved independently for declared global/player allocation, an explicit declared index colliding with a used implicit slot fails with the stable source-attributed index-collision diagnostic, and a declared name wins in its own namespace. The pinned oracle snapshot for the issue-46 fixture constrains global and player slots through structural equivalence." }, { "id": "compilation/opy-primitive-lowering-limits", @@ -742,7 +742,7 @@ "summary": { "byState": { "planned": 0, - "frontend-supported": 23, + "source-supported": 23, "semantic-supported": 13, "lowering-dependent": 13, "end-to-end-supported": 7 diff --git a/compatibility/tests/test_support_matrix.py b/compatibility/tests/test_support_matrix.py index 715d74e..82583cb 100644 --- a/compatibility/tests/test_support_matrix.py +++ b/compatibility/tests/test_support_matrix.py @@ -25,7 +25,7 @@ STATES = { "planned", - "frontend-supported", + "source-supported", "semantic-supported", "lowering-dependent", "end-to-end-supported", diff --git a/crates/opy-cli/Cargo.toml b/crates/opy-cli/Cargo.toml index 2838496..01661b6 100644 --- a/crates/opy-cli/Cargo.toml +++ b/crates/opy-cli/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Standalone OPY frontend CLI: check, inspect, support-matrix, version (Workshop-independent, no Node or Workshop backend required)." +description = "Standalone OPY CLI: check, inspect, support-matrix, version (Workshop-independent, no Node or Workshop backend required)." [lints] workspace = true @@ -12,6 +12,6 @@ workspace = true [dependencies] clap = { version = "4.5", features = ["derive"] } clap_complete = "4.5" -opy-frontend.workspace = true +opy-rs.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/crates/opy-cli/src/cli.rs b/crates/opy-cli/src/cli.rs index 13028af..0ce4a11 100644 --- a/crates/opy-cli/src/cli.rs +++ b/crates/opy-cli/src/cli.rs @@ -11,7 +11,7 @@ use clap::{Args, Parser, Subcommand, ValueEnum}; disable_version_flag = true, disable_help_subcommand = true, subcommand_precedence_over_arg = true, - about = "Workshop-independent OPY frontend tooling", + about = "Workshop-independent OPY tooling", after_help = EXIT_CODES )] pub(crate) struct Cli { @@ -48,7 +48,7 @@ pub(crate) enum Command { Completion(CompletionArgs), /// Show the top-level help. Help, - /// Print crate and frontend protocol identities. + /// Print crate and language protocol identities. Version, } diff --git a/crates/opy-cli/src/main.rs b/crates/opy-cli/src/main.rs index 8508346..1b24ef6 100644 --- a/crates/opy-cli/src/main.rs +++ b/crates/opy-cli/src/main.rs @@ -1,6 +1,6 @@ -//! `opy-cli` — the standalone Workshop-independent OPY frontend CLI. +//! `opy-cli` — the standalone Workshop-independent OPY CLI. //! -//! The CLI owns command parsing and presentation. `opy-frontend` owns OPY +//! The CLI owns command parsing and presentation. `opy-rs` owns OPY //! parsing, semantic resolution, and structured diagnostics. //! //! Exit codes remain: 0 clean/success, 1 source diagnostics, and 2 usage or @@ -15,9 +15,9 @@ use std::process::ExitCode; use clap::{CommandFactory, Parser, error::ErrorKind}; use clap_complete::{generate, shells}; -use opy_frontend::support::{self, SupportMatrixError}; -use opy_frontend::tooling::{CheckOutcome, Diagnostic as FrontendDiagnostic, check}; -use opy_frontend::{FRONTEND_NAME, FRONTEND_VERSION}; +use opy_rs::support::{self, SupportMatrixError}; +use opy_rs::tooling::{CheckOutcome, Diagnostic as OpyDiagnostic, check}; +use opy_rs::{LANGUAGE_NAME, LANGUAGE_VERSION}; use serde::Serialize; use crate::cli::{CheckArgs, Cli, Command, FileArgs, OutputFormatArg, SupportArgs}; @@ -180,11 +180,11 @@ fn cmd_completion(shell: cli::ShellArg) -> ExitCode { fn cmd_version() -> ExitCode { println!("opy-cli {}", env!("CARGO_PKG_VERSION")); - println!("frontend: {FRONTEND_NAME} {FRONTEND_VERSION}"); + println!("language: {LANGUAGE_NAME} {LANGUAGE_VERSION}"); println!( "protocol: {} v{}", - opy_frontend::hir::types::PROTOCOL_NAME, - opy_frontend::hir::types::PROTOCOL_MAJOR + opy_rs::hir::types::PROTOCOL_NAME, + opy_rs::hir::types::PROTOCOL_MAJOR ); ExitCode::SUCCESS } @@ -192,7 +192,7 @@ fn cmd_version() -> ExitCode { #[derive(Serialize)] struct CheckReport<'a> { ok: bool, - diagnostics: &'a [FrontendDiagnostic], + diagnostics: &'a [OpyDiagnostic], } fn check_view(outcome: &CheckOutcome) -> CheckView { @@ -215,7 +215,7 @@ fn check_view(outcome: &CheckOutcome) -> CheckView { } } -fn diagnostic_view(diagnostic: &FrontendDiagnostic) -> DiagnosticView { +fn diagnostic_view(diagnostic: &OpyDiagnostic) -> DiagnosticView { DiagnosticView { severity: DiagnosticSeverity::Error, code: diagnostic.code.clone(), diff --git a/crates/opy-cli/src/present.rs b/crates/opy-cli/src/present.rs index 41b697e..0b74723 100644 --- a/crates/opy-cli/src/present.rs +++ b/crates/opy-cli/src/present.rs @@ -1,6 +1,6 @@ //! CLI-local presentation policy for human and GitHub Actions output. //! -//! `opy-frontend` owns structured diagnostics. This module owns only their +//! `opy-rs` owns structured diagnostics. This module owns only their //! terminal, plain, and GitHub Actions presentation. Machine-readable output //! is rendered by the command handlers before this boundary is entered. diff --git a/crates/opy-cli/tests/cli.rs b/crates/opy-cli/tests/cli.rs index 2402b56..775c43f 100644 --- a/crates/opy-cli/tests/cli.rs +++ b/crates/opy-cli/tests/cli.rs @@ -13,7 +13,7 @@ fn bin() -> Command { /// The WrightKit-authored multi-file fixture shared with the frontend tests. const MULTI_MAIN: &str = concat!( env!("CARGO_MANIFEST_DIR"), - "/../opy-frontend/tests/fixtures/multi-file/main.opy" + "/../opy-rs/tests/fixtures/multi-file/main.opy" ); fn run(args: &[&str]) -> std::process::Output { @@ -174,10 +174,7 @@ fn version_prints_crate_and_protocol_identity() { stdout.contains("wright/opy-hir"), "protocol identity: {stdout}" ); - assert!( - stdout.contains("wright/opy-native"), - "frontend identity: {stdout}" - ); + assert!(stdout.contains("opy-rs"), "language identity: {stdout}"); } #[test] diff --git a/crates/opy-compiler/Cargo.toml b/crates/opy-compiler/Cargo.toml index d4f054e..038d674 100644 --- a/crates/opy-compiler/Cargo.toml +++ b/crates/opy-compiler/Cargo.toml @@ -10,7 +10,7 @@ description = "OPy HIR to canonical Workshop WIR integration compiler" workspace = true [dependencies] -opy-frontend.workspace = true +opy-rs.workspace = true workshop-rs.workspace = true [dev-dependencies] diff --git a/crates/opy-compiler/src/lib.rs b/crates/opy-compiler/src/lib.rs index 65e7252..079f02d 100644 --- a/crates/opy-compiler/src/lib.rs +++ b/crates/opy-compiler/src/lib.rs @@ -1,6 +1,6 @@ //! The first OPY-to-Workshop integration boundary. //! -//! `opy-frontend` remains a standalone OPY/HIR producer. This crate is the +//! `opy-rs` remains a standalone OPY/HIR producer. This crate is the //! consumer-owned compiler layer: it pins the released `workshop-rs` v0.1.11 //! contract, checks the OPY manifest links against the canonical catalog, and //! lowers the supported OPY program structure into canonical WIR before @@ -8,14 +8,14 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use opy_frontend::hir::{ - self, Expr, RuleEntry, Span as HirSpan, Stmt, SwitchArm, default_var_index, -}; -use opy_frontend::manifest::{FunctionKind, Manifest}; +use opy_rs::hir::{self, Expr, RuleEntry, Span as HirSpan, Stmt, SwitchArm, default_var_index}; +use opy_rs::manifest::{FunctionKind, Manifest}; use workshop_rs::catalog::{Catalog, CatalogIdentity, Kind, Locale}; use workshop_rs::source::{Position as WorkshopPosition, SourceFile, Span as WorkshopSpan}; use workshop_rs::wir::{self, Action, Event, PlayerEventKind, Program, Value, ValueNode}; +pub mod reconstruct; + /// The exact released dependency contract consumed by this crate. pub const WORKSHOP_RS_VERSION: &str = "0.1.11"; @@ -475,7 +475,7 @@ impl<'a> Lowering<'a> { } } hir::Declaration::Macro { .. } => { - // Macro definitions are expanded by the frontend; ignored during WIR lowering. + // Macro definitions are expanded by the source implementation; ignored during WIR lowering. } } } @@ -2423,7 +2423,7 @@ fn workshop_error_span(error: &workshop_rs::WorkshopError) -> Option, +} + +/// All reconstruction failures for one program, in deterministic arena +/// order. The emitter never returns partial output: a non-empty issue list +/// means no OPY was produced. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReconstructError { + pub issues: Vec, +} + +impl fmt::Display for ReconstructError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, issue) in self.issues.iter().enumerate() { + if index > 0 { + writeln!(f)?; + } + let location = match issue.span { + Some(span) => format!(" at {}:{}", span.start.line, span.start.col), + None => String::new(), + }; + write!(f, "{}: {}{location}", issue.code, issue.message)?; + } + Ok(()) + } +} + +impl std::error::Error for ReconstructError {} + +/// Reconstruct a validated WIR program into deterministic OPY source. +/// +/// Resolves builtin identities through the built-in OPY semantic manifest +/// and the built-in Workshop catalog (`en-US`), the declared surface for +/// reconstruction (issue #124). Returns an error carrying every +/// non-representable construct diagnostic when the program cannot be +/// reconstructed. +pub fn reconstruct(program: &wir::Program) -> Result { + let manifest = match Manifest::builtin() { + Ok(manifest) => manifest, + Err(error) => { + return Err(ReconstructError { + issues: vec![ReconstructIssue { + code: "manifest-error", + message: format!( + "cannot load the OPY semantic compatibility manifest: {error}" + ), + span: None, + }], + }); + } + }; + let catalog = match Catalog::builtin() { + Ok(catalog) => catalog, + Err(error) => { + return Err(ReconstructError { + issues: vec![ReconstructIssue { + code: "catalog-error", + message: format!("cannot load the Workshop catalog: {error}"), + span: None, + }], + }); + } + }; + reconstruct_with(program, manifest, &catalog, &Locale::new("en-US")) +} + +/// The context-sensitive form of [`reconstruct`]: resolves identities through +/// the supplied manifest and catalog. The locale selects the catalog +/// spellings used for cross-checks (reconstruction emits OPY, which is +/// locale-independent; `en-US` is the catalog's declared surface). +pub fn reconstruct_with( + program: &wir::Program, + manifest: &Manifest, + catalog: &Catalog, + locale: &Locale, +) -> Result { + let mut emitter = Emitter::new(program, manifest, catalog, locale); + emitter.run(); + if emitter.issues.is_empty() { + Ok(emitter.out) + } else { + Err(ReconstructError { + issues: emitter.issues, + }) + } +} + +/// OPY names the parser treats as keywords or literals; a WIR table name that +/// collides with one of these can never be referenced or declared faithfully. +const RESERVED_NAMES: &[&str] = &[ + "true", + "false", + "None", + "null", + "eventPlayer", + "rule", + "def", + "globalvar", + "playervar", + "subroutine", + "enum", + "macro", + "if", + "for", + "while", + "pass", + "elif", + "else", + "in", + "and", + "or", + "not", +]; + +/// Whether `name` is a valid OPY identifier (the lexer's identifier rule). +fn is_opy_identifier(name: &str) -> bool { + let mut chars = name.chars(); + let Some(first) = chars.next() else { + return false; + }; + (first.is_ascii_alphabetic() || first == '_') + && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +/// Binary operator spellings the OPY frontend lowers to `Value::Call`s with +/// the same name (source operators, not Workshop spellings like `add`). +const BINARY_OPS: &[&str] = &[ + "+", "-", "*", "/", "%", "**", "==", "!=", "<", "<=", ">", ">=", "and", "or", +]; + +/// Call names the frontend lowers to dedicated WIR nodes (never `Call`s). +const DEDICATED_ACTION_NAMES: &[&str] = &["debug", "print", "append"]; +const DEDICATED_VALUE_NAMES: &[&str] = &["vect", "range", "chase"]; + +struct Emitter<'a> { + program: &'a wir::Program, + manifest: &'a Manifest, + catalog: &'a Catalog, + locale: &'a Locale, + issues: Vec, + out: String, + /// Subroutine names, for call-vs-subroutine ambiguity checks. + subroutine_names: std::collections::HashSet, +} + +/// The canonical rule layout the frontend's re-lowering reproduces. +struct RuleLayout<'a> { + /// The leading "Initialize global variables" rule, converted to + /// declaration initializers. + global_init: Option>, + /// The leading "Initialize player variables" rule, converted to + /// declaration initializers. + player_init: Option>, + /// Subroutine-body rules (defs), in subroutine table order. + sub_rules: Vec<&'a wir::Rule>, + /// Everything else, in input order. + normal_rules: Vec<&'a wir::Rule>, +} + +impl<'a> Emitter<'a> { + fn new( + program: &'a wir::Program, + manifest: &'a Manifest, + catalog: &'a Catalog, + locale: &'a Locale, + ) -> Self { + let subroutine_names = program + .subroutines + .iter() + .map(|subroutine| subroutine.name.clone()) + .collect(); + Emitter { + program, + manifest, + catalog, + locale, + issues: Vec::new(), + out: String::new(), + subroutine_names, + } + } + + fn run(&mut self) { + self.validate_tables(); + if self.issues.is_empty() { + let layout = self.classify_rules(); + if self.issues.is_empty() { + self.emit_program(&layout); + } + } + } + // ---- diagnostics ---- + + fn issue(&mut self, code: &'static str, message: impl Into, span: Option) { + self.issues.push(ReconstructIssue { + code, + message: message.into(), + span, + }); + } + + // ---- table validation ---- + + fn validate_tables(&mut self) { + if self.program.settings.is_some() { + self.issue( + "unsupported-settings", + "custom-game-settings are outside the reconstruction surface", + None, + ); + } + // Global table: unique names, valid OPY identifiers, non-decreasing + // slot order (the frontend's re-lowering sorts the table by index, + // so only slot-ordered input reproduces the same table). + let mut previous_index: Option = None; + for (position, variable) in self.program.global_variables.iter().enumerate() { + self.check_variable_name(variable.name.as_str(), variable.span, "global variable"); + self.check_duplicate_name( + variable.name.as_str(), + position, + "global variable", + variable.span, + ); + if let Some(previous) = previous_index { + if variable.index < previous { + self.issue( + "unsupported-global-order", + format!( + "global variables must be in ascending index order \ + (slot {} precedes slot {})", + previous, variable.index + ), + variable.span, + ); + } + } + previous_index = Some(variable.index); + } + // Player table: unique names and valid identifiers; player slots are + // explicit in the `playervar name ` form, so no order rule. + for (position, variable) in self.program.player_variables.iter().enumerate() { + self.check_variable_name(variable.name.as_str(), variable.span, "player variable"); + self.check_duplicate_name( + variable.name.as_str(), + position, + "player variable", + variable.span, + ); + } + // Subroutine table: unique names, valid identifiers, and indices + // exactly equal to table position (the OPY `subroutine name` + // declaration cannot carry an index; the re-lowered index is the + // table position). + for (position, subroutine) in self.program.subroutines.iter().enumerate() { + self.check_variable_name(subroutine.name.as_str(), subroutine.span, "subroutine"); + self.check_duplicate_name( + subroutine.name.as_str(), + position, + "subroutine", + subroutine.span, + ); + if subroutine.index as usize != position { + self.issue( + "unsupported-subroutine-index", + format!( + "subroutine '{}' has index {} but the OPY surface requires \ + table position {} (subroutine declarations cannot carry an index)", + subroutine.name, subroutine.index, position + ), + subroutine.span, + ); + } + } + } + + fn check_variable_name(&mut self, name: &str, span: Option, kind: &str) { + if !is_opy_identifier(name) { + self.issue( + "unsupported-name", + format!( + "{kind} name '{name}' is not a valid OPY identifier on the \ + reconstruction surface" + ), + span, + ); + } else if RESERVED_NAMES.contains(&name) { + self.issue( + "unsupported-name", + format!( + "{kind} name '{name}' collides with an OPY keyword or literal \ + and cannot be referenced on the reconstruction surface" + ), + span, + ); + } + } + + /// Whether a name repeats an earlier entry of its table (duplicates + /// cannot be declared or referenced faithfully on the OPY surface). + fn check_duplicate_name( + &mut self, + name: &str, + position: usize, + kind: &str, + span: Option, + ) { + let duplicate = match kind { + "global variable" => self + .program + .global_variables + .iter() + .enumerate() + .take(position) + .any(|(_, other)| other.name == name), + "player variable" => self + .program + .player_variables + .iter() + .enumerate() + .take(position) + .any(|(_, other)| other.name == name), + _ => self + .program + .subroutines + .iter() + .enumerate() + .take(position) + .any(|(_, other)| other.name == name), + }; + if duplicate { + self.issue( + "unsupported-duplicate-name", + format!("duplicate {kind} name '{name}'"), + span, + ); + } + } + + /// The canonical rule layout: optional leading initializer rules, then + /// all subroutine-body rules in subroutine table order, then the normal + /// rules. Any other arrangement cannot be reproduced by the frontend's + /// deterministic re-lowering and is rejected. + fn classify_rules(&mut self) -> RuleLayout<'a> { + let rules: Vec<&wir::Rule> = self.program.rules.iter().collect(); + let mut index = 0; + let mut global_init = None; + let mut player_init = None; + if let Some(rule) = rules.first() { + if rule.name == "Initialize global variables" { + global_init = self.canonical_init(rule, true); + index = 1; + } else if rule.name == "Initialize player variables" { + player_init = self.canonical_init(rule, false); + index = 1; + } + } + if index == 1 { + if let Some(rule) = rules.get(1) { + if rule.name == "Initialize player variables" && global_init.is_some() { + player_init = self.canonical_init(rule, false); + index = 2; + } + } + } + + let mut sub_rules = Vec::new(); + let mut normal_rules = Vec::new(); + let mut in_sub_rules = true; + for rule in rules.iter().copied().skip(index) { + match &rule.event { + Event::Subroutine(_) => { + if !in_sub_rules { + self.issue( + "unsupported-rule-order", + format!( + "subroutine-body rule '{}' appears after a normal rule; \ + the frontend re-lowering emits subroutine rules first", + rule.name + ), + rule.span, + ); + } + if !rule.conditions.is_empty() { + self.issue( + "unsupported-rule-order", + format!( + "subroutine-body rule '{}' carries conditions; `def` \ + bodies cannot express them", + rule.name + ), + rule.span, + ); + } + sub_rules.push(rule); + } + _ => { + in_sub_rules = false; + normal_rules.push(rule); + } + } + } + + // Subroutine rules must be in subroutine table order, and each rule + // must carry the exact name the re-lowering synthesizes for its def. + let mut expected = 0usize; + for rule in &sub_rules { + let Event::Subroutine(subroutine) = &rule.event else { + continue; + }; + if subroutine.index() != expected { + self.issue( + "unsupported-rule-order", + format!( + "subroutine-body rules must appear in subroutine table order; \ + '{}' is out of order", + rule.name + ), + rule.span, + ); + } + expected += 1; + if let Some(definition) = self.program.subroutines.get(*subroutine) { + let expected_name = format!("Subroutine {}", definition.name); + if rule.name != expected_name { + self.issue( + "unsupported-rule-order", + format!( + "subroutine-body rule name '{}' does not match the def \ + form '{}' the frontend synthesizes", + rule.name, expected_name + ), + rule.span, + ); + } + } + } + + RuleLayout { + global_init, + player_init, + sub_rules, + normal_rules, + } + } + + /// Validate a leading initializer rule: exactly the synthesized shape + /// (name, event, empty conditions, all-Set actions). Returns the action + /// ids to convert into declaration initializers, or records an issue. + fn canonical_init(&mut self, rule: &wir::Rule, global: bool) -> Option> { + let expected_name = if global { + "Initialize global variables" + } else { + "Initialize player variables" + }; + if !rule.conditions.is_empty() { + self.issue( + "unsupported-init-rule", + format!( + "initializer rule '{expected_name}' carries conditions; the \ + frontend synthesizes it from declarations with none" + ), + rule.span, + ); + return None; + } + let mut actions = Vec::with_capacity(rule.actions.len()); + for action in &rule.actions { + let Some(node) = self.program.actions.get(*action) else { + self.issue("unsupported-dangling", "dangling action id", rule.span); + return None; + }; + let set = matches!( + (global, node), + (true, Action::SetGlobalVariable { .. }) + | (false, Action::SetPlayerVariable { .. }) + ); + if !set { + self.issue( + "unsupported-init-rule", + format!( + "initializer rule '{expected_name}' mixes non-Set actions; \ + the frontend's synthesized initializer rule is all-Set" + ), + node.span(), + ); + return None; + } + actions.push(*action); + } + Some(actions) + } + + // ---- emission ---- + + fn emit_program(&mut self, layout: &RuleLayout) { + let global_initializers = self.collect_global_initializers(&layout.global_init); + let player_initializers = self.collect_player_initializers(&layout.player_init); + self.check_initializer_slot(&global_initializers); + + // Declarations. + for (position, variable) in self.program.global_variables.iter().enumerate() { + self.out.push_str("globalvar "); + self.out.push_str(&variable.name); + match global_initializers.get(&position) { + Some(value) => { + self.out.push_str(" = "); + self.emit_initializer(*value); + } + None => { + self.out.push(' '); + self.out.push_str(&variable.index.to_string()); + } + } + self.out.push('\n'); + } + for (position, variable) in self.program.player_variables.iter().enumerate() { + self.out.push_str("playervar "); + self.out.push_str(&variable.name); + match player_initializers.get(&position) { + Some(value) => { + self.out.push_str(" = "); + self.emit_initializer(*value); + } + None => { + self.out.push(' '); + self.out.push_str(&variable.index.to_string()); + } + } + self.out.push('\n'); + } + if self.program.subroutines.is_empty() { + self.out.push('\n'); + } else { + for subroutine in self.program.subroutines.iter() { + self.out.push_str("subroutine "); + self.out.push_str(&subroutine.name); + self.out.push('\n'); + } + self.out.push('\n'); + } + + // Subroutine bodies. + for rule in &layout.sub_rules { + let Event::Subroutine(subroutine) = &rule.event else { + continue; + }; + let Some(definition) = self.program.subroutines.get(*subroutine) else { + continue; + }; + self.out.push_str("def "); + self.out.push_str(&definition.name); + self.out.push_str("():\n"); + self.emit_actions(&rule.actions, 1); + self.out.push('\n'); + } + + // Rules. + for rule in &layout.normal_rules { + if rule.disabled { + self.issue( + "unsupported-disabled-rule", + format!( + "rule '{}' is disabled; the OPY surface cannot express it", + rule.name + ), + rule.span, + ); + continue; + } + if rule.actions.is_empty() { + continue; + } + self.out.push_str("rule \""); + self.out.push_str(&rule.name); + self.out.push_str("\":\n"); + match &rule.event { + Event::Global => self.out.push_str(" @Event global\n"), + Event::EachPlayer => self.out.push_str(" @Event eachPlayer\n"), + Event::EachPlayerWithFilters { + team: workshop_rs::wir::EventTeam::All, + target: workshop_rs::wir::EventTarget::All, + } => self.out.push_str(" @Event eachPlayer\n"), + Event::EachPlayerWithFilters { .. } | Event::Player { .. } => { + self.issue( + "unsupported-rule-event", + format!("rule '{}' uses an event outside the OPY surface", rule.name), + rule.span, + ); + continue; + } + Event::Subroutine(_) => { + self.issue( + "unsupported-rule-order", + format!( + "rule '{}' has a subroutine event outside the def layout", + rule.name + ), + rule.span, + ); + continue; + } + } + for condition in &rule.conditions { + self.out.push_str(" @Condition "); + self.emit_value(*condition); + self.out.push('\n'); + } + self.emit_actions(&rule.actions, 1); + self.out.push('\n'); + } + } + + /// Map initializer rule actions onto declaration positions (table order), + /// validating the rule's Sets are in table order like the frontend's + /// synthesized initializer rule. + fn collect_global_initializers( + &mut self, + actions: &Option>, + ) -> std::collections::HashMap { + let mut initializers = std::collections::HashMap::new(); + let Some(actions) = actions else { + return initializers; + }; + let mut previous: Option = None; + for action in actions { + let span = self + .program + .actions + .get(*action) + .and_then(|node| node.span()); + let Some(Action::SetGlobalVariable { + variable, value, .. + }) = self.program.actions.get(*action) + else { + continue; + }; + let variable_position = variable.index(); + let name = self + .program + .global_variables + .get(*variable) + .map(|variable| variable.name.clone()) + .unwrap_or_default(); + if let Some(previous_position) = previous { + if variable_position <= previous_position { + self.issue( + "unsupported-init-rule", + format!( + "initializer rule Sets '{name}' out of global table order; \ + the frontend synthesizes initializers in declaration order" + ), + span, + ); + } + } + previous = Some(variable_position); + initializers.insert(variable_position, *value); + } + initializers + } + + fn collect_player_initializers( + &mut self, + actions: &Option>, + ) -> std::collections::HashMap { + let mut initializers = std::collections::HashMap::new(); + let Some(actions) = actions else { + return initializers; + }; + let mut previous: Option = None; + for action in actions { + let span = self + .program + .actions + .get(*action) + .and_then(|node| node.span()); + let Some(Action::SetPlayerVariable { + player, + variable, + value, + .. + }) = self.program.actions.get(*action) + else { + continue; + }; + if !self.is_event_player(*player) { + self.issue( + "unsupported-init-rule", + "player initializer targets a non-event-player expression", + span, + ); + } + let variable_position = variable.index(); + let name = self + .program + .player_variables + .get(*variable) + .map(|variable| variable.name.clone()) + .unwrap_or_default(); + if let Some(previous_position) = previous { + if variable_position <= previous_position { + self.issue( + "unsupported-init-rule", + format!( + "initializer rule Sets '{name}' out of player table order; \ + the frontend synthesizes initializers in declaration order" + ), + span, + ); + } + } + previous = Some(variable_position); + initializers.insert(variable_position, *value); + } + initializers + } + + /// A declaration initializer: same value emission, but zero literals are + /// spelled `0.0` because the frontend drops integer-`0` initializers + /// (matching the reference adapter). + fn emit_initializer(&mut self, value: wir::ValueId) { + let Some(node) = self.program.values.get(value) else { + self.issue("unsupported-dangling", "dangling value id", None); + return; + }; + if let Value::Number { value: number, .. } = &node.value { + if *number == 0.0 { + self.out.push_str("0.0"); + return; + } + } + self.emit_value(value); + } + + /// The OPY declaration `globalvar name = value` cannot carry an explicit + /// slot, so the frontend re-lowering assigns the lowest free slot. An + /// initializer-bearing global is only representable when that slot equals + /// its WIR index; otherwise the reconstructed table would differ. + fn check_initializer_slot( + &mut self, + initializers: &std::collections::HashMap, + ) { + let mut taken: std::collections::HashSet = std::collections::HashSet::new(); + for (position, variable) in self.program.global_variables.iter().enumerate() { + if initializers.contains_key(&position) { + let mut next_free = 0u32; + while taken.contains(&next_free) { + next_free += 1; + } + if next_free != variable.index { + self.issues.push(ReconstructIssue { + code: "unsupported-indexed-initializer", + message: format!( + "initializer-bearing global '{}' occupies slot {} but the \ + OPY `globalvar name = value` form assigns the lowest free \ + slot ({}) on re-lowering", + variable.name, variable.index, next_free + ), + span: variable.span, + }); + } + taken.insert(next_free); + } else { + taken.insert(variable.index); + } + } + } + + fn emit_actions(&mut self, actions: &[wir::ActionId], level: usize) { + for action in actions { + self.emit_action(*action, level); + } + } + + fn indent(level: usize) -> String { + " ".repeat(level) + } + + fn emit_action(&mut self, id: wir::ActionId, level: usize) { + let Some(node) = self.program.actions.get(id) else { + self.issue("unsupported-dangling", "dangling action id", None); + return; + }; + let span = node.span(); + let indent = Self::indent(level); + match node { + Action::SetGlobalVariable { + variable, value, .. + } => { + let variable_id = *variable; + let Some(variable) = self.program.global_variables.get(variable_id) else { + self.issue("unsupported-dangling", "dangling global variable id", span); + return; + }; + if self.set_has_modify_pattern(*value, variable_id.index(), true) { + self.issue( + "unsupported-set-binary", + format!( + "Set Global Variable('{}', ) \ + re-lowers to a Modify action; emit the modify form", + variable.name + ), + span, + ); + return; + } + self.out.push_str(&indent); + self.out.push_str(&variable.name); + self.out.push_str(" = "); + self.emit_value(*value); + self.out.push('\n'); + } + Action::ModifyGlobalVariable { + variable, + op, + value, + .. + } => { + let Some(variable) = self.program.global_variables.get(*variable) else { + self.issue("unsupported-dangling", "dangling global variable id", span); + return; + }; + self.emit_modify(level, &variable.name, *op, *value, span); + } + Action::SetPlayerVariable { + player, + variable, + value, + .. + } => { + let variable_id = *variable; + let Some(variable) = self.program.player_variables.get(variable_id) else { + self.issue("unsupported-dangling", "dangling player variable id", span); + return; + }; + if !self.is_event_player(*player) { + self.issue( + "unsupported-arbitrary-player-target", + "Set Player Variable targets a non-event-player expression; \ + the OPY surface only exposes eventPlayer.member" + .to_string(), + span, + ); + return; + } + if self.set_has_modify_pattern(*value, variable_id.index(), false) { + self.issue( + "unsupported-set-binary", + format!( + "Set Player Variable('{}', ) \ + re-lowers to a Modify action; emit the modify form", + variable.name + ), + span, + ); + return; + } + self.out.push_str(&indent); + self.out.push_str("eventPlayer."); + self.out.push_str(&variable.name); + self.out.push_str(" = "); + self.emit_value(*value); + self.out.push('\n'); + } + Action::ModifyPlayerVariable { + player, + variable, + op, + value, + .. + } => { + let Some(variable) = self.program.player_variables.get(*variable) else { + self.issue("unsupported-dangling", "dangling player variable id", span); + return; + }; + if !self.is_event_player(*player) { + self.issue( + "unsupported-arbitrary-player-target", + "Modify Player Variable targets a non-event-player expression; \ + the OPY surface only exposes eventPlayer.member" + .to_string(), + span, + ); + return; + } + self.emit_modify( + level, + &format!("eventPlayer.{}", variable.name), + *op, + *value, + span, + ); + } + Action::AssignMember { span, .. } => { + self.issue( + "unsupported-member-assignment", + "dynamic member assignments are outside the OPY reconstruction surface", + *span, + ); + } + Action::CallSubroutine { + subroutine, span, .. + } => { + let Some(subroutine) = self.program.subroutines.get(*subroutine) else { + self.issue("unsupported-dangling", "dangling subroutine id", *span); + return; + }; + self.out.push_str(&indent); + self.out.push_str(&subroutine.name); + self.out.push_str("()\n"); + } + Action::If { + branches, + else_body, + span, + } => { + for (index, branch) in branches.iter().enumerate() { + let keyword = if index == 0 { "if" } else { "elif" }; + self.out.push_str(&indent); + self.out.push_str(keyword); + self.out.push(' '); + self.emit_value(branch.condition); + self.out.push_str(":\n"); + self.emit_actions(&branch.body, level + 1); + } + if let Some(else_body) = else_body { + self.out.push_str(&indent); + self.out.push_str("else:\n"); + self.emit_actions(else_body, level + 1); + } + let _ = span; + } + Action::While { + condition, + body, + span, + } => { + self.out.push_str(&indent); + self.out.push_str("while "); + self.emit_value(*condition); + self.out.push_str(":\n"); + self.emit_actions(body, level + 1); + let _ = span; + } + Action::ForGlobalVariable { + variable, + start, + stop, + step, + body, + span, + .. + } => { + let Some(variable) = self.program.global_variables.get(*variable) else { + self.issue("unsupported-dangling", "dangling loop variable id", *span); + return; + }; + self.out.push_str(&indent); + self.out.push_str("for "); + self.out.push_str(&variable.name); + self.out.push_str(" in range("); + self.emit_value(*start); + self.out.push_str(", "); + self.emit_value(*stop); + self.out.push_str(", "); + self.emit_value(*step); + self.out.push_str("):\n"); + self.emit_actions(body, level + 1); + } + Action::ForPlayerVariable { span, .. } => { + self.issue( + "unsupported-per-player-loop", + "For Player Variable is outside the reconstruction surface \ + (the OPY `for` form binds a global variable)", + *span, + ); + } + Action::Debug { value, span } => { + self.out.push_str(&indent); + self.out.push_str("debug("); + self.emit_value(*value); + self.out.push_str(")\n"); + let _ = span; + } + Action::Print { message, span } => { + self.out.push_str(&indent); + self.out.push_str("print("); + self.emit_value(*message); + self.out.push_str(")\n"); + let _ = span; + } + Action::Call { name, args, span } => { + self.emit_call_action(name, args, &indent, *span); + } + } + } + + /// `x = x v` (or the player form) re-lowers to a Modify action, so a + /// Set whose value matches the pattern cannot be reconstructed as a Set. + fn set_has_modify_pattern( + &self, + value: wir::ValueId, + variable_index: usize, + global: bool, + ) -> bool { + let Some(node) = self.program.values.get(value) else { + return false; + }; + let Value::Call { name, args } = &node.value else { + return false; + }; + if !matches!(name.as_str(), "+" | "-" | "*" | "/" | "%" | "**") { + return false; + } + if args.len() != 2 { + return false; + } + args.iter().any(|operand| { + let Some(node) = self.program.values.get(*operand) else { + return false; + }; + if global { + matches!(node.value, Value::GlobalVariable(id) if id.index() == variable_index) + } else { + matches!( + node.value, + Value::PlayerVariable { variable: id, .. } if id.index() == variable_index + ) + } + }) + } + + /// Whether a value node is the event-player pseudo-symbol. + fn is_event_player(&self, value: wir::ValueId) -> bool { + matches!( + self.program.values.get(value).map(|node| &node.value), + Some(Value::EventPlayer) + ) + } + + fn emit_modify( + &mut self, + level: usize, + name: &str, + op: ModifyOp, + value: wir::ValueId, + span: Option, + ) { + let indent = Self::indent(level); + match op { + ModifyOp::AppendToArray => { + self.out.push_str(&indent); + self.out.push_str(name); + self.out.push_str(".append("); + self.emit_value(value); + self.out.push_str(")\n"); + } + ModifyOp::RemoveFromArray => { + self.issue( + "unsupported-modify-op", + "Modify ... Remove From Array is outside the reconstruction surface \ + (the OPY surface has no remove-from-array form)", + span, + ); + } + ModifyOp::RemoveFromArrayByIndex => { + self.issue( + "unsupported-modify-op", + "Modify ... Remove From Array By Index is outside the reconstruction \ + surface (the OPY surface has no indexed remove-from-array form)", + span, + ); + } + ModifyOp::Min | ModifyOp::Max => { + self.issue( + "unsupported-modify-op", + format!( + "Modify ... {} is outside the reconstruction surface \ + (the OPY surface has no equivalent modification form)", + op.as_str() + ), + span, + ); + } + ModifyOp::Add + | ModifyOp::Subtract + | ModifyOp::Multiply + | ModifyOp::Divide + | ModifyOp::Modulo + | ModifyOp::RaiseToPower => { + let operator = match op { + ModifyOp::Add => "+", + ModifyOp::Subtract => "-", + ModifyOp::Multiply => "*", + ModifyOp::Divide => "/", + ModifyOp::Modulo => "%", + ModifyOp::RaiseToPower => "**", + _ => unreachable!(), + }; + self.out.push_str(&indent); + self.out.push_str(name); + self.out.push_str(" = "); + self.out.push_str(name); + self.out.push(' '); + self.out.push_str(operator); + self.out.push(' '); + self.emit_value(value); + self.out.push('\n'); + } + } + } + + /// A generic or member action call in statement position. + fn emit_call_action( + &mut self, + name: &str, + args: &[wir::ValueId], + indent: &str, + span: Option, + ) { + if DEDICATED_ACTION_NAMES.contains(&name) { + self.issue( + "unsupported-action-call", + format!( + "action call '{name}' is lowered to a dedicated WIR node by the \ + OPY frontend and has no reconstructible call form" + ), + span, + ); + return; + } + let Some(entry) = self.manifest.resolve_function(name) else { + match self.manifest.resolve_member(name) { + Some(entry) if entry.kind.is_action() => { + self.emit_member_call(entry, args, indent, span); + } + Some(_) => { + self.issue( + "unsupported-action-call", + format!( + "member value '{name}' cannot be emitted as an action on \ + the reconstruction surface" + ), + span, + ); + } + None => { + self.issue( + "unsupported-action-call", + format!( + "action call '{name}' has no OPY source form on the \ + reconstruction surface" + ), + span, + ); + } + } + return; + }; + if !entry.kind.is_action() { + self.issue( + "unsupported-action-call", + format!( + "value function '{name}' cannot be emitted as an action on \ + the reconstruction surface" + ), + span, + ); + return; + } + if args.is_empty() && self.subroutine_names.contains(name) { + self.issue( + "unsupported-action-call", + format!( + "action '{name}' with no arguments is ambiguous with a subroutine \ + of the same name on the OPY surface" + ), + span, + ); + return; + } + self.out.push_str(indent); + self.emit_manifest_call(entry, args, false, span); + self.out.push('\n'); + } + + /// Emit a manifest function call with explicit full-arity arguments, no + /// indent and no trailing newline (the caller frames the line). The OPY + /// frontend fills declared defaults at recompile time, so any WIR call + /// that omits a defaulted or required parameter cannot be reconstructed + /// identically and is rejected. + fn emit_manifest_call( + &mut self, + entry: &Function, + args: &[wir::ValueId], + member: bool, + span: Option, + ) { + let (receiver, params) = if member { + match args.split_first() { + Some((receiver, rest)) => (Some(*receiver), rest), + None => { + self.issue( + "unsupported-invalid-arity", + format!("member '{}' requires a receiver argument", entry.id), + span, + ); + return; + } + } + } else { + (None, args) + }; + let name = entry.id.as_str(); + if params.len() > entry.params.len() { + self.issue( + "unsupported-invalid-arity", + format!( + "{} '{}' expects at most {} arguments but the WIR carries {}", + kind_label(entry.kind), + name, + entry.params.len(), + params.len() + ), + span, + ); + return; + } + // Every parameter beyond the provided arguments must be omittable + // (`optional`). A required parameter (with or without a declared + // default) cannot be omitted: the OPY frontend would reject it or + // fill its default, changing the recompiled WIR. + for (_index, param) in entry.params.iter().enumerate().skip(params.len()) { + if !param.optional { + self.issue( + "unsupported-missing-argument", + format!( + "{} '{}' omits parameter '{}'; the OPY frontend would \ + reject or default-fill it and change the recompiled WIR", + kind_label(entry.kind), + name, + param.name + ), + span, + ); + } + } + + if let Some(receiver) = receiver { + self.emit_value(receiver); + self.out.push('.'); + } + self.out.push_str(name); + self.out.push('('); + // Cross-check through the Workshop catalog: a manifest entry with a + // declared `catalogId` must resolve there under the matching kind and + // the reconstruction locale (mirroring the manifest's own catalog + // cross-check test), so the reconstruction identity layer never + // drifts from the catalog. + if let Some(catalog_id) = &entry.catalog_id { + let expected_kind = match entry.kind { + FunctionKind::Action | FunctionKind::MemberAction => { + workshop_rs::catalog::Kind::Action + } + FunctionKind::Value | FunctionKind::MemberValue => { + workshop_rs::catalog::Kind::Value + } + }; + if self + .catalog + .spelling(expected_kind, self.locale, catalog_id) + .is_none() + { + self.issue( + "catalog-error", + format!( + "manifest entry '{}' links catalogId '{catalog_id}' which is \ + missing from the Workshop catalog", + entry.id + ), + span, + ); + } + } + for (index, arg) in params.iter().enumerate() { + if index > 0 { + self.out.push_str(", "); + } + self.check_param_argument(entry, index, *arg, span); + self.emit_value(*arg); + } + self.out.push(')'); + } + + /// A member call: `receiver.name(args...)`. + fn emit_member_call( + &mut self, + entry: &Function, + args: &[wir::ValueId], + indent: &str, + span: Option, + ) { + self.out.push_str(indent); + self.emit_manifest_call(entry, args, true, span); + self.out.push('\n'); + } + + /// Validate a provided argument against its manifest parameter: enum + /// domains are enforced (like the frontend) and `variable`-required + /// parameters must be variable references. + fn check_param_argument( + &mut self, + entry: &Function, + index: usize, + arg: wir::ValueId, + span: Option, + ) { + let Some(param) = entry.params.get(index) else { + return; + }; + let Some(node) = self.program.values.get(arg) else { + return; + }; + if let Some(domain) = ¶m.domain { + match &node.value { + Value::Enum { value_type, value } if value_type == domain => { + if !self.enum_member_in_domain(domain, value) { + self.issue( + "unsupported-enum-member", + format!( + "argument {} of '{}' uses enum member '{domain}.{value}' \ + which is outside the manifest's declared domain", + index + 1, + entry.id + ), + span, + ); + } + } + Value::Enum { value_type, .. } => { + self.issue( + "unsupported-enum-domain-mismatch", + format!( + "argument {} of '{}' expects enum domain '{domain}' but \ + the WIR carries '{value_type}'", + index + 1, + entry.id + ), + span, + ); + } + _ => { + self.issue( + "unsupported-enum-domain-mismatch", + format!( + "argument {} of '{}' expects an enum member of domain \ + '{domain}'", + index + 1, + entry.id + ), + span, + ); + } + } + } + if param.variable { + let is_variable = matches!( + node.value, + Value::GlobalVariable(_) | Value::PlayerVariable { .. } + ); + if !is_variable { + self.issue( + "unsupported-invalid-argument", + format!( + "argument {} of '{}' must be a variable reference", + index + 1, + entry.id + ), + span, + ); + } + } + } + + fn enum_member_in_domain(&self, domain: &str, member: &str) -> bool { + self.catalog.enum_domain(domain).is_some_and(|domain| { + domain + .members + .iter() + .any(|candidate| candidate.member == member) + }) + } + + // ---- value emission ---- + + fn emit_value(&mut self, id: wir::ValueId) { + let Some(node) = self.program.values.get(id) else { + self.issue("unsupported-dangling", "dangling value id", None); + return; + }; + match &node.value { + Value::Number { value, .. } => { + if !value.is_finite() { + self.issue( + "unsupported-non-finite-number", + format!("non-finite number literal '{value}' has no OPY spelling"), + node.span, + ); + } else if *value < 0.0 { + self.issue( + "unsupported-negative-number", + format!( + "negative number literal '{}' has no OPY literal form \ + (the lexer has no negative-number token)", + workshop_rs::format::format_number(*value) + ), + node.span, + ); + } else { + self.out + .push_str(&workshop_rs::format::format_number(*value)); + } + } + Value::String(value) => self.emit_string_literal(value), + Value::LocalizedString(value) => { + self.issue( + "unsupported-localized-string", + format!("localized Workshop preset string '{value}' has no OPY source representation"), + node.span, + ); + } + Value::Bool(value) => { + self.out.push_str(if *value { "true" } else { "false" }); + } + Value::Null => { + self.out.push_str("None"); + } + Value::Array(elements) => { + self.out.push('['); + for (index, element) in elements.iter().enumerate() { + if index > 0 { + self.out.push_str(", "); + } + self.emit_value(*element); + } + self.out.push(']'); + } + Value::Vector { x, y, z } => { + self.out.push_str("vect("); + self.emit_value(*x); + self.out.push_str(", "); + self.emit_value(*y); + self.out.push_str(", "); + self.emit_value(*z); + self.out.push(')'); + } + Value::Enum { value_type, value } => { + self.emit_enum(value_type, value, node.span); + } + Value::GlobalVariable(variable) => { + let Some(variable) = self.program.global_variables.get(*variable) else { + self.issue( + "unsupported-dangling", + "dangling global variable id", + node.span, + ); + return; + }; + self.out.push_str(&variable.name); + } + Value::PlayerVariable { player, variable } => { + if !self.is_event_player(*player) { + self.issue( + "unsupported-arbitrary-player-target", + "a player-variable access on a non-event-player expression is \ + outside the reconstruction surface (only eventPlayer.member \ + is representable)", + node.span, + ); + return; + } + let Some(variable) = self.program.player_variables.get(*variable) else { + self.issue( + "unsupported-dangling", + "dangling player variable id", + node.span, + ); + return; + }; + self.out.push_str("eventPlayer."); + self.out.push_str(&variable.name); + } + Value::Subroutine(_) => { + self.issue( + "unsupported-subroutine-value", + "subroutine values are outside the OPY reconstruction surface", + node.span, + ); + } + Value::EventPlayer => { + self.out.push_str("eventPlayer"); + } + Value::Call { name, args } => { + self.emit_value_call(name, args, node.span); + } + } + } + + fn emit_enum(&mut self, value_type: &str, value: &str, span: Option) { + let Some(domain) = self.catalog.enum_domain(value_type) else { + self.issue( + "unsupported-enum-domain", + format!( + "enum domain '{value_type}' is outside the manifest's declared \ + reconstruction surface" + ), + span, + ); + return; + }; + if !domain.members.iter().any(|member| member.member == value) { + self.issue( + "unsupported-enum-member", + format!( + "enum member '{value_type}.{value}' is outside the manifest's \ + declared domain" + ), + span, + ); + return; + } + self.out.push_str(value_type); + self.out.push('.'); + self.out.push_str(value); + } + + fn emit_value_call(&mut self, name: &str, args: &[wir::ValueId], span: Option) { + // Binary and unary operator calls keep their source spelling. + if BINARY_OPS.contains(&name) && args.len() == 2 { + self.out.push('('); + self.emit_value(args[0]); + self.out.push(' '); + self.out.push_str(name); + self.out.push(' '); + self.emit_value(args[1]); + self.out.push(')'); + return; + } + if name == "not" && args.len() == 1 { + self.out.push_str("(not "); + self.emit_value(args[0]); + self.out.push(')'); + return; + } + if name == "-" && args.len() == 1 { + self.out.push_str("(-"); + self.emit_value(args[0]); + self.out.push(')'); + return; + } + // The `format` special form: `"text".format(args...)`. + if name == "format" { + let Some(first) = args.first() else { + self.issue( + "unsupported-value-call", + "format call without a receiver is outside the reconstruction surface", + span, + ); + return; + }; + let Some(Value::String(text)) = self.program.values.get(*first).map(|node| &node.value) + else { + self.issue( + "unsupported-value-call", + "format call without a string receiver is outside the \ + reconstruction surface", + span, + ); + return; + }; + self.emit_string_literal(text); + self.out.push_str(".format("); + for (index, arg) in args.iter().skip(1).enumerate() { + if index > 0 { + self.out.push_str(", "); + } + self.emit_value(*arg); + } + self.out.push(')'); + return; + } + if DEDICATED_VALUE_NAMES.contains(&name) { + self.issue( + "unsupported-value-call", + format!( + "value call '{name}' is lowered to a dedicated WIR node by the \ + OPY frontend and has no reconstructible call form" + ), + span, + ); + return; + } + let Some(entry) = self.manifest.resolve_function(name) else { + match self.manifest.resolve_member(name) { + Some(entry) if entry.kind.is_value() => { + self.emit_manifest_call(entry, args, true, span); + } + Some(_) => { + self.issue( + "unsupported-value-call", + format!( + "member action '{name}' cannot be emitted as a value on \ + the reconstruction surface" + ), + span, + ); + } + None => { + self.issue( + "unsupported-value-call", + format!( + "value call '{name}' has no OPY source form on the \ + reconstruction surface" + ), + span, + ); + } + } + return; + }; + if !entry.kind.is_value() { + self.issue( + "unsupported-value-call", + format!( + "action function '{name}' cannot be emitted as a value on the \ + reconstruction surface" + ), + span, + ); + return; + } + if entry.context.is_some() { + self.issue( + "unsupported-value-call", + format!( + "value call '{name}' is only valid as a for-loop iterable on \ + the OPY surface" + ), + span, + ); + return; + } + self.emit_manifest_call(entry, args, false, span); + } + + fn emit_string_literal(&mut self, value: &str) { + self.out.push('"'); + for ch in value.chars() { + match ch { + '\\' => self.out.push_str("\\\\"), + '"' => self.out.push_str("\\\""), + '\n' => self.out.push_str("\\n"), + '\t' => self.out.push_str("\\t"), + '\r' => self.out.push_str("\\r"), + other => self.out.push(other), + } + } + self.out.push('"'); + } +} + +fn kind_label(kind: FunctionKind) -> &'static str { + match kind { + FunctionKind::Action => "action", + FunctionKind::Value => "value", + FunctionKind::MemberAction => "member action", + FunctionKind::MemberValue => "member value", + } +} diff --git a/crates/opy-compiler/tests/issue_46_oracle.rs b/crates/opy-compiler/tests/issue_46_oracle.rs index 2463fb1..5f67167 100644 --- a/crates/opy-compiler/tests/issue_46_oracle.rs +++ b/crates/opy-compiler/tests/issue_46_oracle.rs @@ -43,7 +43,7 @@ fn oracle_workshop(dir: &Path) -> String { fn compile_fixture(dir: &Path) -> opy_compiler::CompilationArtifact { let source = std::fs::read_to_string(dir.join("source.opy")).expect("source must be readable"); - let hir = opy_frontend::compile(&source, "source.opy", dir).expect("fixture must resolve"); + let hir = opy_rs::compile(&source, "source.opy", dir).expect("fixture must resolve"); Compiler::new() .expect("released workshop contract must load") .compile_hir(&hir) @@ -76,7 +76,7 @@ fn issue_46_native_lowering_matches_the_pinned_oracle() { fn issue_46_unsupported_primitive_fails_with_stable_source_attribution() { let dir = fixture_dir("issue-46-unsupported"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir) + let hir = opy_rs::compile(&source, "source.opy", &dir) .expect("the frontend resolves the negative fixture"); let error = match Compiler::new().unwrap().compile_hir(&hir) { Ok(_) => panic!("dict primitive lowering unexpectedly succeeded"), diff --git a/crates/opy-compiler/tests/issue_47_oracle.rs b/crates/opy-compiler/tests/issue_47_oracle.rs index 2eb84cd..f1c54b4 100644 --- a/crates/opy-compiler/tests/issue_47_oracle.rs +++ b/crates/opy-compiler/tests/issue_47_oracle.rs @@ -27,7 +27,7 @@ fn oracle_workshop(dir: &Path) -> String { fn issue_47_control_flow_matches_the_pinned_oracle() { let dir = fixture_dir("issue-47-control-flow"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).expect("fixture must resolve"); + let hir = opy_rs::compile(&source, "source.opy", &dir).expect("fixture must resolve"); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); let catalog = Catalog::builtin().unwrap(); let locale = Locale::new("en-US"); @@ -45,7 +45,7 @@ fn issue_47_control_flow_matches_the_pinned_oracle() { fn issue_47_nested_switch_break_matches_the_pinned_oracle() { let dir = fixture_dir("issue-33-switch-break"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).expect("fixture must resolve"); + let hir = opy_rs::compile(&source, "source.opy", &dir).expect("fixture must resolve"); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); let catalog = Catalog::builtin().unwrap(); let locale = Locale::new("en-US"); @@ -63,7 +63,7 @@ fn issue_47_nested_switch_break_matches_the_pinned_oracle() { fn issue_47_switch_order_matches_the_pinned_oracle() { let dir = fixture_dir("issue-47-switch-order"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).expect("fixture must resolve"); + let hir = opy_rs::compile(&source, "source.opy", &dir).expect("fixture must resolve"); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); let catalog = Catalog::builtin().unwrap(); let locale = Locale::new("en-US"); @@ -81,7 +81,7 @@ fn issue_47_switch_order_matches_the_pinned_oracle() { fn issue_47_structured_switch_target_matches_the_pinned_oracle() { let dir = fixture_dir("issue-47-switch-structured-target"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).expect("fixture must resolve"); + let hir = opy_rs::compile(&source, "source.opy", &dir).expect("fixture must resolve"); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); let catalog = Catalog::builtin().unwrap(); let locale = Locale::new("en-US"); @@ -99,7 +99,7 @@ fn issue_47_structured_switch_target_matches_the_pinned_oracle() { fn issue_47_do_while_break_shapes_match_the_pinned_oracle() { let dir = fixture_dir("issue-47-do-while-shapes"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).expect("fixture must resolve"); + let hir = opy_rs::compile(&source, "source.opy", &dir).expect("fixture must resolve"); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); let catalog = Catalog::builtin().unwrap(); let locale = Locale::new("en-US"); @@ -118,7 +118,7 @@ fn issue_47_multiple_switch_breaks_are_not_silently_dropped() { let compiler = Compiler::new().unwrap(); let dir = fixture_dir("issue-47-switch-multiple-break"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).unwrap(); + let hir = opy_rs::compile(&source, "source.opy", &dir).unwrap(); let error = match compiler.compile_hir(&hir) { Ok(_) => panic!("multi-break switch must not be silently truncated"), Err(error) => error, @@ -131,7 +131,7 @@ fn issue_47_multiple_switch_breaks_are_not_silently_dropped() { fn issue_47_invalid_do_while_placement_is_source_attributed() { let dir = fixture_dir("issue-47-do-while-invalid-placement"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let error = opy_frontend::compile(&source, "source.opy", &dir) + let error = opy_rs::compile(&source, "source.opy", &dir) .expect_err("invalid do-while placement must be rejected"); assert_eq!(error.code, "do-while-placement"); assert_eq!(error.span.unwrap().start.line, 6); @@ -142,7 +142,7 @@ fn issue_47_nested_switch_break_is_source_attributed_when_not_representable() { let compiler = Compiler::new().unwrap(); let dir = fixture_dir("issue-47-unsupported"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); - let hir = opy_frontend::compile(&source, "source.opy", &dir).unwrap(); + let hir = opy_rs::compile(&source, "source.opy", &dir).unwrap(); let error = match compiler.compile_hir(&hir) { Ok(_) => panic!("nested switch break unexpectedly lowered"), Err(error) => error, diff --git a/crates/opy-frontend/Cargo.toml b/crates/opy-rs/Cargo.toml similarity index 60% rename from crates/opy-frontend/Cargo.toml rename to crates/opy-rs/Cargo.toml index 01605f0..bb748fd 100644 --- a/crates/opy-frontend/Cargo.toml +++ b/crates/opy-rs/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "opy-frontend" +name = "opy-rs" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Standalone OverPy-compatible .opy frontend: lexer, CST/parser, preprocessing, semantic resolution, and Opy HIR lowering (Workshop-independent)." +description = "Standalone OverPy-compatible .opy implementation: lexer, CST/parser, preprocessing, semantic resolution, and Opy HIR lowering (Workshop-independent)." [lints] workspace = true diff --git a/crates/opy-frontend/src/cst.rs b/crates/opy-rs/src/cst.rs similarity index 100% rename from crates/opy-frontend/src/cst.rs rename to crates/opy-rs/src/cst.rs diff --git a/crates/opy-frontend/src/diag.rs b/crates/opy-rs/src/diag.rs similarity index 83% rename from crates/opy-frontend/src/diag.rs rename to crates/opy-rs/src/diag.rs index df21652..f3d6103 100644 --- a/crates/opy-frontend/src/diag.rs +++ b/crates/opy-rs/src/diag.rs @@ -1,6 +1,6 @@ //! Frontend diagnostics: structured, source-located failures. //! -//! Every frontend failure is a [`FrontendError`] with a stable `code`, a +//! Every frontend failure is a [`OpyError`] with a stable `code`, a //! human message, and an optional source span. The `code` is the machine //! contract; wording is not. //! @@ -10,7 +10,7 @@ /// A structured frontend error. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct FrontendError { +pub struct OpyError { /// A stable machine-readable code, e.g. `parse-error`. pub code: String, /// Human-readable message (not part of the machine contract). @@ -47,12 +47,12 @@ impl Span { } /// A crate-wide result alias. -pub type FrontendResult = Result; +pub type OpyResult = Result; -impl FrontendError { +impl OpyError { /// An error without a source span. - pub fn new(code: impl Into, message: impl Into) -> FrontendError { - FrontendError { + pub fn new(code: impl Into, message: impl Into) -> OpyError { + OpyError { code: code.into(), message: message.into(), span: None, @@ -60,8 +60,8 @@ impl FrontendError { } /// An error at a source position. - pub fn at(code: impl Into, message: impl Into, span: Span) -> FrontendError { - FrontendError { + pub fn at(code: impl Into, message: impl Into, span: Span) -> OpyError { + OpyError { code: code.into(), message: message.into(), span: Some(span), @@ -69,10 +69,10 @@ impl FrontendError { } } -impl std::fmt::Display for FrontendError { +impl std::fmt::Display for OpyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.code, self.message) } } -impl std::error::Error for FrontendError {} +impl std::error::Error for OpyError {} diff --git a/crates/opy-frontend/src/hir/dump.rs b/crates/opy-rs/src/hir/dump.rs similarity index 100% rename from crates/opy-frontend/src/hir/dump.rs rename to crates/opy-rs/src/hir/dump.rs diff --git a/crates/opy-frontend/src/hir/error.rs b/crates/opy-rs/src/hir/error.rs similarity index 100% rename from crates/opy-frontend/src/hir/error.rs rename to crates/opy-rs/src/hir/error.rs diff --git a/crates/opy-frontend/src/hir/mod.rs b/crates/opy-rs/src/hir/mod.rs similarity index 100% rename from crates/opy-frontend/src/hir/mod.rs rename to crates/opy-rs/src/hir/mod.rs diff --git a/crates/opy-frontend/src/hir/types.rs b/crates/opy-rs/src/hir/types.rs similarity index 100% rename from crates/opy-frontend/src/hir/types.rs rename to crates/opy-rs/src/hir/types.rs diff --git a/crates/opy-frontend/src/hir/validate.rs b/crates/opy-rs/src/hir/validate.rs similarity index 100% rename from crates/opy-frontend/src/hir/validate.rs rename to crates/opy-rs/src/hir/validate.rs diff --git a/crates/opy-frontend/src/lexer.rs b/crates/opy-rs/src/lexer.rs similarity index 96% rename from crates/opy-frontend/src/lexer.rs rename to crates/opy-rs/src/lexer.rs index 3397332..dd07f6d 100644 --- a/crates/opy-frontend/src/lexer.rs +++ b/crates/opy-rs/src/lexer.rs @@ -5,7 +5,7 @@ //! captured as a single directive token for the preprocessor. Positions are //! 1-based line/column, matching the Opy HIR protocol. -use crate::diag::{FrontendError, FrontendResult, Position, Span}; +use crate::diag::{OpyError, OpyResult, Position, Span}; /// The kind of a token. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -94,7 +94,7 @@ pub struct LexInput<'a> { } /// Lex one source file into a token stream. -pub fn lex(input: LexInput<'_>) -> FrontendResult> { +pub fn lex(input: LexInput<'_>) -> OpyResult> { Lexer::new(input.file_id, input.text).run() } @@ -119,7 +119,7 @@ impl Lexer { } } - fn run(mut self) -> FrontendResult> { + fn run(mut self) -> OpyResult> { while self.pos < self.chars.len() { let ch = self.chars[self.pos]; match ch { @@ -178,7 +178,7 @@ impl Lexer { '>' => self.two(TokenKind::Gt, TokenKind::Ge, '='), '!' => self.two(TokenKind::LexBang, TokenKind::Ne, '='), other => { - return Err(FrontendError::at( + return Err(OpyError::at( "lex-error", format!("unexpected character '{other}'"), self.here(1), @@ -192,7 +192,7 @@ impl Lexer { } /// `#` starts a `#!` directive (captured as one token) or a comment. - fn lex_hash(&mut self) -> FrontendResult<()> { + fn lex_hash(&mut self) -> OpyResult<()> { if self.peek(1) == Some('!') { let start = self.here(2); self.advance(); @@ -216,7 +216,7 @@ impl Lexer { Ok(()) } - fn skip_block_comment(&mut self) -> FrontendResult<()> { + fn skip_block_comment(&mut self) -> OpyResult<()> { let start = self.here(2); self.advance(); self.advance(); @@ -234,14 +234,14 @@ impl Lexer { self.advance(); } } - Err(FrontendError::at( + Err(OpyError::at( "lex-error", "unterminated block comment", start, )) } - fn lex_string(&mut self, quote: char) -> FrontendResult<()> { + fn lex_string(&mut self, quote: char) -> OpyResult<()> { let start = self.here(1); self.advance(); let mut value = String::new(); @@ -281,7 +281,7 @@ impl Lexer { continue; } if ch == '\n' { - return Err(FrontendError::at( + return Err(OpyError::at( "lex-error", "unterminated string literal", start, @@ -291,14 +291,14 @@ impl Lexer { value.push(ch); self.advance(); } - Err(FrontendError::at( + Err(OpyError::at( "lex-error", "unterminated string literal", start, )) } - fn lex_number(&mut self) -> FrontendResult<()> { + fn lex_number(&mut self) -> OpyResult<()> { let start = self.here(1); let mut text = String::new(); if self.chars[self.pos] == '0' && matches!(self.peek(1), Some('x' | 'X')) { @@ -312,7 +312,7 @@ impl Lexer { self.advance(); } if self.pos == digits_start { - return Err(FrontendError::at( + return Err(OpyError::at( "lex-error", "hexadecimal literal requires at least one hexadecimal digit", Span::new(self.file_id, start.start, self.here(0).start), diff --git a/crates/opy-frontend/src/lib.rs b/crates/opy-rs/src/lib.rs similarity index 79% rename from crates/opy-frontend/src/lib.rs rename to crates/opy-rs/src/lib.rs index 22c9512..b295791 100644 --- a/crates/opy-frontend/src/lib.rs +++ b/crates/opy-rs/src/lib.rs @@ -1,11 +1,11 @@ -//! The standalone OverPy-compatible `.opy` frontend (opy-rs). +//! The standalone OverPy-compatible `.opy` implementation (opy-rs). //! //! Owns the OPY source-language surface of the `opy-rs` repository: a lexer, //! an indentation-aware CST/parser with structured diagnostics and recovery, //! token-level preprocessing (includes and `#!define` macros), semantic //! resolution, and lowering into the opy-rs-owned Opy HIR contract //! ([`hir::Program`]). Everything from source through the Opy HIR semantic -//! model is Workshop-independent: the frontend never depends on `workshop-rs`, +//! model is Workshop-independent: source analysis never depends on `workshop-rs`, //! OverPy, or Node, and the integration boundary toward `workshop-rs` is //! documented rather than implemented here. //! @@ -17,18 +17,18 @@ //! during preprocessing with the reference's argument-injection ABI, and //! resource limits mirror the pinned reference constants //! (`opy_macro_js::Limits::default()`). Script-macro expansion is -//! compile-time behavior and is frontend-supported. +//! compile-time behavior and is source-supported. //! //! `#!postCompileHook` is recognized, parsed, validated, and recorded only //! (see [`preprocess`] and [`CompileOutcome::post_compile_hook`]): the -//! frontend never executes the hook. Real hook execution receives the final -//! Workshop text produced by lowering and is lowering-dependent (workshop-rs -//! emission, issue #8); the frontend never fabricates a Workshop payload. +//! The source implementation never executes the hook. Real hook execution +//! receives the final Workshop text produced by lowering and is +//! lowering-dependent (workshop-rs emission, issue #8); source analysis never +//! fabricates a Workshop payload. //! -//! This crate was extracted from the mature Wright frontend (the wright -//! repository's `crates/wright-opy`); module provenance and issue references -//! follow the original implementation. Workshop→OPY reconstruction and the -//! differential harness are not part of this crate (see the opy-rs roadmap). +//! This crate owns the OverPy source-language implementation. Workshop→OPY +//! reconstruction and the differential harness are not part of this crate +//! (see the opy-rs roadmap). pub mod cst; pub mod diag; @@ -45,7 +45,7 @@ pub mod tooling; use std::path::Path; use diag::Span; -pub use diag::{FrontendError, FrontendResult}; +pub use diag::{OpyError, OpyResult}; pub use lower::lower; pub use parser::parse; pub use preprocess::{preprocess, preprocess_with_overlay}; @@ -56,7 +56,7 @@ mod tests { use std::path::Path; #[test] - fn unsupported_operator_aliases_fail_at_the_frontend_boundary() { + fn unsupported_operator_aliases_fail_at_the_source_boundary() { for expression in ["a // 2", "a //= 2", "a ^ 2", "a && 2", "a || 2", "a = !2"] { let source = format!( "globalvar a\nrule \"unsupported operator\":\n @Event global\n {expression}\n" @@ -81,20 +81,20 @@ mod tests { } } -/// The frontend's supported protocol identity for generated HIR. +/// The producer identity for generated HIR. /// /// The producer identity and the Opy HIR protocol envelope (`wright/opy-hir` /// v2) is emitted for the ordered switch-arm wire grammar; v1 consumers must /// reject it until they migrate to the v2 contract. -pub const FRONTEND_NAME: &str = "wright/opy-native"; -pub const FRONTEND_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const LANGUAGE_NAME: &str = "opy-rs"; +pub const LANGUAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Compile one `.opy` source end-to-end into the Opy HIR contract: /// preprocess (includes/defines) → parse (CST) → lower (HIR). /// /// `main_path` is the file's display path recorded in the HIR file registry; /// `root` is the include base. `compile` never requires Node or OverPy. -pub fn compile(source: &str, main_path: &str, root: &Path) -> FrontendResult { +pub fn compile(source: &str, main_path: &str, root: &Path) -> OpyResult { compile_with_overlay(source, main_path, root, &std::collections::BTreeMap::new()) } @@ -106,7 +106,7 @@ pub fn compile_with_overlay( main_path: &str, root: &Path, overlay: &std::collections::BTreeMap, -) -> FrontendResult { +) -> OpyResult { let outcome = compile_with_overlay_outcome(source, main_path, root, overlay); match outcome.hir { Some(hir) => Ok(hir), @@ -118,21 +118,22 @@ pub fn compile_with_overlay( /// The outcome of a compile with overlays. /// -/// Unlike [`compile_with_overlay`], this retains the frontend file registry +/// Unlike [`compile_with_overlay`], this retains the source file registry /// even when parsing or lowering fails, so language tooling can map span file /// ids to their actual source identities without building a diagnostics-only /// project model. pub struct CompileOutcome { pub hir: Option, - pub error: Option, + pub error: Option, pub files: Vec, /// The declared `#!postCompileHook` script, when the source declared one /// and compilation succeeded. /// - /// This is the declaration record, not an execution result: the frontend + /// This is the declaration record, not an execution result: the OPY + /// implementation /// recognizes, parses, validates, and records the directive, but never /// executes the hook. Execution against the final Workshop text is - /// lowering-dependent (workshop-rs emission, issue #8); the frontend + /// lowering-dependent (workshop-rs emission, issue #8); source analysis /// never fabricates a Workshop payload. pub post_compile_hook: Option, } @@ -150,8 +151,8 @@ pub struct PostCompileHookRecord { pub span: Option, } -/// Compile with open-document overlays while retaining the frontend file -/// registry on parse/lower failure. +/// Compile with open-document overlays while retaining the source file registry +/// on parse/lower failure. /// /// This is the compile contract view of [`tooling::check_with_overlay`]: the /// two share one pipeline, so `check` and `compile` never disagree about @@ -165,7 +166,7 @@ pub fn compile_with_overlay_outcome( let outcome = tooling::check_with_overlay(source, main_path, root, overlay); // Every failed check carries at least one diagnostic, so a None model // always yields an error (the compile outcome invariant). - let error = outcome.diagnostics.first().map(|diagnostic| FrontendError { + let error = outcome.diagnostics.first().map(|diagnostic| OpyError { code: diagnostic.code.clone(), message: diagnostic.message.clone(), span: diagnostic @@ -174,7 +175,7 @@ pub fn compile_with_overlay_outcome( .map(tooling::SourceLocation::to_span), }); // The directive was parsed, validated, and recorded by preprocessing; the - // frontend never executes the hook (real hook execution receives the + // source implementation never executes the hook (real hook execution receives the // final Workshop text and is lowering-dependent, issue #8 — see // `PostCompileHookRecord`). let post_compile_hook = outcome.post_compile_hook.map(|hook| PostCompileHookRecord { diff --git a/crates/opy-frontend/src/lower.rs b/crates/opy-rs/src/lower.rs similarity index 99% rename from crates/opy-frontend/src/lower.rs rename to crates/opy-rs/src/lower.rs index abc9c59..c4c610d 100644 --- a/crates/opy-frontend/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -36,7 +36,7 @@ use crate::hir::types::{ }; use crate::cst::{self, CallArg, Decl, Expr, RuleEntry as CstRuleEntry, Stmt}; -use crate::diag::{FrontendError, FrontendResult, Span}; +use crate::diag::{OpyError, OpyResult, Span}; use crate::manifest::{ Function, FunctionContext, FunctionKind, Manifest, Param, ParamDefault, ReceiverCategory, }; @@ -69,7 +69,7 @@ struct Lowerer { allow_dict_literal: bool, /// The authoritative builtin semantic table (issue #109). manifest: &'static Manifest, - errors: Vec, + errors: Vec, } /// Lower a parsed program into the Opy HIR contract. @@ -77,7 +77,7 @@ pub fn lower( program: &cst::Program, files: Vec, defines: Vec, -) -> FrontendResult { +) -> OpyResult { lower_with_preprocessing(program, files, defines, &PreprocessingState::default()) } @@ -86,11 +86,11 @@ pub fn lower_with_preprocessing( files: Vec, defines: Vec, preprocessing: &PreprocessingState, -) -> FrontendResult { +) -> OpyResult { let manifest = match Manifest::builtin() { Ok(manifest) => manifest, Err(error) => { - return Err(FrontendError::new( + return Err(OpyError::new( "manifest-error", format!("cannot load the OPY semantic compatibility manifest: {error}"), )); @@ -227,9 +227,9 @@ pub fn lower_with_preprocessing( version: PROTOCOL_VERSION.to_string(), }, generator: Generator { - name: crate::FRONTEND_NAME.to_string(), - version: crate::FRONTEND_VERSION.to_string(), - frontend: "wright-native".to_string(), + name: crate::LANGUAGE_NAME.to_string(), + version: crate::LANGUAGE_VERSION.to_string(), + frontend: crate::LANGUAGE_NAME.to_string(), }, files, defines, @@ -262,7 +262,7 @@ fn render_rule_name( span: Span, files: &[SourceFile], preprocessing: &PreprocessingState, -) -> FrontendResult { +) -> OpyResult { let Some(template) = preprocessing .rule_prefix_template .as_ref() @@ -289,7 +289,7 @@ fn render_rule_name( ("$pathLower", TemplateValue::String(path.to_lowercase())), ]; evaluate_template(template, &values).map_err(|message| { - FrontendError::at( + OpyError::at( "rule-prefix-template-invalid", format!("could not resolve rule prefix template: {message}"), span, @@ -612,7 +612,7 @@ impl Lowerer { rule: &cst::Rule, files: &[SourceFile], preprocessing: &PreprocessingState, - ) -> FrontendResult { + ) -> OpyResult { let conditions = rule .conditions .iter() @@ -1832,7 +1832,7 @@ impl Lowerer { } fn error_at(&mut self, code: &str, message: String, span: Span) { - self.errors.push(FrontendError::at(code, message, span)); + self.errors.push(OpyError::at(code, message, span)); } } @@ -2191,7 +2191,7 @@ mod tests { // --- Builtin semantic manifest coverage (#109) --- /// Assert a compile failure has the given code at the given line. - fn compile_error(source: &str, line: u32) -> FrontendError { + fn compile_error(source: &str, line: u32) -> OpyError { let error = crate::compile(source, "test.opy", std::path::Path::new("")) .expect_err("expected a compile failure"); let span = error.span.expect("the error is source-located"); diff --git a/crates/opy-frontend/src/manifest/data/manifest.json b/crates/opy-rs/src/manifest/data/manifest.json similarity index 100% rename from crates/opy-frontend/src/manifest/data/manifest.json rename to crates/opy-rs/src/manifest/data/manifest.json diff --git a/crates/opy-frontend/src/manifest/mod.rs b/crates/opy-rs/src/manifest/mod.rs similarity index 100% rename from crates/opy-frontend/src/manifest/mod.rs rename to crates/opy-rs/src/manifest/mod.rs diff --git a/crates/opy-frontend/src/manifest/probes/action-in-value-position.opy b/crates/opy-rs/src/manifest/probes/action-in-value-position.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/action-in-value-position.opy rename to crates/opy-rs/src/manifest/probes/action-in-value-position.opy diff --git a/crates/opy-frontend/src/manifest/probes/aliases.opy b/crates/opy-rs/src/manifest/probes/aliases.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/aliases.opy rename to crates/opy-rs/src/manifest/probes/aliases.opy diff --git a/crates/opy-frontend/src/manifest/probes/catalog-only-names.opy b/crates/opy-rs/src/manifest/probes/catalog-only-names.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/catalog-only-names.opy rename to crates/opy-rs/src/manifest/probes/catalog-only-names.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-arg3-keyword-required.opy b/crates/opy-rs/src/manifest/probes/chase-arg3-keyword-required.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-arg3-keyword-required.opy rename to crates/opy-rs/src/manifest/probes/chase-arg3-keyword-required.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-duplicate-keyword.opy b/crates/opy-rs/src/manifest/probes/chase-duplicate-keyword.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-duplicate-keyword.opy rename to crates/opy-rs/src/manifest/probes/chase-duplicate-keyword.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-keyword-binding.opy b/crates/opy-rs/src/manifest/probes/chase-keyword-binding.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-keyword-binding.opy rename to crates/opy-rs/src/manifest/probes/chase-keyword-binding.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-keywords.opy b/crates/opy-rs/src/manifest/probes/chase-keywords.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-keywords.opy rename to crates/opy-rs/src/manifest/probes/chase-keywords.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-missing-argument.opy b/crates/opy-rs/src/manifest/probes/chase-missing-argument.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-missing-argument.opy rename to crates/opy-rs/src/manifest/probes/chase-missing-argument.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-over-time-defaults.opy b/crates/opy-rs/src/manifest/probes/chase-over-time-defaults.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-over-time-defaults.opy rename to crates/opy-rs/src/manifest/probes/chase-over-time-defaults.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-over-time-variable.opy b/crates/opy-rs/src/manifest/probes/chase-over-time-variable.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-over-time-variable.opy rename to crates/opy-rs/src/manifest/probes/chase-over-time-variable.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-over-time.opy b/crates/opy-rs/src/manifest/probes/chase-over-time.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-over-time.opy rename to crates/opy-rs/src/manifest/probes/chase-over-time.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-positional-after-keyword.opy b/crates/opy-rs/src/manifest/probes/chase-positional-after-keyword.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-positional-after-keyword.opy rename to crates/opy-rs/src/manifest/probes/chase-positional-after-keyword.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-reeval-context.opy b/crates/opy-rs/src/manifest/probes/chase-reeval-context.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-reeval-context.opy rename to crates/opy-rs/src/manifest/probes/chase-reeval-context.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-reeval-outside.opy b/crates/opy-rs/src/manifest/probes/chase-reeval-outside.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-reeval-outside.opy rename to crates/opy-rs/src/manifest/probes/chase-reeval-outside.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-unknown-keyword.opy b/crates/opy-rs/src/manifest/probes/chase-unknown-keyword.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-unknown-keyword.opy rename to crates/opy-rs/src/manifest/probes/chase-unknown-keyword.opy diff --git a/crates/opy-frontend/src/manifest/probes/chase-variable-first-arg.opy b/crates/opy-rs/src/manifest/probes/chase-variable-first-arg.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/chase-variable-first-arg.opy rename to crates/opy-rs/src/manifest/probes/chase-variable-first-arg.opy diff --git a/crates/opy-frontend/src/manifest/probes/enum-gated-members.opy b/crates/opy-rs/src/manifest/probes/enum-gated-members.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/enum-gated-members.opy rename to crates/opy-rs/src/manifest/probes/enum-gated-members.opy diff --git a/crates/opy-frontend/src/manifest/probes/generic-builtins.opy b/crates/opy-rs/src/manifest/probes/generic-builtins.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/generic-builtins.opy rename to crates/opy-rs/src/manifest/probes/generic-builtins.opy diff --git a/crates/opy-frontend/src/manifest/probes/generic-member-only-action.opy b/crates/opy-rs/src/manifest/probes/generic-member-only-action.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/generic-member-only-action.opy rename to crates/opy-rs/src/manifest/probes/generic-member-only-action.opy diff --git a/crates/opy-frontend/src/manifest/probes/get-players-in-radius.opy b/crates/opy-rs/src/manifest/probes/get-players-in-radius.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/get-players-in-radius.opy rename to crates/opy-rs/src/manifest/probes/get-players-in-radius.opy diff --git a/crates/opy-frontend/src/manifest/probes/invalid-arity-member.opy b/crates/opy-rs/src/manifest/probes/invalid-arity-member.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/invalid-arity-member.opy rename to crates/opy-rs/src/manifest/probes/invalid-arity-member.opy diff --git a/crates/opy-frontend/src/manifest/probes/invalid-arity-too-few.opy b/crates/opy-rs/src/manifest/probes/invalid-arity-too-few.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/invalid-arity-too-few.opy rename to crates/opy-rs/src/manifest/probes/invalid-arity-too-few.opy diff --git a/crates/opy-frontend/src/manifest/probes/invalid-arity-wait.opy b/crates/opy-rs/src/manifest/probes/invalid-arity-wait.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/invalid-arity-wait.opy rename to crates/opy-rs/src/manifest/probes/invalid-arity-wait.opy diff --git a/crates/opy-frontend/src/manifest/probes/invalid-receiver-append.opy b/crates/opy-rs/src/manifest/probes/invalid-receiver-append.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/invalid-receiver-append.opy rename to crates/opy-rs/src/manifest/probes/invalid-receiver-append.opy diff --git a/crates/opy-frontend/src/manifest/probes/invalid-receiver-format.opy b/crates/opy-rs/src/manifest/probes/invalid-receiver-format.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/invalid-receiver-format.opy rename to crates/opy-rs/src/manifest/probes/invalid-receiver-format.opy diff --git a/crates/opy-frontend/src/manifest/probes/is-game-in-progress.opy b/crates/opy-rs/src/manifest/probes/is-game-in-progress.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/is-game-in-progress.opy rename to crates/opy-rs/src/manifest/probes/is-game-in-progress.opy diff --git a/crates/opy-frontend/src/manifest/probes/keyword-arguments-unsupported.opy b/crates/opy-rs/src/manifest/probes/keyword-arguments-unsupported.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/keyword-arguments-unsupported.opy rename to crates/opy-rs/src/manifest/probes/keyword-arguments-unsupported.opy diff --git a/crates/opy-frontend/src/manifest/probes/member-aliases.opy b/crates/opy-rs/src/manifest/probes/member-aliases.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/member-aliases.opy rename to crates/opy-rs/src/manifest/probes/member-aliases.opy diff --git a/crates/opy-frontend/src/manifest/probes/member-value-in-action-position.opy b/crates/opy-rs/src/manifest/probes/member-value-in-action-position.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/member-value-in-action-position.opy rename to crates/opy-rs/src/manifest/probes/member-value-in-action-position.opy diff --git a/crates/opy-frontend/src/manifest/probes/probes.json b/crates/opy-rs/src/manifest/probes/probes.json similarity index 100% rename from crates/opy-frontend/src/manifest/probes/probes.json rename to crates/opy-rs/src/manifest/probes/probes.json diff --git a/crates/opy-frontend/src/manifest/probes/range-for-header.opy b/crates/opy-rs/src/manifest/probes/range-for-header.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/range-for-header.opy rename to crates/opy-rs/src/manifest/probes/range-for-header.opy diff --git a/crates/opy-frontend/src/manifest/probes/range-standalone.opy b/crates/opy-rs/src/manifest/probes/range-standalone.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/range-standalone.opy rename to crates/opy-rs/src/manifest/probes/range-standalone.opy diff --git a/crates/opy-frontend/src/manifest/probes/receiver-calls.opy b/crates/opy-rs/src/manifest/probes/receiver-calls.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/receiver-calls.opy rename to crates/opy-rs/src/manifest/probes/receiver-calls.opy diff --git a/crates/opy-frontend/src/manifest/probes/unknown-enum.opy b/crates/opy-rs/src/manifest/probes/unknown-enum.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/unknown-enum.opy rename to crates/opy-rs/src/manifest/probes/unknown-enum.opy diff --git a/crates/opy-frontend/src/manifest/probes/unknown-function.opy b/crates/opy-rs/src/manifest/probes/unknown-function.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/unknown-function.opy rename to crates/opy-rs/src/manifest/probes/unknown-function.opy diff --git a/crates/opy-frontend/src/manifest/probes/unknown-member.opy b/crates/opy-rs/src/manifest/probes/unknown-member.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/unknown-member.opy rename to crates/opy-rs/src/manifest/probes/unknown-member.opy diff --git a/crates/opy-frontend/src/manifest/probes/unknown-value.opy b/crates/opy-rs/src/manifest/probes/unknown-value.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/unknown-value.opy rename to crates/opy-rs/src/manifest/probes/unknown-value.opy diff --git a/crates/opy-frontend/src/manifest/probes/validate.py b/crates/opy-rs/src/manifest/probes/validate.py similarity index 97% rename from crates/opy-frontend/src/manifest/probes/validate.py rename to crates/opy-rs/src/manifest/probes/validate.py index 18047f3..7dcd391 100644 --- a/crates/opy-frontend/src/manifest/probes/validate.py +++ b/crates/opy-rs/src/manifest/probes/validate.py @@ -19,7 +19,7 @@ Stdlib-only; run from anywhere: - python3 crates/opy-frontend/src/manifest/probes/validate.py + python3 crates/opy-rs/src/manifest/probes/validate.py Exit code 0 only when every probe matches the recorded oracle evidence. """ @@ -31,7 +31,7 @@ import sys HERE = os.path.dirname(os.path.abspath(__file__)) -# crates/opy-frontend/src/manifest/probes -> repository root (5 levels up). +# crates/opy-rs/src/manifest/probes -> repository root (5 levels up). WORKSPACE = os.path.abspath(os.path.join(HERE, "..", "..", "..", "..", "..")) ORACLE = os.path.join( WORKSPACE, "compatibility", "oracle", "node_modules", "overpy", "cli.js" diff --git a/crates/opy-frontend/src/manifest/probes/value-in-action-position.opy b/crates/opy-rs/src/manifest/probes/value-in-action-position.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/value-in-action-position.opy rename to crates/opy-rs/src/manifest/probes/value-in-action-position.opy diff --git a/crates/opy-frontend/src/manifest/probes/wait-keyword-names.opy b/crates/opy-rs/src/manifest/probes/wait-keyword-names.opy similarity index 100% rename from crates/opy-frontend/src/manifest/probes/wait-keyword-names.opy rename to crates/opy-rs/src/manifest/probes/wait-keyword-names.opy diff --git a/crates/opy-frontend/src/parser.rs b/crates/opy-rs/src/parser.rs similarity index 98% rename from crates/opy-frontend/src/parser.rs rename to crates/opy-rs/src/parser.rs index abec6f4..017abf4 100644 --- a/crates/opy-frontend/src/parser.rs +++ b/crates/opy-rs/src/parser.rs @@ -2,7 +2,7 @@ //! //! Consumes the expanded token stream from [`crate::preprocess`] and builds a //! [`cst::Program`]. Parsing is deterministic and corpus-backed; malformed -//! input produces structured [`FrontendError`]s rather than panics, and the +//! input produces structured [`OpyError`]s rather than panics, and the //! parser recovers at statement/line boundaries so multiple useful errors are //! reported. The returned [`ParseOutput`] carries either a complete program //! or the collected errors (never both). @@ -11,7 +11,7 @@ use crate::cst::{ Annotation, AnnotationArg, CallArg, Decl, DictEntry, Event, Expr, IfBranch, Program, Rule, RuleEntry, Stmt, SwitchArm, }; -use crate::diag::{FrontendError, Position, Span}; +use crate::diag::{OpyError, Position, Span}; use crate::lexer::{Token, TokenKind}; /// The outcome of a parse. @@ -20,7 +20,7 @@ pub struct ParseOutput { /// The parsed program, present only when no errors were collected. pub program: Option, /// Every structured error collected during the parse. - pub errors: Vec, + pub errors: Vec, } /// Parse an expanded token stream into a CST program. @@ -53,7 +53,7 @@ pub fn parse_with_options(tokens: &[Token], allow_macro_redeclaration: bool) -> struct Parser<'a> { tokens: &'a [Token], pos: usize, - errors: Vec, + errors: Vec, allow_macro_redeclaration: bool, } @@ -126,8 +126,7 @@ impl Parser<'_> { fn error_at_current(&mut self, message: String) { let span = self.peek().span; - self.errors - .push(FrontendError::at("parse-error", message, span)); + self.errors.push(OpyError::at("parse-error", message, span)); } // ---- program ---- @@ -219,7 +218,7 @@ impl Parser<'_> { let token = self.advance(); index = token.text.parse::().ok(); if index.is_none() { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "parse-error", format!( "invalid variable index '{}' (expected an integer)", @@ -311,7 +310,7 @@ impl Parser<'_> { if !self.allow_macro_redeclaration && members.iter().any(|(name, _)| name == &member.text) { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "macro-redeclaration", format!("enum member '{name}.{}' is already defined", member.text), member_span, @@ -370,7 +369,7 @@ impl Parser<'_> { matches!(declaration, Decl::Macro { name: existing, .. } if existing == &name) }) { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "macro-redeclaration", format!("macro '{name}' is already defined"), name_token.span, @@ -1146,7 +1145,7 @@ impl Parser<'_> { } } if arms.is_empty() { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "parse-error", "switch must contain at least one case or default arm".to_string(), start.span, @@ -1342,7 +1341,7 @@ impl Parser<'_> { span: Span::new(span.file, span.start, end), }, _other => { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "parse-error", "cannot call this expression".to_string(), self.peek().span, @@ -1699,7 +1698,7 @@ impl Parser<'_> { '{' => { let end = self.find_f_string_end(&chars, index + 1); let Some(end) = end else { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "parse-error", "unterminated f-string interpolation".to_string(), string_span, @@ -1708,7 +1707,7 @@ impl Parser<'_> { }; let expression: String = chars[index + 1..end].iter().collect(); if expression.trim().is_empty() { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "parse-error", "f-string interpolation cannot be empty".to_string(), Span::new( @@ -1741,7 +1740,7 @@ impl Parser<'_> { index = end + 1; } '}' => { - self.errors.push(FrontendError::at( + self.errors.push(OpyError::at( "parse-error", "single '}' is not valid in an f-string".to_string(), string_span, @@ -1794,11 +1793,7 @@ impl Parser<'_> { /// Parse one f-string expression fragment and shift its local token spans /// into the original source file. -fn parse_expression_fragment( - text: &str, - file: u32, - origin: Position, -) -> Result { +fn parse_expression_fragment(text: &str, file: u32, origin: Position) -> Result { let mut tokens = crate::lexer::lex(crate::lexer::LexInput { file_id: file, text, @@ -1814,7 +1809,7 @@ fn parse_expression_fragment( }; let expression = parser.parse_expr().map_err(|()| { parser.errors.first().cloned().unwrap_or_else(|| { - FrontendError::at( + OpyError::at( "parse-error", "invalid f-string expression", Span::new(file, origin, origin), @@ -1877,7 +1872,7 @@ mod tests { output.program.unwrap() } - fn parse_err(text: &str) -> Vec { + fn parse_err(text: &str) -> Vec { let tokens = lex(LexInput { file_id: 0, text }).unwrap(); parse(&tokens).errors } diff --git a/crates/opy-frontend/src/preprocess.rs b/crates/opy-rs/src/preprocess.rs similarity index 95% rename from crates/opy-frontend/src/preprocess.rs rename to crates/opy-rs/src/preprocess.rs index bda1e15..5cf8305 100644 --- a/crates/opy-frontend/src/preprocess.rs +++ b/crates/opy-rs/src/preprocess.rs @@ -34,7 +34,7 @@ //! issue #8); the frontend never fabricates a Workshop payload. //! //! Boundary: `__script__` macros expand at compile time through the runtime -//! (frontend-supported); `#!postCompileHook` is recorded and executed only +//! (source-supported); `#!postCompileHook` is recorded and executed only //! against the real Workshop output (lowering-dependent). The runtime's hook //! ABI is tested separately on synthetic content in `opy-macro-js` (see its //! `hooks` test suite). @@ -44,7 +44,7 @@ use std::path::{Path, PathBuf}; use opy_macro_js::{Limits, MacroArg, MacroError, MacroRuntime}; -use crate::diag::{FrontendError, FrontendResult, Span}; +use crate::diag::{OpyError, OpyResult, Span}; use crate::hir::types::{ DirectiveRecord, DirectiveValue, OptimizationState, PreprocessingSnapshot, PreprocessingState, TranslationState, @@ -112,7 +112,7 @@ pub fn preprocess( main_text: &str, main_path: &str, root: &Path, -) -> FrontendResult<(Preprocessed, Vec)> { +) -> OpyResult<(Preprocessed, Vec)> { preprocess_with_overlay(main_text, main_path, root, &BTreeMap::new()) } @@ -125,7 +125,7 @@ pub fn preprocess_with_overlay( main_path: &str, root: &Path, overlay: &BTreeMap, -) -> FrontendResult<(Preprocessed, Vec)> { +) -> OpyResult<(Preprocessed, Vec)> { preprocess_with_overlay_outcome(main_text, main_path, root, overlay).result } @@ -133,7 +133,7 @@ pub fn preprocess_with_overlay( /// registered so far even when a directive or expansion fails, so callers can /// map an error's span file id to its actual source. pub struct PreprocessOutcome { - pub result: FrontendResult<(Preprocessed, Vec)>, + pub result: OpyResult<(Preprocessed, Vec)>, pub files: Vec, } @@ -171,7 +171,7 @@ pub fn preprocess_with_overlay_outcome( crate::diag::Position::new(1, first_line.chars().count() as u32 + 1), ); return PreprocessOutcome { - result: Err(FrontendError::at( + result: Err(OpyError::at( "main-file-invalid", "`#!mainFile` expects one quoted path on the first line", span, @@ -202,7 +202,7 @@ pub fn preprocess_with_overlay_outcome( None => { let Some(canonical) = canonical else { return PreprocessOutcome { - result: Err(FrontendError::at( + result: Err(OpyError::at( "main-file-not-found", format!("cannot find main file '{main_file}'"), span, @@ -214,7 +214,7 @@ pub fn preprocess_with_overlay_outcome( Ok(text) => text, Err(error) => { return PreprocessOutcome { - result: Err(FrontendError::at( + result: Err(OpyError::at( "main-file-not-found", format!("cannot read main file '{main_file}': {error}"), span, @@ -351,7 +351,7 @@ fn first_main_file_directive(text: &str) -> Option<(String, Span)> { impl Preprocessor { /// Process `#!` directive tokens, splicing includes and registering /// defines. Non-directive tokens are kept in place. - fn process_directives(&mut self, tokens: &mut Vec) -> FrontendResult<()> { + fn process_directives(&mut self, tokens: &mut Vec) -> OpyResult<()> { let mut out: Vec = Vec::with_capacity(tokens.len()); for token in tokens.drain(..) { if token.kind == TokenKind::Directive { @@ -381,7 +381,7 @@ impl Preprocessor { Ok(()) } - fn handle_directive(&mut self, token: Token, out: &mut Vec) -> FrontendResult<()> { + fn handle_directive(&mut self, token: Token, out: &mut Vec) -> OpyResult<()> { let text = token.text.trim(); let span = token.span; let (name, rest) = split_directive(text); @@ -392,7 +392,7 @@ impl Preprocessor { .and_then(|r| r.strip_suffix('"')) .or_else(|| rest.strip_prefix('\'').and_then(|r| r.strip_suffix('\''))); let Some(include) = include else { - return Err(FrontendError::at( + return Err(OpyError::at( "include-invalid", format!( "invalid include directive: `{text}` (expected `#!include \"file.opy\"`)" @@ -410,7 +410,7 @@ impl Preprocessor { if name == "undef" { let name = rest.trim(); if name.is_empty() || name.chars().any(|ch| !is_identifier_char(ch)) { - return Err(FrontendError::at( + return Err(OpyError::at( "undef-invalid", "malformed `#!undef` directive: expected one macro name", span, @@ -424,7 +424,7 @@ impl Preprocessor { if name == "postCompileHook" { let rest = rest.trim(); let Some(path) = strip_quoted(rest) else { - return Err(FrontendError::at( + return Err(OpyError::at( "script-invalid", format!( "invalid postCompileHook directive: `{text}` (expected `#!postCompileHook \"hook.js\"`)" @@ -433,7 +433,7 @@ impl Preprocessor { )); }; if self.post_compile_hook.is_some() { - return Err(FrontendError::at( + return Err(OpyError::at( "post-compile-hook-duplicate", "post-compile hook is already defined".to_string(), span, @@ -449,7 +449,7 @@ impl Preprocessor { return Ok(()); } if name == "mainFile" { - return Err(FrontendError::at( + return Err(OpyError::at( "main-file-placement", "`#!mainFile` must be the first directive in the main source", span, @@ -479,7 +479,7 @@ impl Preprocessor { } if name == "rulePrefix" { let prefix = strip_quoted(rest.trim()).ok_or_else(|| { - FrontendError::at( + OpyError::at( "rule-prefix-invalid", "`#!rulePrefix` expects one quoted string", span, @@ -494,7 +494,7 @@ impl Preprocessor { } if name == "rulePrefixTemplate" { if self.preprocessing.rule_prefix_template.is_some() { - return Err(FrontendError::at( + return Err(OpyError::at( "rule-prefix-template-duplicate", "a rule prefix template is already defined", span, @@ -526,7 +526,7 @@ impl Preprocessor { .filter_map(|item| replacement_family(&item.name)) .any(|item_family| item_family == family) { - return Err(FrontendError::at( + return Err(OpyError::at( "replacement-duplicate", format!("a replacement for `{family}` is already defined"), span, @@ -539,7 +539,7 @@ impl Preprocessor { self.record(name, Some(replacement), span); return Ok(()); } - Err(FrontendError::at( + Err(OpyError::at( "unsupported-directive", format!("unsupported preprocessing directive `#!{text}`"), span, @@ -584,9 +584,9 @@ impl Preprocessor { /// Resolve a script path root-relative (the reference's /// `getFilePaths(path, rootPath)` convention) and read its text. - fn resolve_script(&self, path: &str, span: Span) -> FrontendResult { + fn resolve_script(&self, path: &str, span: Span) -> OpyResult { let canonical = self.root.join(path).canonicalize().map_err(|_| { - FrontendError::at( + OpyError::at( "script-not-found", format!( "cannot find script '{path}' under root '{}'", @@ -596,7 +596,7 @@ impl Preprocessor { ) })?; let source = std::fs::read_to_string(&canonical).map_err(|error| { - FrontendError::at( + OpyError::at( "script-not-found", format!("cannot read script '{path}': {error}"), span, @@ -609,7 +609,7 @@ impl Preprocessor { } /// Resolve, lex, and splice one included file. - fn include(&mut self, include: &str, span: Span, out: &mut Vec) -> FrontendResult<()> { + fn include(&mut self, include: &str, span: Span, out: &mut Vec) -> OpyResult<()> { // The include base is the root; the main file is the only file in the // registry (reference convention), so path resolution is root-based. let candidate = self.root.join(include); @@ -631,7 +631,7 @@ impl Preprocessor { // otherwise the candidate path (overlays may not have a disk backing). let identity = canonical.clone().unwrap_or_else(|| candidate.clone()); if self.include_stack.contains(&identity) { - return Err(FrontendError::at( + return Err(OpyError::at( "include-cycle", format!( "include cycle detected: '{}' is already being included", @@ -645,7 +645,7 @@ impl Preprocessor { Some(text) => text, None => { let canonical = canonical.ok_or_else(|| { - FrontendError::at( + OpyError::at( "include-not-found", format!( "cannot find included file '{include}' under root '{}'", @@ -655,7 +655,7 @@ impl Preprocessor { ) })?; std::fs::read_to_string(&canonical).map_err(|error| { - FrontendError::at( + OpyError::at( "include-not-found", format!("cannot read included file '{include}': {error}"), span, @@ -679,7 +679,7 @@ impl Preprocessor { match crate::settings::find_blocks(&text, file_id) { Err(error) => return Err(error), Ok(blocks) if !blocks.is_empty() => { - return Err(FrontendError::at( + return Err(OpyError::at( "settings-placement", "settings blocks are only supported in the main file".to_string(), blocks[0].keyword_span, @@ -716,7 +716,7 @@ impl Preprocessor { /// A define is function-like when `(` immediately follows the name /// (`cakeBeam(start, end)`); a parenthesized object-like value /// (`#!define X (a + b)`) keeps its parentheses as value tokens. - fn define(&mut self, rest: &str, span: Span) -> FrontendResult<()> { + fn define(&mut self, rest: &str, span: Span) -> OpyResult<()> { let rest = rest.trim(); let first_open = rest.find('(').unwrap_or(usize::MAX); let first_space = rest.find(char::is_whitespace).unwrap_or(usize::MAX); @@ -725,7 +725,7 @@ impl Preprocessor { let (name, params, body_text) = if is_function_like { let name = rest[..first_open].trim(); let Some(close) = rest[first_open..].find(')') else { - return Err(FrontendError::at( + return Err(OpyError::at( "define-invalid", format!("malformed function-like define `#!define {rest}`: missing `)`"), span, @@ -749,7 +749,7 @@ impl Preprocessor { (name.to_string(), Vec::new(), body) }; if name.is_empty() { - return Err(FrontendError::at( + return Err(OpyError::at( "define-invalid", "malformed `#!define` directive: missing macro name", span, @@ -757,7 +757,7 @@ impl Preprocessor { } if self.macros.iter().any(|macro_def| macro_def.name == name) { if !self.preprocessing.allow_macro_redeclaration { - return Err(FrontendError::at( + return Err(OpyError::at( "macro-redeclaration", format!("macro '{name}' is already defined"), span, @@ -773,7 +773,7 @@ impl Preprocessor { // at the define site (missing files fail at compile time). let inner = &body_text["__script__(".len()..]; let inner = inner.strip_suffix(')').ok_or_else(|| { - FrontendError::at( + OpyError::at( "script-invalid", format!( "malformed script macro `#!define {rest}`: expected `__script__(\"path.js\")`" @@ -782,7 +782,7 @@ impl Preprocessor { ) })?; let Some(path) = strip_quoted(inner.trim()) else { - return Err(FrontendError::at( + return Err(OpyError::at( "script-invalid", format!( "malformed script macro `#!define {rest}`: expected a quoted script path" @@ -820,7 +820,7 @@ impl Preprocessor { } /// Expand all macros across the token stream, recursively. - fn expand(&self, tokens: Vec) -> FrontendResult> { + fn expand(&self, tokens: Vec) -> OpyResult> { let mut out = Vec::new(); let mut index = 0; while index < tokens.len() { @@ -860,11 +860,7 @@ impl Preprocessor { /// Collect the argument token lists of a function-like macro call, /// returning `(args, index_after_closing_paren)`. - fn collect_args( - &self, - tokens: &[Token], - open: usize, - ) -> FrontendResult<(Vec>, usize)> { + fn collect_args(&self, tokens: &[Token], open: usize) -> OpyResult<(Vec>, usize)> { let mut args: Vec> = Vec::new(); let mut current: Vec = Vec::new(); let mut depth = 0usize; @@ -888,7 +884,7 @@ impl Preprocessor { } cursor += 1; } - Err(FrontendError::new( + Err(OpyError::new( "macro-invalid", "unterminated macro invocation: missing closing `)`", )) @@ -905,9 +901,9 @@ impl Preprocessor { mac: &MacroDef, args: Vec>, use_site: Span, - ) -> FrontendResult> { + ) -> OpyResult> { if mac.is_function && args.len() != mac.params.len() { - return Err(FrontendError::at( + return Err(OpyError::at( "macro-arity", format!( "macro '{}' expects {} argument(s) but got {}", @@ -963,7 +959,7 @@ impl Preprocessor { script: &ScriptMacro, args: Vec>, use_site: Span, - ) -> FrontendResult> { + ) -> OpyResult> { let macro_args: Vec = mac .params .iter() @@ -998,9 +994,9 @@ impl Preprocessor { tokens: &mut Vec, stack: &mut Vec, depth: usize, - ) -> FrontendResult<()> { + ) -> OpyResult<()> { if depth > 64 { - return Err(FrontendError::new( + return Err(OpyError::new( "macro-recursion", "macro expansion exceeded the recursion limit (possible recursive define)", )); @@ -1013,7 +1009,7 @@ impl Preprocessor { let name = token.text.clone(); if let Some(mac) = self.macros.iter().find(|m| m.name == name) { if stack.iter().any(|s| s == &name) { - return Err(FrontendError::new( + return Err(OpyError::new( "macro-recursion", format!("recursive macro expansion detected for '{name}'"), )); @@ -1059,10 +1055,10 @@ fn is_identifier_char(ch: char) -> bool { ch.is_ascii_alphanumeric() || ch == '_' } -fn parse_words(rest: &str, directive: &str, span: Span) -> FrontendResult> { +fn parse_words(rest: &str, directive: &str, span: Span) -> OpyResult> { let words: Vec = rest.split_whitespace().map(str::to_string).collect(); if words.is_empty() { - return Err(FrontendError::at( + return Err(OpyError::at( "directive-invalid", format!("`#!{directive}` expects at least one argument"), span, @@ -1072,7 +1068,7 @@ fn parse_words(rest: &str, directive: &str, span: Span) -> FrontendResult FrontendResult FrontendResult> { +fn parse_translations(rest: &str, span: Span) -> OpyResult> { let values: Vec = rest .split_whitespace() .map(|language| language.replace('-', "_").to_lowercase()) .collect(); if values.is_empty() { - return Err(FrontendError::at( + return Err(OpyError::at( "translations-invalid", "`#!translations` expects at least one language", span, @@ -1101,7 +1097,7 @@ fn parse_translations(rest: &str, span: Span) -> FrontendResult> { .iter() .any(|language| !PINNED_LANGUAGES.contains(&language.as_str())) { - return Err(FrontendError::at( + return Err(OpyError::at( "translations-invalid", "invalid translation language; expected one of the pinned OverPy language codes", span, @@ -1112,7 +1108,7 @@ fn parse_translations(rest: &str, span: Span) -> FrontendResult> { .iter() .any(|value| value == "es_es" || value == "es_mx") { - return Err(FrontendError::at( + return Err(OpyError::at( "translations-invalid", "cannot combine `es` with `es_es` or `es_mx`", span, @@ -1123,7 +1119,7 @@ fn parse_translations(rest: &str, span: Span) -> FrontendResult> { .iter() .any(|value| value == "zh_cn" || value == "zh_tw") { - return Err(FrontendError::at( + return Err(OpyError::at( "translations-invalid", "cannot combine `zh` with `zh_cn` or `zh_tw`", span, @@ -1239,7 +1235,7 @@ fn json_string_literal(value: &str) -> String { /// line/column). Non-string completion values are `script-result-not-string` /// with the reference's wording, and engine setup failures are /// `script-internal`. -pub(crate) fn map_macro_error(error: &MacroError, script_path: &str, span: Span) -> FrontendError { +pub(crate) fn map_macro_error(error: &MacroError, script_path: &str, span: Span) -> OpyError { match error { MacroError::Script(script) => { let code = match script.message.as_str() { @@ -1253,7 +1249,7 @@ pub(crate) fn map_macro_error(error: &MacroError, script_path: &str, span: Span) (Some(line), None) => format!(" (line {line})"), _ => String::new(), }; - FrontendError::at( + OpyError::at( code, format!( "script '{}' failed: {}{}", @@ -1262,14 +1258,14 @@ pub(crate) fn map_macro_error(error: &MacroError, script_path: &str, span: Span) span, ) } - MacroError::InvalidResult { type_name } => FrontendError::at( + MacroError::InvalidResult { type_name } => OpyError::at( "script-result-not-string", format!( "JavaScript macro returned value with type of {type_name}, expected string. Try using .toString()" ), span, ), - MacroError::Internal(message) => FrontendError::at( + MacroError::Internal(message) => OpyError::at( "script-internal", format!("script '{}' runtime failure: {message}", script_path), span, diff --git a/crates/opy-frontend/src/settings.rs b/crates/opy-rs/src/settings.rs similarity index 96% rename from crates/opy-frontend/src/settings.rs rename to crates/opy-rs/src/settings.rs index c6623a2..a422eb0 100644 --- a/crates/opy-frontend/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -15,7 +15,7 @@ //! Workshop integration boundary, never in a local allowlist here. use crate::cst; -use crate::diag::{FrontendError, FrontendResult, Position, Span}; +use crate::diag::{OpyError, OpyResult, Position, Span}; /// A top-of-file `settings { ... }` block. #[derive(Debug, Clone)] @@ -42,7 +42,7 @@ pub struct SettingsBlock { /// a second/later block is `settings-placement` at its keyword span; brace /// matching respects `"`/`'` strings, `\` escapes, and nesting; an /// unterminated block is `settings-invalid`. -pub fn find_blocks(text: &str, file_id: u32) -> FrontendResult> { +pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { let chars: Vec = text.chars().collect(); let mut scanner = Scanner { chars: &chars, @@ -85,7 +85,7 @@ pub fn find_blocks(text: &str, file_id: u32) -> FrontendResult FrontendResult { +) -> OpyResult { scanner.skip_whitespace(); if scanner.chars.get(scanner.pos) != Some(&'{') { - return Err(FrontendError::at( + return Err(OpyError::at( "settings-invalid", "settings block must be a `settings { ... }` block (the `settings \"file\"` form is not supported)" .to_string(), @@ -129,7 +129,7 @@ fn match_block( let mut text_start = None; loop { let Some(ch) = scanner.chars.get(scanner.pos).copied() else { - return Err(FrontendError::at( + return Err(OpyError::at( "settings-invalid", "unterminated settings block (missing closing brace)".to_string(), keyword_span, @@ -199,7 +199,7 @@ pub fn sanitize_for_lex(text: &str, block: &SettingsBlock) -> String { /// (f64), `true`/`false`, arrays of strings, nested objects, trailing commas /// in objects and arrays. Rejections (`settings-invalid`): duplicate keys, /// non-object root, missing `gamemodes` group, malformed values. -pub fn parse_block(block: &SettingsBlock) -> FrontendResult { +pub fn parse_block(block: &SettingsBlock) -> OpyResult { let mut parser = Jsonc { text: &block.text, pos: 0, @@ -222,7 +222,7 @@ pub fn parse_block(block: &SettingsBlock) -> FrontendResult { .iter() .any(|node| matches!(node, cst::SettingsNode::Group { name, .. } if name == "gamemodes")) { - return Err(FrontendError::at( + return Err(OpyError::at( "settings-invalid", "settings block must contain a gamemodes group".to_string(), block.span, @@ -342,19 +342,19 @@ impl Jsonc<'_> { } } - fn error(&self, code: &str, message: String) -> FrontendError { - FrontendError::at( + fn error(&self, code: &str, message: String) -> OpyError { + OpyError::at( code, message, Span::new(self.file, self.here(), self.here()), ) } - fn error_at(&self, code: &str, message: String, span: Span) -> FrontendError { - FrontendError::at(code, message, span) + fn error_at(&self, code: &str, message: String, span: Span) -> OpyError { + OpyError::at(code, message, span) } - fn parse_object(&mut self) -> FrontendResult<(Vec, Span)> { + fn parse_object(&mut self) -> OpyResult<(Vec, Span)> { let open = self.here(); if self.advance() != Some('{') { return Err(self.error( @@ -370,7 +370,7 @@ impl Jsonc<'_> { /// Parse `key: value, ...` members. `root` is true when the enclosing /// object's braces are the settings block's own braces (the text runs to /// the end of the block, and a trailing comma before it is allowed). - fn parse_members(&mut self, root: bool) -> FrontendResult> { + fn parse_members(&mut self, root: bool) -> OpyResult> { let mut nodes = Vec::new(); let mut names = Vec::new(); self.skip_whitespace(); @@ -443,7 +443,7 @@ impl Jsonc<'_> { /// Parse one value; returns the built node (name placeholder) and the /// position after it. - fn parse_value(&mut self) -> FrontendResult<(cst::SettingsNode, Position)> { + fn parse_value(&mut self) -> OpyResult<(cst::SettingsNode, Position)> { let start = self.here(); let ch = self.peek(); let node = match ch { @@ -511,7 +511,7 @@ impl Jsonc<'_> { Ok((node, end)) } - fn expect_word(&mut self, word: &str) -> FrontendResult<()> { + fn expect_word(&mut self, word: &str) -> OpyResult<()> { let start = self.here(); for expected in word.chars() { if self.advance() != Some(expected) { @@ -525,7 +525,7 @@ impl Jsonc<'_> { Ok(()) } - fn parse_number(&mut self) -> FrontendResult { + fn parse_number(&mut self) -> OpyResult { let start = self.here(); let mut text = String::new(); if self.peek() == Some('-') { @@ -557,7 +557,7 @@ impl Jsonc<'_> { }) } - fn parse_list(&mut self) -> FrontendResult> { + fn parse_list(&mut self) -> OpyResult> { self.advance(); // '[' let mut elements = Vec::new(); self.skip_whitespace(); diff --git a/crates/opy-frontend/src/support.rs b/crates/opy-rs/src/support.rs similarity index 97% rename from crates/opy-frontend/src/support.rs rename to crates/opy-rs/src/support.rs index 92cf02f..1844235 100644 --- a/crates/opy-frontend/src/support.rs +++ b/crates/opy-rs/src/support.rs @@ -12,7 +12,7 @@ //! strict read-only consumer — it never writes, rewrites, or caches a //! modified copy of the matrix. //! -//! The five declared feature states (`planned`, `frontend-supported`, +//! The five declared feature states (`planned`, `source-supported`, //! `semantic-supported`, `lowering-dependent`, `end-to-end-supported`) are //! documented in the matrix itself; Workshop-dependent items stay //! `lowering-dependent` and are never approximated here (repo ownership @@ -34,7 +34,7 @@ pub const SUPPORT_MATRIX_SCHEMA_VERSION: u32 = 1; /// The declared feature states (see the matrix's `states` map for wording). pub const FEATURE_STATES: [&str; 5] = [ "planned", - "frontend-supported", + "source-supported", "semantic-supported", "lowering-dependent", "end-to-end-supported", @@ -198,10 +198,10 @@ mod tests { let matrix = SupportMatrix::builtin().unwrap(); let lexing = matrix.feature("syntax/lexing").expect("declared feature"); assert_eq!(lexing.category, "syntax"); - assert_eq!(lexing.state, "frontend-supported"); + assert_eq!(lexing.state, "source-supported"); assert_eq!( matrix.feature_state("syntax/lexing"), - Some("frontend-supported") + Some("source-supported") ); assert_eq!( matrix.feature_state("compilation/workshop-lowering"), diff --git a/crates/opy-frontend/src/tooling.rs b/crates/opy-rs/src/tooling.rs similarity index 99% rename from crates/opy-frontend/src/tooling.rs rename to crates/opy-rs/src/tooling.rs index 6565f64..b322265 100644 --- a/crates/opy-frontend/src/tooling.rs +++ b/crates/opy-rs/src/tooling.rs @@ -33,7 +33,7 @@ use std::path::Path; use serde::Serialize; use crate::cst; -use crate::diag::{FrontendError, Position, Span}; +use crate::diag::{OpyError, Position, Span}; use crate::hir; use crate::hir::types::{ Declaration, Define, Expr as HirExpr, RuleEntry, SourceFile, Stmt as HirStmt, @@ -188,7 +188,7 @@ pub struct Diagnostic { } impl Diagnostic { - fn from_error(error: FrontendError, files: &[FileRecord]) -> Diagnostic { + fn from_error(error: OpyError, files: &[FileRecord]) -> Diagnostic { Diagnostic { severity: DiagnosticSeverity::Error, code: error.code, diff --git a/crates/opy-frontend/tests/differential.rs b/crates/opy-rs/tests/differential.rs similarity index 98% rename from crates/opy-frontend/tests/differential.rs rename to crates/opy-rs/tests/differential.rs index ee037e9..349a56c 100644 --- a/crates/opy-frontend/tests/differential.rs +++ b/crates/opy-rs/tests/differential.rs @@ -80,7 +80,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use opy_frontend::{FRONTEND_NAME, FRONTEND_VERSION, compile}; +use opy_rs::{LANGUAGE_NAME, LANGUAGE_VERSION, compile}; use serde_json::{Value, json}; fn workspace_root() -> PathBuf { @@ -498,15 +498,15 @@ fn load_fixture(path: &Path) -> (String, String, String) { } /// Collect authored rule names from the native HIR in program order. -fn native_rule_names(program: &opy_frontend::hir::Program) -> Vec { +fn native_rule_names(program: &opy_rs::hir::Program) -> Vec { program .rules .iter() .filter_map(|entry| match entry { - opy_frontend::hir::RuleEntry::Rule(rule) => Some(rule.name.clone()), + opy_rs::hir::RuleEntry::Rule(rule) => Some(rule.name.clone()), // Subroutines are emitted as synthesized `rule ("Subroutine …")` // entries by the reference; both sides are normalized away. - opy_frontend::hir::RuleEntry::SubroutineDef { .. } => None, + opy_rs::hir::RuleEntry::SubroutineDef { .. } => None, }) .collect() } @@ -575,13 +575,13 @@ fn run_native( source_name: &str, fixture_dir: &Path, id: &str, -) -> Result { +) -> Result { let program = compile(source, source_name, fixture_dir)?; program .validate() .expect("native HIR must satisfy the v1 invariants"); let wire = serde_json::to_value(&program).expect("HIR serialization is infallible"); - let round_trip = opy_frontend::hir::parse_value(wire) + let round_trip = opy_rs::hir::parse_value(wire) .expect("the native wire payload must be consumable by parse_value"); round_trip.validate().expect("round-trip HIR must validate"); let dump = program.dump(); @@ -903,8 +903,8 @@ fn native_and_reference_agree_on_the_declared_corpus() { let report = json!({ "schemaVersion": 1, "artifact": "opy-rs native-vs-reference differential report (issue #25)", - "generatedBy": "crates/opy-frontend/tests/differential.rs", - "frontend": { "name": FRONTEND_NAME, "version": FRONTEND_VERSION }, + "generatedBy": "crates/opy-rs/tests/differential.rs", + "frontend": { "name": LANGUAGE_NAME, "version": LANGUAGE_VERSION }, "reference": matrix["reference"], "summary": { "total": counts["total"], diff --git a/crates/opy-frontend/tests/fixtures/macros/addfive.js b/crates/opy-rs/tests/fixtures/macros/addfive.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/addfive.js rename to crates/opy-rs/tests/fixtures/macros/addfive.js diff --git a/crates/opy-frontend/tests/fixtures/macros/addfive.opy b/crates/opy-rs/tests/fixtures/macros/addfive.opy similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/addfive.opy rename to crates/opy-rs/tests/fixtures/macros/addfive.opy diff --git a/crates/opy-frontend/tests/fixtures/macros/boom.js b/crates/opy-rs/tests/fixtures/macros/boom.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/boom.js rename to crates/opy-rs/tests/fixtures/macros/boom.js diff --git a/crates/opy-frontend/tests/fixtures/macros/hook-boom.js b/crates/opy-rs/tests/fixtures/macros/hook-boom.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/hook-boom.js rename to crates/opy-rs/tests/fixtures/macros/hook-boom.js diff --git a/crates/opy-frontend/tests/fixtures/macros/hook.js b/crates/opy-rs/tests/fixtures/macros/hook.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/hook.js rename to crates/opy-rs/tests/fixtures/macros/hook.js diff --git a/crates/opy-frontend/tests/fixtures/macros/hook.opy b/crates/opy-rs/tests/fixtures/macros/hook.opy similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/hook.opy rename to crates/opy-rs/tests/fixtures/macros/hook.opy diff --git a/crates/opy-frontend/tests/fixtures/macros/notstring.js b/crates/opy-rs/tests/fixtures/macros/notstring.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/notstring.js rename to crates/opy-rs/tests/fixtures/macros/notstring.js diff --git a/crates/opy-frontend/tests/fixtures/macros/runaway.js b/crates/opy-rs/tests/fixtures/macros/runaway.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/runaway.js rename to crates/opy-rs/tests/fixtures/macros/runaway.js diff --git a/crates/opy-frontend/tests/fixtures/macros/shout.js b/crates/opy-rs/tests/fixtures/macros/shout.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/shout.js rename to crates/opy-rs/tests/fixtures/macros/shout.js diff --git a/crates/opy-frontend/tests/fixtures/macros/shout.opy b/crates/opy-rs/tests/fixtures/macros/shout.opy similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/shout.opy rename to crates/opy-rs/tests/fixtures/macros/shout.opy diff --git a/crates/opy-frontend/tests/fixtures/macros/vect-macro.opy b/crates/opy-rs/tests/fixtures/macros/vect-macro.opy similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/vect-macro.opy rename to crates/opy-rs/tests/fixtures/macros/vect-macro.opy diff --git a/crates/opy-frontend/tests/fixtures/macros/vect.js b/crates/opy-rs/tests/fixtures/macros/vect.js similarity index 100% rename from crates/opy-frontend/tests/fixtures/macros/vect.js rename to crates/opy-rs/tests/fixtures/macros/vect.js diff --git a/crates/opy-frontend/tests/fixtures/multi-file/main.opy b/crates/opy-rs/tests/fixtures/multi-file/main.opy similarity index 100% rename from crates/opy-frontend/tests/fixtures/multi-file/main.opy rename to crates/opy-rs/tests/fixtures/multi-file/main.opy diff --git a/crates/opy-frontend/tests/fixtures/multi-file/shared/defs.opy b/crates/opy-rs/tests/fixtures/multi-file/shared/defs.opy similarity index 100% rename from crates/opy-frontend/tests/fixtures/multi-file/shared/defs.opy rename to crates/opy-rs/tests/fixtures/multi-file/shared/defs.opy diff --git a/crates/opy-frontend/tests/macro_integration.rs b/crates/opy-rs/tests/macro_integration.rs similarity index 97% rename from crates/opy-frontend/tests/macro_integration.rs rename to crates/opy-rs/tests/macro_integration.rs index d36377b..e9512d4 100644 --- a/crates/opy-frontend/tests/macro_integration.rs +++ b/crates/opy-rs/tests/macro_integration.rs @@ -26,8 +26,8 @@ use std::path::{Path, PathBuf}; -use opy_frontend::compile; -use opy_frontend::compile_with_overlay_outcome; +use opy_rs::compile; +use opy_rs::compile_with_overlay_outcome; fn fixture_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) @@ -35,13 +35,13 @@ fn fixture_dir() -> PathBuf { .to_path_buf() } -fn compile_fixture(name: &str) -> Result { +fn compile_fixture(name: &str) -> Result { let dir = fixture_dir(); let source = std::fs::read_to_string(dir.join(name)).unwrap(); compile(&source, name, &dir) } -fn outcome(name: &str) -> opy_frontend::CompileOutcome { +fn outcome(name: &str) -> opy_rs::CompileOutcome { let dir = fixture_dir(); let source = std::fs::read_to_string(dir.join(name)).unwrap(); compile_with_overlay_outcome(&source, name, &dir, &Default::default()) diff --git a/crates/opy-frontend/tests/tooling.rs b/crates/opy-rs/tests/tooling.rs similarity index 94% rename from crates/opy-frontend/tests/tooling.rs rename to crates/opy-rs/tests/tooling.rs index 92817a9..0f6de5e 100644 --- a/crates/opy-frontend/tests/tooling.rs +++ b/crates/opy-rs/tests/tooling.rs @@ -1,12 +1,12 @@ //! Integration tests for the Workshop-independent tooling API (issue #7): -//! multi-file project validation through [`opy_frontend::tooling::check`], +//! multi-file project validation through [`opy_rs::tooling::check`], //! semantic queries on the resolved model, and stable diagnostic codes for //! representative malformed inputs. use std::path::Path; -use opy_frontend::diag::{Position, Span}; -use opy_frontend::tooling::{self, SymbolKind, check}; +use opy_rs::diag::{Position, Span}; +use opy_rs::tooling::{self, SymbolKind, check}; /// The WrightKit-authored multi-file fixture: `main.opy` includes /// `shared/defs.opy`, declares `playervar P`, and uses symbols declared in @@ -42,22 +42,22 @@ fn multi_file_project_checks_and_resolves_end_to_end() { assert!(model .declarations() .iter() - .any(|decl| matches!(decl, opy_frontend::hir::types::Declaration::GlobalVariable { name, .. } if name == "total"))); + .any(|decl| matches!(decl, opy_rs::hir::types::Declaration::GlobalVariable { name, .. } if name == "total"))); assert!(model .declarations() .iter() - .any(|decl| matches!(decl, opy_frontend::hir::types::Declaration::PlayerVariable { name, .. } if name == "P"))); + .any(|decl| matches!(decl, opy_rs::hir::types::Declaration::PlayerVariable { name, .. } if name == "P"))); // Rule listing: the rule plus the def'd subroutine (the include splices // first, so the def entry precedes the rule entry). assert_eq!(model.rules().len(), 2); assert!(model.rules().iter().any(|entry| matches!( entry, - opy_frontend::hir::types::RuleEntry::Rule(rule) if rule.name == "collect" + opy_rs::hir::types::RuleEntry::Rule(rule) if rule.name == "collect" ))); assert!(model.rules().iter().any(|entry| matches!( entry, - opy_frontend::hir::types::RuleEntry::SubroutineDef { name, .. } if name == "finish" + opy_rs::hir::types::RuleEntry::SubroutineDef { name, .. } if name == "finish" ))); // Macro-expansion provenance: the #!define is recorded with its site. @@ -260,7 +260,7 @@ fn semantic_errors_follow_the_compile_first_error_contract() { ); let diagnostic = outcome.diagnostics.first().expect("first semantic error"); assert_eq!(diagnostic.code, "unknown-identifier"); - let compile_error = opy_frontend::compile( + let compile_error = opy_rs::compile( "rule \"r\":\n @Event global\n x = frobnicate()\n", "main.opy", Path::new(""), diff --git a/docs/README.md b/docs/README.md index c4465b5..1aef444 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ architecture, compatibility evidence, APIs, and internal contracts live here. - [Implementation role](opy/implementation-role.md): standalone OverPy implementation identity, relationship with `workshop-rs`, and Wright integration terminology. -- [Architecture](opy/architecture.md): internal source frontend, semantic HIR, +- [Architecture](opy/architecture.md): source parsing, semantic HIR, compiler/reconstruction boundaries, and dependency direction. - [Tooling API](opy/tooling-api.md): Rust library and CLI contracts for checking, inspection, overlays, diagnostics, and support queries. diff --git a/docs/compatibility/upstream-references.md b/docs/compatibility/upstream-references.md index 5040e0d..739d651 100644 --- a/docs/compatibility/upstream-references.md +++ b/docs/compatibility/upstream-references.md @@ -59,7 +59,7 @@ never `latest` or a range (see the pinning policy below). ### Oracle role OverPy 9.7.10 (pinned content) is the compatibility **oracle** and **behavior -reference** for `opy-rs`'s `.opy` frontend. It is not a production runtime +reference** for `opy-rs`'s `.opy` source implementation. It is not a production runtime dependency of `opy-rs` and is never bundled into release artifacts. Concretely, it serves as: @@ -71,7 +71,7 @@ it serves as: and [`docs/opy/compat-manifest-spec.md`](../opy/compat-manifest-spec.md)); * the reference for differential parity at the Opy HIR v2 boundary ([`docs/hir/opy-hir-v2.md`](../hir/opy-hir-v2.md)): the native differential - suite (`crates/opy-frontend/tests/differential.rs`, merged in PR #13) runs + suite (`crates/opy-rs/tests/differential.rs`, merged in PR #13) runs every corpus fixture through the native pipeline in `cargo test` and compares status, rule-name, and diagnostic evidence against the recorded oracle snapshots. @@ -253,7 +253,7 @@ when the oracle is absent. `else` is unverified; the version-sensitivity matrix must be re-run before any new acceptance is claimed. * **Round-trip boundary.** Emitted `settings` sections are deliberately not - reparseable by the Workshop frontend; a `.ws` decompiler is a non-goal for + reparseable by the Workshop source implementation; a `.ws` decompiler is a non-goal for `opy-rs` (Workshop → OPY decompilation is deferred to the `workshop-rs` integration stage; see `support-matrix.md`). diff --git a/docs/hir/opy-hir-v1.md b/docs/hir/opy-hir-v1.md index 084e15c..634978b 100644 --- a/docs/hir/opy-hir-v1.md +++ b/docs/hir/opy-hir-v1.md @@ -1,14 +1,14 @@ -# Opy HIR v1: opy-rs frontend protocol +# Opy HIR v1: opy-rs source implementation protocol Status: accepted baseline for v0.1; opy-rs-owned contract (adopted from the WrightKit evidence base, issue #2) -Scope: the interchange format produced by the opy-rs native frontend and +Scope: the interchange format produced by the opy-rs native source implementation and consumed by opy-rs tooling (and, later, WrightKit tooling consumers) This document is the normative specification for the Opy HIR protocol version -`1.1.0`. It defines the JSON payload that the native frontend in -`crates/opy-frontend` (the lowering stage) emits and that the Rust consumer -in the same crate validates and consumes. The frontend parses `.opy` source +`1.1.0`. It defines the JSON payload that the native source implementation in +`crates/opy-rs` (the lowering stage) emits and that the Rust consumer +in the same crate validates and consumes. The source implementation parses `.opy` source directly and owns the mapping from OPY syntax onto this schema; no component imports or wraps the reference implementation's AST (clean-room boundary, [`upstream-references.md`](../compatibility/upstream-references.md)). @@ -31,11 +31,11 @@ The protocol must: declarations, rules, events, conditions, statements, and expressions; 2. preserve file, line, and column provenance so later stages can report diagnostics against source; -3. be deterministic: the same source and frontend version produce +3. be deterministic: the same source and source implementation version produce byte-identical JSON; 4. be versioned so a producer and consumer can agree on compatibility without inspecting each other's implementation; -5. fail loudly on constructs the frontend cannot map, and be rejected or +5. fail loudly on constructs the source implementation cannot map, and be rejected or reported by the consumer rather than silently ignored. ## 2. Protocol envelope @@ -47,7 +47,7 @@ Every payload is a JSON object with the following top-level fields. | `protocol` | object | yes | Protocol identity and version. | | `generator` | object | yes | Producer identity for provenance. | | `files` | array | yes | Source-file registry referenced by spans. | -| `defines` | array | no | Preprocessor constant/function macros seen by the frontend. | +| `defines` | array | no | Preprocessor constant/function macros seen by the source implementation. | | `declarations` | array | yes | Symbols declared at program scope, grouped by kind, each group in declaration order. | | `rules` | array | yes | Rule and subroutine-definition bodies, in source order. | | `settings` | object | no | The typed custom-game-settings block, when the source had one (§2.5). | @@ -70,18 +70,18 @@ Every payload is a JSON object with the following top-level fields. ```jsonc { - "name": "wright/opy-native", + "name": "opy-rs", "version": "0.1.0", - "frontend": "overpy@9.7.10" + "source implementation": "overpy@9.7.10" } ``` * `name` identifies the producer. * `version` is the producer's own version. -* `frontend` records the exact external frontend identity (package and +* `source implementation` records the exact producer identity (package and version) the producer translated from, so compatibility evidence can name - the reference. The opy-rs native frontend records its own identity here - (`FRONTEND_NAME` = `wright/opy-native`, version = the crate version); the + the reference. The opy-rs native source implementation records its own identity here + (`LANGUAGE_NAME` = `opy-rs`, version = the crate version); the pinned reference identity is `overpy@9.7.10` (content commit `889d9749d1def17f146548cbddb94ea1ab015847`, see [`docs/compatibility/upstream-references.md`](../compatibility/upstream-references.md)). @@ -96,14 +96,14 @@ Every payload is a JSON object with the following top-level fields. ``` * `id` is a non-negative integer, unique within the payload. -* `path` is the file name as the frontend reported it, unique within the +* `path` is the file name as the source implementation reported it, unique within the payload. Paths are recorded for diagnostics; they are not canonicalized by the protocol. ### 2.4 `defines` Preprocessing definitions (`#!define` constants and function macros) that the -frontend expanded before parsing. They are recorded for provenance so a +source implementation expanded before parsing. They are recorded for provenance so a diagnostic can explain where a value came from; they carry no semantic payload because expansion already happened. @@ -175,17 +175,17 @@ half-open interval in a file: Declaration, rule, and `subroutineDef` nodes additionally carry an optional `name_span` field (wire spelling `name_span`): the exact source span of the identifier token (the declared or defined identifier, or the rule name inside -its string literal) when the frontend can record it. It is optional and +its string literal) when the source implementation can record it. It is optional and omitted when absent; it is never emitted as `null`, and it has the same shape -and validation as any other span (§8). The native frontend records it; the +and validation as any other span (§8). The native source implementation records it; the differential suite's normalization strips `span`-family fields from the per-fixture native wire-payload artifact (`target/opy-differential/`) as -documented frontend-internal provenance. Protocol and generator identities +documented source implementation-internal provenance. Protocol and generator identities are kept, and the oracle comparison itself uses status, rule-name, and diagnostic evidence rather than span data. Spans are for diagnostics and identity, not for byte-accurate reconstruction. -The frontend producer is responsible for emitting them; the consumer +The source implementation producer is responsible for emitting them; the consumer validates them (§8). A span whose end would precede its start (for example a node expanded from a preprocessor macro that mixes call-site and definition-site positions) must be normalized to a degenerate interval anchored at the start, so every @@ -209,9 +209,9 @@ discriminated by `kind`. All kinds carry `name` and `span` unless noted. ``` * `index` is the explicit index the source requested (`globalvar x 5`), or - `null` when the frontend assigns it later. + `null` when the source implementation assigns it later. * `initializer` is an expression or `null`. It is present only when the source - provided a non-trivial initializer; the frontend's implicit defaults are + provided a non-trivial initializer; the source implementation's implicit defaults are not emitted. * `name_span` is the exact span of the declared identifier token (see §3). @@ -281,7 +281,7 @@ rule object or a `subroutineDef` node (§4.3). Rules appear in source order. | --- | --- | --- | --- | | `name` | string | yes | The rule name as written (empty is allowed for delimiter rules). | | `span` | span | yes | The `rule` line. | -| `name_span` | span | no | The exact span of the rule name inside its string literal, when the frontend records it. | +| `name_span` | span | no | The exact span of the rule name inside its string literal, when the source implementation records it. | | `disabled` | boolean | yes | `true` when the rule is disabled by annotation. | | `event` | event | yes | The rule's event. | | `conditions` | array | yes | `@Condition` expressions, in source order. | @@ -310,15 +310,15 @@ whose `kind` the consumer does not recognize is an *unsupported node* (§7.3). | Kind | Fields | Meaning | | --- | --- | --- | | `expr` | `expr`, `span` | An expression statement (typically a call with side effects). | -| `assign` | `target`, `value`, `span` | Assignment. Compound assignments are desugared by the frontend. | +| `assign` | `target`, `value`, `span` | Assignment. Compound assignments are desugared by the source implementation. | | `if` | `branches`, `else`, `span` | Conditional. `branches` is an array of `{ "condition", "body" }`; `else` is an array of statements or `null`. | | `for` | `variable`, `iterable`, `body`, `span` | Iteration. `variable` is an expression naming the loop variable (a `globalVar` reference). | | `while` | `condition`, `body`, `span` | Loop. | | `doWhile` | `body`, `condition`, `span` | Loop whose body executes before its condition. | -| `switch` | `value`, `arms`, `span` | Source-order arms; execution falls through until a `break` or the end of the switch. Each arm is tagged `case` or `default`; a case has `value` and `body`, while a default has `body`, and each arm may carry `span`. At most one default arm is valid. | -| `break` | `span` | Exit the innermost switch or loop; invalid contexts are rejected by the frontend. | +| `switch` | `value`, `cases`, `default`, `span` | Source-order arms; execution falls through until a `break` or the end of the switch. | +| `break` | `span` | Exit the innermost switch or loop; invalid contexts are rejected by the source implementation. | | `callSubroutine` | `name`, `span` | Call a subroutine by name. | -| `pass` | `span` | A no-op emitted by the frontend. | +| `pass` | `span` | A no-op emitted by the source implementation. | Example `for` with `if`: @@ -377,13 +377,13 @@ Example `for` with `if`: ### 6.5 Operator semantics -Operators are opy-rs spellings for the semantics the frontend parsed: +Operators are opy-rs spellings for the semantics the source implementation parsed: * arithmetic: `+ - * / % **`; * comparison: `== != < <= > >=` (non-strict, Workshop semantics); * logical: `and or`, with `not` as a unary operator. -The frontend maps parsed OPY operator syntax onto these fixed spellings. The +The source implementation maps parsed OPY operator syntax onto these fixed spellings. The consumer treats `op` as an opaque string and validates it only structurally (§8). @@ -420,7 +420,7 @@ the program body. A node with an unknown `kind` (or an unknown statement/expression variant) is an *unsupported node*. The consumer reports a structured error that names the node kind and its span, so a regression report is explicit. Unsupported is -never a silent pass: the frontend refuses to emit nodes it cannot map, and the +never a silent pass: the source implementation refuses to emit nodes it cannot map, and the consumer refuses to consume nodes it cannot understand. ## 8. Validation requirements @@ -454,7 +454,7 @@ of the stable contract; the code and structured fields are. ## 9. Determinism and debug output -For the same input and frontend version, the producer must +For the same input and source implementation version, the producer must emit byte-identical JSON: object keys are emitted in a fixed order and collections (files, declarations, rules, branches, args) preserve source order. The consumer's debug dump (§10) must be stable for the same validated @@ -475,7 +475,7 @@ implementation-defined presentation, not part of the wire contract. It must: ## 11. Out of scope for v1 The following are intentionally not modeled in v1 and are rejected by the -frontend as unsupported when encountered: +source implementation as unsupported when encountered: * rule labels and relative gotos (`__skip__` / `__distanceTo__` forms); * decompilation-only constructs; @@ -489,11 +489,11 @@ reason to extend the schema silently. ## 12. Ownership * The protocol contract is owned by opy-rs and lives in this document. -* The native frontend owns all knowledge of how OPY source maps to this +* The native source implementation owns all knowledge of how OPY source maps to this schema. It never imports or depends on the reference implementation's types (clean-room boundary, [`upstream-references.md`](../compatibility/upstream-references.md)). -* Changes to the node grammar require a review of this document, the frontend +* Changes to the node grammar require a review of this document, the source implementation producer, the Rust consumer, and the corpus fixtures together (see [`docs/opy/support-matrix.md`](../opy/support-matrix.md) and [`docs/compatibility/upstream-references.md`](../compatibility/upstream-references.md)). @@ -504,4 +504,4 @@ reason to extend the schema silently. typed settings nodes, validation checks (§8 item 6), and a settings dump section. No existing node or field changed; consumers of the 1.x major accept the payload unchanged (`check_envelope` gates the major only). - The opy-rs native frontend emits 1.1.0. + The opy-rs native source implementation emits 1.1.0. diff --git a/docs/opy/architecture.md b/docs/opy/architecture.md index 4c30bc1..4f778af 100644 --- a/docs/opy/architecture.md +++ b/docs/opy/architecture.md @@ -50,9 +50,9 @@ The first path is partially implemented end-to-end today; the reverse path is not yet implemented. The support matrix, tests, and real-project evidence are the authority for current support rather than the intended pipeline alone. -## Internal frontend boundary +## Workshop-independent source path -The **frontend** is the Workshop-independent portion of `opy-rs`: +The Workshop-independent source path in `opy-rs` is: ```text source → preprocessing → parser → semantic model / HIR @@ -63,8 +63,6 @@ Workshop emission. This allows diagnostics, semantic queries, source-aware analysis, and validated edit foundations to work without forcing every tooling request through the compiler backend. -`frontend` is an implementation-stage term, not the product identity of -`opy-rs`. ## Ownership @@ -98,14 +96,14 @@ an OverPy-specific interpretation or lowering remains here. ## Dependency direction ```text -opy-frontend +opy-rs ↓ opy-compiler ↓ workshop-rs ``` -`opy-frontend` remains independently buildable and usable for the semantic +`opy-rs` remains independently buildable and usable for the semantic workflows that do not require canonical Workshop output. `opy-compiler` is the Workshop-dependent integration layer. diff --git a/docs/opy/compat-manifest-spec.md b/docs/opy/compat-manifest-spec.md index 3d5a47e..8eb076b 100644 --- a/docs/opy/compat-manifest-spec.md +++ b/docs/opy/compat-manifest-spec.md @@ -2,11 +2,11 @@ Status: accepted specification. An opy-rs-owned semantic contract, ported and adapted from the WrightKit evidence base (issue #2), implemented by the -frontend workstream (issues #4/#5) and merged on `main` (PRs #9/#13). +source implementation workstream (issues #4/#5) and merged on `main` (PRs #9/#13). Scope: the opy-rs-owned representation for builtin action/value identities, member functions, signatures, parameter enum-domain identities (catalog links), and source aliases; reference-validated and consumed by the native -frontend. The implementation lives in `crates/opy-frontend/src/manifest/` +source implementation. The implementation lives in `crates/opy-rs/src/manifest/` (data in `data/manifest.json`, probe evidence in `probes/`); this document is the schema and boundary contract for that data. @@ -17,8 +17,8 @@ opy-rs-owned manifest is justified: the declared parse surface exceeds the initial semantic surface, and residual `unknown-action`/`unknown-value`/ `unsupported-member` gaps are semantic-coverage gaps, not grammar gaps. The manifest replaced the hardcoded `KNOWN_ENUMS` table (removed from -`crates/opy-frontend/src/lower.rs` in the ownership-fixed frontend, PR #9) -with data and gives the frontend a single, reference-validated source for: +`crates/opy-rs/src/lower.rs` in the ownership-fixed source implementation, PR #9) +with data and gives the source implementation a single, reference-validated source for: * builtin actions and values (generic and member); * member-function metadata (receiver + argument signatures); @@ -35,7 +35,7 @@ It is **language-compatibility metadata**, distinct from: (`catalogId`) rather than duplicating spellings; * authoritative Workshop enum member lists, hero/map/mode/settings content, locale spellings, and canonical member/domain existence. These are - Workshop-owned catalog content that the frontend **never approximates**: + Workshop-owned catalog content that the source implementation **never approximates**: member accesses on a declared domain identity resolve as opaque identities, and member/domain/catalog validation stays `lowering-dependent` (#8); and * a runtime content registry (heroes/maps/abilities content data, extension @@ -112,11 +112,11 @@ Entry semantics: * `kind`: `action`/`value` are generic builtins; `memberAction`/`memberValue` are receiver methods whose `params` are the **explicit** arguments (the - receiver is separate). The frontend enforces action/value position + receiver is separate). The source implementation enforces action/value position (`value-in-action-position`, `action-in-value-position`). * `receiver`: the declared receiver category. `Player` is metadata for player-oriented members (the pinned reference does not type-check those - receivers, so the frontend accepts any receiver); `Variable` and `String` + receivers, so the source implementation accepts any receiver); `Variable` and `String` are enforced where the reference semantics are clear (`.append` requires an assignable receiver, `.format` a string literal). * `params`: ordered arguments. Arity is `(first defaulted/optional param @@ -157,7 +157,7 @@ Entry semantics: * `catalogId`: the canonical Workshop emission catalog id. A direct catalog entry uses `catalogLink: "canonical"` (the default); a missing `catalogId` must carry an explicit `catalogLink` reason: `special-lowering` for a - frontend form with custom lowering (`debug`, `print`, `chase`, `range`, or + source implementation form with custom lowering (`debug`, `print`, `chase`, `range`, or `append`), `legacy-alias` for a source identity whose compatibility is represented by an alias path (`stopChasing`), or `catalog-gap` for a probe-evidenced source member without a current canonical catalog entry @@ -166,7 +166,7 @@ Entry semantics: * `evidence`: every entry must reference at least one probe recording oracle acceptance (deterministic `check` failure otherwise). -Entries carry the minimal semantic data the frontend needs to resolve names, +Entries carry the minimal semantic data the source implementation needs to resolve names, check arity, resolve enum domains, and lower; they deliberately omit upstream description/localization text. @@ -185,9 +185,9 @@ hash, and, for rejections, the diagnostic category fragment). ## Validation rules -* `Manifest::load` (`crates/opy-frontend/src/manifest`): schema validation, +* `Manifest::load` (`crates/opy-rs/src/manifest`): schema validation, duplicate/colliding ids, colliding or missing aliases, declared parameter - domain identities (tracked for the frontend's opaque member resolution), + domain identities (tracked for the source implementation's opaque member resolution), enum-member defaults requiring a declared domain, keyword-binding data sanity (`keywordOnly`/`positionalOnly` are mutually exclusive, alternate spellings do not collide with other parameters), contextual-domain @@ -203,26 +203,26 @@ hash, and, for rejections, the diagnostic category fragment). * `probes/validate.py` performs reference validation. Every probe runs against the pinned oracle and must match its recorded accept/reject, normalized emission hash, and diagnostic category. The probe set and validator are - frontend-workstream-owned; the validator requires the pinned oracle (Node + + source implementation-workstream-owned; the validator requires the pinned oracle (Node + pnpm) and runs standalone like `compatibility/run_oracle.py`, so it is not part of the oracle-less harness suite. The probe names are recorded as evidence references in `compatibility/support-matrix.json`, and the native - differential suite (`crates/opy-frontend/tests/differential.rs`, merged in + differential suite (`crates/opy-rs/tests/differential.rs`, merged in PR #13) covers the same surface end-to-end in `cargo test`. -* The frontend consumes the manifest in `lower.rs`: unknown names, wrong +* The source implementation consumes the manifest in `lower.rs`: unknown names, wrong action/value position, invalid arity, invalid receiver category, and named/keyword argument binding (`unknown-keyword`, `duplicate-argument`, `missing-argument`, `positional-after-keyword`, `keyword-required`, `keyword-unsupported`, `invalid-argument`) produce structured, - source-located frontend diagnostics before Workshop emission. Workshop + source-located source implementation diagnostics before Workshop emission. Workshop enum member/domain mismatch checks were removed from the core in PR #9 (they require canonical Workshop catalog knowledge) and are `lowering-dependent` (#8); custom user-declared enum member validation is - OPY-level source semantics and stays frontend-owned. + OPY-level source semantics and stays source implementation-owned. ## Consumers -* the opy-rs frontend (`crates/opy-frontend`): name/member/enum resolution, +* the opy-rs source implementation (`crates/opy-rs`): name/member/enum resolution, arity and signature checks, early resolution of unknown-action/value errors; * `workshop-rs`: canonical-id linkage to the emission catalog and @@ -235,9 +235,9 @@ hash, and, for rejections, the diagnostic category fragment). ## Integration cross-check contract (#30, consumed by #8) -The manifest is the frontend-owned side of the integration contract. The +The manifest is the source implementation-owned side of the integration contract. The consumer receives the resolved Opy HIR plus this validated manifest; it does -not need to import OverPy data or add a parser/frontend dependency. HIR +not need to import OverPy data or add a parser/source implementation dependency. HIR `call`/`receiverCall` names are resolved against the manifest before lowering, so source spans and OPY diagnostics remain owned by `opy-rs`. diff --git a/docs/opy/compatibility-baseline.md b/docs/opy/compatibility-baseline.md index 40b67d6..5ee93ee 100644 --- a/docs/opy/compatibility-baseline.md +++ b/docs/opy/compatibility-baseline.md @@ -8,7 +8,7 @@ implementation tier and by support dimension This document is the planning counterpart to [`support-matrix.md`](support-matrix.md): the support matrix records the -corpus-evidenced surface the opy-rs frontend targets, while this baseline +corpus-evidenced surface the opy-rs source implementation targets, while this baseline records how the remaining surface is **tiered and sequenced**. A construct is not called supported merely because it parses; each row states parse, semantic, compilation, tooling/analysis, and reference coverage separately. @@ -17,8 +17,8 @@ The reference identity is the pinned OverPy 9.7.10 content (`889d9749d1def17f146548cbddb94ea1ab015847`); see [`docs/compatibility/upstream-references.md`](../compatibility/upstream-references.md) for provenance. Evidence claims in this document were verified against the -pinned oracle; the fixture manifest and runner own the current corpus -inventory. The opy-rs frontend foundation and #7 readiness work are +pinned oracle (the declared corpus now contains 42 provenance-linked +snapshots). The opy-rs source implementation foundation and #7 readiness work are implemented on `main` (issues #3–#7, #28–#30, and #33); the category table is the **tier assignment contract** for the remaining surface. The state column of `compatibility/support-matrix.json` tracks actual implementation progress @@ -39,7 +39,7 @@ implemented unless the table says otherwise. For each category the following dimensions are distinguished: -* **Parse**: accepted by the opy-rs frontend grammar; +* **Parse**: accepted by the opy-rs source implementation grammar; * **Semantic resolution**: resolved to a meaningful HIR/semantic value (names, members, enums, call semantics); * **Compilation**: standalone compile/emission through the `workshop-rs` @@ -52,7 +52,7 @@ work; * **Reference coverage**: oracle probes/fixtures validate the behavior. In the table, `✅` marks a dimension that is part of the declared contract for -the tier (evidenced by the merged frontend via the corpus and the native +the tier (evidenced by the merged source implementation via the corpus and the native differential suite), `❌` a deliberately rejected/documented-absent dimension, `—` an inapplicable dimension, and `partial` a bounded subset. @@ -62,14 +62,14 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and | # | Category | Tier | Parse | Sem | Comp | Tooling | Ref | | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | **Expression/postfix/member/call grammar**: operators and precedence, `[]` indexing, `.` member, calls, `del`, `in`/`not in`, hex `0x` | `baseline-supported` for the corpus subset (operators, indexing, calls, member/call); `++`/`--` remain tracked by opy-rs#59 | ✅ corpus | ✅ | ✅ (integration) | ✅ | ✅ differential (issue #7) | -| 1a | `switch`/`case`/`default`, `break`, `do…while`, `in`/`not in`, `0x` hex literals | `baseline-supported` for the pinned frontend surface; Workshop control-flow lowering remains integration-owned | ✅ | ✅ | partial (integration) | ✅ | ✅ oracle probes | -| 1b | String modifiers (`f`/`w`/`l`/`b`/`c`/`t`), dict literals, list comprehensions, signature-gated `lambda` | `baseline-supported` for the pinned frontend surface; formatting/emission remains lowering-dependent | ✅ | ✅ | partial (integration) | ✅ | ✅ oracle probes | +| 1a | `switch`/`case`/`default`, `break`, `do…while`, `in`/`not in`, `0x` hex literals | `baseline-supported` for the pinned source implementation surface; Workshop control-flow lowering remains integration-owned | ✅ | ✅ | partial (integration) | ✅ | ✅ oracle probes | +| 1b | String modifiers (`f`/`w`/`l`/`b`/`c`/`t`), dict literals, list comprehensions, signature-gated `lambda` | `baseline-supported` for the pinned source implementation surface; formatting/emission remains lowering-dependent | ✅ | ✅ | partial (integration) | ✅ | ✅ oracle probes | | 2 | **Declarations**: `globalvar`/`playervar` (index + initializer forms), `subroutine`, `enum`, `macro` constants (incl. member constants) | `baseline-supported` | ✅ | ✅ | ✅ (integration) | ✅ | ✅ | | 3 | **Assignments & control flow**: `=`, evidenced augmented (`+= -= *= /= %= **=`), `if`/`elif`/`else`, `for … in range(...)`, `while`, `pass` | `baseline-supported`; `min=`/`max=` remain workshop-rs#95 follow-up | ✅ | ✅ | ✅ (integration) | ✅ | ✅ | | 4 | **Rule directives & annotations**: `@Event`, `@Condition`, bare `@Team`/`@Slot`, rule name, event defaults (`global`, `all` team/player) | `baseline-supported` (bare forms) | ✅ | ✅ | ✅ (integration) | ✅ | ✅ | -| 4a | `@Team`/`@Slot` with arguments, `@Name`, `@Hero`, `@Disabled`, `@Delimiter`, `@NewPage`, `@SuppressWarnings` | `baseline-supported` for frontend state; Workshop domain/UI effects remain lowering-dependent | ✅ | ✅ | partial | ✅ | ✅ oracle probes | +| 4a | `@Team`/`@Slot` with arguments, `@Name`, `@Hero`, `@Disabled`, `@Delimiter`, `@NewPage`, `@SuppressWarnings` | `baseline-supported` for source implementation state; Workshop domain/UI effects remain lowering-dependent | ✅ | ✅ | partial | ✅ | ✅ oracle probes | | 5 | **Preprocessing/include/macro**: `#!include`, `#!define` (object- and function-like), `#!undef`, include cycle detection | `baseline-supported` | ✅ | ✅ | ✅ (integration) | ✅ | ✅ | -| 5a | `#!mainFile`, `#!allowMacroRedeclaration`, `#!optimize*`/`#!replace0By*` family, `#!translations`, `#!rulePrefix*`, `__script__` JS hooks | `baseline-supported` for frontend state; optimizer/locale/hook effects remain lowering-dependent | ✅ | ✅ | partial | ✅ | ✅ oracle probes | +| 5a | `#!mainFile`, `#!allowMacroRedeclaration`, `#!optimize*`/`#!replace0By*` family, `#!translations`, `#!rulePrefix*`, `__script__` JS hooks | `baseline-supported` for source implementation state; optimizer/locale/hook effects remain lowering-dependent | ✅ | ✅ | partial | ✅ | ✅ oracle probes | | 6 | **OPY builtin actions & values (generic)**: manifest identities, signatures, aliases, and call semantics | `baseline-supported` for the probe-validated manifest overlay; canonical Workshop existence/content/emission is `lowering-dependent` | ✅ | ✅ | partial (integration) | ✅ | ✅ probes | | 6a | **Canonical Workshop builtin action/value catalog**: full catalog existence, content, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | | 7 | **OPY receiver/member semantics**: receiver categories, explicit-argument signatures, variable receivers | `baseline-supported` for the manifest-declared OPY overlay; canonical member existence/content/emission is `lowering-dependent` | ✅ | ✅ | partial (integration) | ✅ | ✅ probes | @@ -82,13 +82,13 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and | 11 | **Named/keyword arguments**: `chase(A, B, rate=30, …)`, generic `name = expr` binding against manifest signatures | `baseline-supported` for the evidence surface (generic keyword binding plus the `chase`/`ChaseReeval` call-context forms); `raycast` `include=`/`exclude=` forms and macro keyword arguments stay `evidence-prioritized` (no corpus/reference evidence in the declared surface) | ✅ | ✅ | ✅ (integration) | ✅ | ✅ probes | | 12 | **Settings/content metadata**: `settings { … }` blocks | `baseline-supported` (JSONC subset + typed HIR payload); the Workshop `settings` emission table is **lowering-dependent** | ✅ | ✅ | ❌ (integration) | ✅ | ✅ | | 12a | `settings "file"`, richer settings expressions, hero/map/ability content beyond the pin | `legacy-quirk/demand-driven` / `reference-limited` | ❌/partial | ❌ | ❌ | ❌ | partial (data newer than pin unavailable per the pinning policy) | -| 13 | **Source identity & diagnostics**: structured, source-located frontend errors, `wright-result/v1` | `baseline-supported` | ✅ | ✅ | — | ✅ | ✅ S/D | +| 13 | **Source identity & diagnostics**: structured, source-located source implementation errors, `wright-result/v1` | `baseline-supported` | ✅ | ✅ | — | ✅ | ✅ S/D | ## Current `planned` entries There are no remaining `planned` entries in -`compatibility/support-matrix.json`. The pinned OPY frontend surface from -#28/#29/#30/#33 is represented as frontend- or semantic-supported; Workshop +`compatibility/support-matrix.json`. The pinned OPY source implementation surface from +#28/#29/#30/#33 is represented as source implementation- or semantic-supported; Workshop catalog, emission, and runtime effects remain explicitly `lowering-dependent`. Their tiers above distinguish **evidence-prioritized** work (broad or high-fan-out surface with @@ -102,7 +102,7 @@ requires them), not every upstream quirk is a planned implementation. Verified against the pinned oracle. Each item is classified with the tier it belongs to; none is a per-symbol implementation request. Items marked *manifest-covered* resolve through the OPY semantic compatibility manifest -(`crates/opy-frontend/src/manifest/`), which is merged on `main`; rows below +(`crates/opy-rs/src/manifest/`), which is merged on `main`; rows below record their current opy-rs status against the current support-matrix baseline, and remaining gaps stay classified rather than being filed per-symbol. @@ -115,8 +115,8 @@ and remaining gaps stay classified rather than being filed per-symbol. | **Member value/signature gap**: `getPlayersInRadius(...).setStatusEffect(eventPlayer, 30)` | **reject** (arity: `.setStatusEffect` needs `player, assister, status, duration`) | implemented: rejects with a structured arity diagnostic (`missing-argument`); probe `invalid-arity-member` | `baseline-supported` (manifest-covered) | | **Enum-gated members**: `eventPlayer.setInvisibility(Invis.ALL)`, `eventPlayer.getThrottle()`, `worldVector(...)` (args typed `Invis`/`Transform`) | accept | implemented: manifest domain identities resolve as opaque members; member-existence validation is **lowering-dependent** (#8); probe `enum-gated-members` | `baseline-supported` (manifest-covered); catalog spellings lowering-dependent | | **Named arguments / `ChaseReeval` alias**: `chase(A, 10, rate=2, ChaseReeval.NONE)` | accept (contextual alias resolution) | implemented: generic `name = expr` binding plus the `chase` special form; probes `chase-keywords`, `chase-reeval-context`, `chase-keyword-binding`, the `chase-*` diagnostic probes, and the `synthetic/chase-keywords` corpus fixture | `baseline-supported` (manifest-covered) | -| **Ambiguous Workshop enum spelling**: `ChaseTimeReeval.NONE`, `ChaseRateReeval.NONE`, and `Invis.NONE` all emit as bare `None` | — | emission-context resolution is **lowering-dependent** (needs the Workshop emission context); frontend-side signature-pinned resolution stays frontend-owned | `lowering-dependent` for context-free `None`; signature-pinned contexts are `baseline-supported` | -| **Constant-0 canonicalization**: `globalvar A = 0` drops the initializer; `= 5`/`= 0.0` preserved via the Initialize rule; `globalvar A 0` is an explicit index | canonical | implemented: `globalvar A = 0` drops the initializer and `= 5`/`= 0.0` are preserved, matching the reference; Initialize-rule synthesis is lowering-dependent | `baseline-supported` (frontend part); lowering-dependent (Initialize synthesis) | +| **Ambiguous Workshop enum spelling**: `ChaseTimeReeval.NONE`, `ChaseRateReeval.NONE`, and `Invis.NONE` all emit as bare `None` | — | emission-context resolution is **lowering-dependent** (needs the Workshop emission context); source implementation-side signature-pinned resolution stays source implementation-owned | `lowering-dependent` for context-free `None`; signature-pinned contexts are `baseline-supported` | +| **Constant-0 canonicalization**: `globalvar A = 0` drops the initializer; `= 5`/`= 0.0` preserved via the Initialize rule; `globalvar A 0` is an explicit index | canonical | implemented: `globalvar A = 0` drops the initializer and `= 5`/`= 0.0` are preserved, matching the reference; Initialize-rule synthesis is lowering-dependent | `baseline-supported` (source implementation part); lowering-dependent (Initialize synthesis) | | **Diagnostic provenance**: unresolved action/value errors surface as structured semantic diagnostics, not emitter catalog misses | — | implemented: structured semantic diagnostics (`unknown-action`, `unknown-value`, `unknown-member`, `invalid-arity`, `invalid-receiver`, `action-in-value-position`, `value-in-action-position`, `invalid-call-context`, `invalid-iterable`, argument-binding codes); Workshop enum member/domain mismatch codes were removed with the catalog validation (PR #9) and stay `lowering-dependent` | `baseline-supported` (manifest-covered) | ## Boundaries @@ -141,6 +141,6 @@ and remaining gaps stay classified rather than being filed per-symbol. * [`docs/compatibility/upstream-references.md`](../compatibility/upstream-references.md): pinned reference identity and provenance * [`support-matrix.md`](support-matrix.md): corpus-evidenced declared surface -* [`compat-manifest-spec.md`](compat-manifest-spec.md): machine-readable semantic manifest specification (data in `crates/opy-frontend/src/manifest/`) +* [`compat-manifest-spec.md`](compat-manifest-spec.md): machine-readable semantic manifest specification (data in `crates/opy-rs/src/manifest/`) * [`compatibility/support-matrix.json`](../../compatibility/support-matrix.json): machine-readable state tracking * [`compatibility/README.md`](../../compatibility/README.md): corpus and harness layout diff --git a/docs/opy/implementation-role.md b/docs/opy/implementation-role.md index b6c05f0..12eb83e 100644 --- a/docs/opy/implementation-role.md +++ b/docs/opy/implementation-role.md @@ -1,7 +1,8 @@ # opy-rs implementation role `opy-rs` is an independently usable Rust implementation of the OverPy language. -Its product boundary is larger than a parser/frontend and larger than an LPP +Its product boundary includes parsing, preprocessing, semantic HIR, compiler +integration, diagnostics, tooling, and reconstruction, in addition to any LPP provider process. ## Durable model @@ -9,7 +10,7 @@ provider process. ```text OPY source ↓ -opy-rs frontend +opy-rs parsing / preprocessing / semantic HIR ↓ OPY semantic model / HIR ↓ @@ -37,18 +38,7 @@ Workshop boundary. It does not need to reimplement raw Workshop to be a complete OverPy implementation; it deliberately reuses the canonical Workshop implementation in `workshop-rs`. -## Terminology - -### Frontend - -A frontend is an internal stage: source text → parsed/source model → semantic -model/HIR. The frontend is intentionally Workshop-independent so diagnostics, -semantic queries, source tooling, and other non-emission workflows do not need -the compiler backend. - -Do not use **frontend** as shorthand for the repository's overall product role. - -### Provider +## Provider A provider is a process/API role through which an implementation can expose language intelligence to a tooling client such as Wright. LPP may be one such @@ -80,7 +70,7 @@ The dependency direction is `opy-rs → workshop-rs`; there is no dependency fro The repository already exposes standalone check/inspect/support tooling. OPY → Workshop compilation is only partially implemented, and Workshop → OPY reconstruction is not yet implemented. These are implementation-completeness -gaps, not reasons to redefine `opy-rs` as a frontend-only repository. +gaps, not reasons to narrow `opy-rs` to one compiler stage. Support claims must continue to follow the compatibility matrix and executable evidence rather than this architectural intent alone. diff --git a/docs/opy/tooling-api.md b/docs/opy/tooling-api.md index 030a618..24e33e8 100644 --- a/docs/opy/tooling-api.md +++ b/docs/opy/tooling-api.md @@ -1,7 +1,7 @@ # opy-rs Tooling API -Workshop-independent tooling surface for the OPY frontend (issue #7): a -library API in `crates/opy-frontend` (`opy_frontend::tooling`) and a +Workshop-independent tooling surface for the OPY source implementation (issue #7): a +library API in `crates/opy-rs` (`opy_rs::tooling`) and a standalone CLI in `crates/opy-cli` (`opy-cli`). Both operate on `.opy` source only. No Workshop backend, catalog, Node, or OverPy is required or invoked. @@ -17,7 +17,7 @@ points never disagree about a project's verdict. Resolution stops at the Workshop-independent Opy HIR semantic model ([`hir::Program`]). There is no Workshop emission step. -## Library API (`opy_frontend::tooling`) +## Library API (`opy_rs::tooling`) ```rust pub fn check(source: &str, main_path: &str, root: &Path) -> CheckOutcome @@ -37,7 +37,7 @@ pub struct CheckOutcome { the file registry to `(file id, path, line/col)`. * `PostCompileHook`: the declared `#!postCompileHook` script (root-relative path plus directive span), present only when the source declared one and - the project checked clean. It is a declaration record only — the frontend + the project checked clean. It is a declaration record only — the source implementation never executes the hook (execution is lowering-dependent, issue #8). `SemanticModel` wraps the resolved program and answers queries: @@ -57,7 +57,7 @@ Symbols are indexed per binding: a `subroutine NAME` declaration and a `def NAME():` definition of the same name are separate entries, and call sites are attached to both. Rules are listed but are not symbols (rule names are not name-resolvable identifiers in OPY). `Constant` is a declared -binding kind in the contract; the current frontend produces no constant +binding kind in the contract; the current source implementation produces no constant declarations (custom enums fold instead). Source provenance: the file registry maps every span's file id to its path. @@ -107,7 +107,7 @@ semantic-resolution diagnostics follow the compile contract and report the first error. `check` and `compile` agree on the verdict; only the parse-stage reporting depth differs. -## Support-matrix accessor (`opy_frontend::support`) +## Support-matrix accessor (`opy_rs::support`) `compatibility/support-matrix.json` is the repository's machine-readable support state source (merged with the evidence base, PR #10) and is consumed @@ -121,7 +121,7 @@ crate rebuilds when the file changes), parsed once, and exposed as slices * `categories()`, `declared_states()`, `summary()`: declared surface -The five declared states (`planned`, `frontend-supported`, +The five declared states (`planned`, `source-supported`, `semantic-supported`, `lowering-dependent`, `end-to-end-supported`) are documented in the matrix itself. Workshop-dependent items stay `lowering-dependent`; nothing here approximates them. @@ -134,7 +134,7 @@ opy-cli check --format json # machine JSON result/diagnos opy-cli inspect # resolved model as JSON on stdout opy-cli support [--json] [] # embedded matrix (or slice) as JSON opy-cli completion bash|zsh|fish|powershell # static completion from the command model -opy-cli version # crate + frontend protocol identity +opy-cli version # crate + source implementation protocol identity ``` Exit codes: `0` clean/success, `1` diagnostics found, `2` usage or I/O @@ -173,13 +173,13 @@ stdout. * `def NAME():` bodies resolve, but calls resolve only against `subroutine NAME` declarations; a def-only subroutine call is an `unknown-action` - diagnostic (existing frontend resolution contract; tracked as frontend + diagnostic (existing source implementation resolution contract; tracked as source implementation follow-up). * Custom enums fold to constants in the HIR (reference behavior); enum declarations are queryable through `SemanticModel::enums`, not the HIR declaration list. * Workshop emission, decompilation, settings-section emission, and locale data are `lowering-dependent` (see the support matrix). The native - differential suite (`crates/opy-frontend/tests/differential.rs`, merged in + differential suite (`crates/opy-rs/tests/differential.rs`, merged in PR #13) consumes this pipeline end-to-end in `cargo test` against the recorded oracle snapshots. diff --git a/docs/opy/trivia-retention-policy.md b/docs/opy/trivia-retention-policy.md index b286a3f..795be7d 100644 --- a/docs/opy/trivia-retention-policy.md +++ b/docs/opy/trivia-retention-policy.md @@ -1,8 +1,8 @@ # Trivia and Source-Provenance Retention Policy Status: accepted policy. Issue #3 acceptance. -Scope: what the OPY frontend retains from authored source and what it -intentionally discards, for the Workshop-independent frontend surface +Scope: what the OPY source implementation retains from authored source and what it +intentionally discards, for the Workshop-independent source implementation surface ## Policy @@ -11,14 +11,14 @@ intentionally discards, for the Workshop-independent frontend surface | Authored identifiers (declaration and reference spellings) | Yes | CST nodes carry the authored text; the semantic model preserves names exactly | | Line comments (`# …`) and block comments (`/* … */`) | **No** | The lexer discards comments before tokenization (they never enter the token stream) | | Whitespace and indentation | No (reconstructed deterministically) | The CST stores statements/blocks, not original indentation | -| Source spans | Yes | 1-based line/column spans per token and CST node; `FrontendError` diagnostics carry spans; the file registry maps span file ids to paths | +| Source spans | Yes | 1-based line/column spans per token and CST node; `OpyError` diagnostics carry spans; the file registry maps span file ids to paths | | File provenance | Yes | Preprocess `FileRecord` per file (id + path); HIR `SourceFile` entries; spans are attributed across include boundaries | | Macro/define expansion provenance | Yes | `#!define` expansions carry the define's span; diagnostics attribute to authored and expansion sites | | Settings blocks | No (consumed pre-lexing) | Parsed into the typed settings payload; source layout not retained | ## Rationale -The declared frontend surface is analysis-oriented: parsing, semantic +The declared source implementation surface is analysis-oriented: parsing, semantic resolution, diagnostics, inspection, and validated source *editing* (which operates on authored source ranges, not on regenerated files). Comment/trivia retention exists to support byte-stable source *regeneration* and diff --git a/docs/overpy-support.md b/docs/overpy-support.md new file mode 100644 index 0000000..357cde1 --- /dev/null +++ b/docs/overpy-support.md @@ -0,0 +1,65 @@ +# OverPy support + +This is the canonical, human-readable compatibility contract for `opy-rs`. +The detailed inventories linked here are part of the same contract. + +## Reference and audit boundary + +| Field | Value | +| --- | --- | +| OverPy package | `9.7.10` | +| Content commit | `889d9749d1def17f146548cbddb94ea1ab015847` (`v9.7.10`) | +| Repository | | +| Registry integrity | `sha512-oX17nauJcPTaKIrRFY/rD0Rl8atqFUVv9Hg2TKH+A68/fC8+ZO344Mkd1A/Y0oOVp1hr5tktMBjzMEDDnMEYUw==` | +| Audited language | `en-US` | + +The inventory was audited from the pinned upstream tree, from outside the +`opy-rs` implementation: the upstream README and public API declaration; +`src/compiler/` grammar, preprocessing, compiler, translation and decompiler +surfaces; `src/data/opy/` keyword, annotation, builtin, member, module, macro +and preprocessing registries; `src/data/` Workshop domains; upstream compile, +decompile, CLI and QuickJS tests; and the pinned executable oracle. Existing +`opy-rs` fixtures, HIR names, support matrix entries and issue lists were used +only to determine the second column, never to construct the audited set. + +## Status vocabulary + +Only these public states are used: + +- `✅ Supported` — the claimed user-visible behavior works within the notes. +- `🚧 Coming soon` — the pinned capability is recognized, but current behavior + is incomplete. +- `❌ Unsupported` — the capability is outside the current contract. + +“Supported” is an end-to-end claim for the stated row. Parsing a construct or +having a name in a manifest is not enough to make a compilation row green. + +## Audited capability summary + +| Area | Status | Detailed inventory | +| --- | --- | --- | +| Source syntax, literals and expressions | 🚧 Coming soon | [syntax and project composition](overpy-support/syntax-and-projects.md) | +| Assignments, declarations, rules and control flow | 🚧 Coming soon | [syntax and project composition](overpy-support/syntax-and-projects.md) | +| Builtins, member functions, constants and contextual domains | 🚧 Coming soon | [callables and domains](overpy-support/callables-and-domains.md) | +| Preprocessing, includes, modules and macros | 🚧 Coming soon | [syntax and project composition](overpy-support/syntax-and-projects.md) | +| Strings, translations and custom-game settings | 🚧 Coming soon | [syntax and project composition](overpy-support/syntax-and-projects.md) | +| Compiler directives, optimization and post-compile hooks | 🚧 Coming soon | [tooling and backend](overpy-support/tooling-and-backend.md) | +| Standalone compiler and CLI | ✅ Supported | [tooling and backend](overpy-support/tooling-and-backend.md) | +| Workshop-to-OPY decompilation | ❌ Unsupported | [tooling and backend](overpy-support/tooling-and-backend.md) | + +The summary is intentionally conservative: the audited upstream surface is +larger than the currently evidenced `opy-rs` surface. Detailed rows make gaps +explicit instead of hiding them in a category-level green row. + +## Contract maintenance + +`compatibility/support-matrix.json` is retained as **internal engineering +metadata** for fixture relationships, provenance and implementation tracking. +It is not a public inventory and its internal states are not public support +states. `docs/opy/support-matrix.md` is retained as historical context and +must not introduce another public status vocabulary. + +The next step is a separate exhaustive conformance issue driven by the audited +leaf identities in these documents. This issue does not turn the inventory +into a fixed feature-count assertion or silently convert known gaps into +passing cases. diff --git a/docs/overpy-support/callables-and-domains.md b/docs/overpy-support/callables-and-domains.md new file mode 100644 index 0000000..2c76525 --- /dev/null +++ b/docs/overpy-support/callables-and-domains.md @@ -0,0 +1,63 @@ +# OverPy audited inventory: callables and domains + +Source: pinned OverPy `9.7.10`, content commit +`889d9749d1def17f146548cbddb94ea1ab015847`. The external callable registries +are `src/data/opy/functions.ts`, `memberFunctions.ts`, `constants.ts`, +`modules.ts`, and `macros.ts`; Workshop registries are `src/data/actions.ts`, +`values.ts`, `constants.ts`, `heroes.ts`, `maps.ts`, `gamemodes.ts`, +`localizedStrings.ts`, and `customGameSettings.ts`. + +The upstream registries are the audited inventory source and are not copied +into `opy-rs`. Each callable contract has a spelling, receiver (if any), +ordered arguments, argument type/domain, optional/default behavior, return +behavior, and dispatch rule. + +## Standalone functions and operators + +| Feature / representative leaf | Status | Audited contract | +| --- | --- | --- | +| `abs(value)` | ✅ Supported | One numeric value; numeric result. | +| `len(arrayOrString)` | ✅ Supported | One array/string value; integer result. | +| `range(stop)` / `range(start, stop[, step])` | ✅ Supported | Optional start and step have distinct defaults. | +| `wait(duration[, reevaluation])` | ✅ Supported | Reevaluation has an optional default. | +| `raiseToPower(base, exponent)` | ✅ Supported | Two numeric arguments in order; value operation. | +| `sorted(array[, key])` | ✅ Supported | Optional lambda key; element/index binder is contextual. | +| `all(array)` / `any(array)` | ✅ Supported | One boolean-array value. | +| `random.randint(min, max)` | 🚧 Coming soon | Two inclusive integer bounds; integer result. | +| `random.uniform(min, max)` | 🚧 Coming soon | Two float bounds; float result. | +| `random.choice(array)` | 🚧 Coming soon | One array; returns an element or supplied non-array value. | +| `random.shuffle(array)` | 🚧 Coming soon | One array; returns a copied array. | +| `_(contextOrString[, string])` | 🚧 Coming soon | One-argument and two-argument modes differ. | + +## Receiver/member functions + +| Feature / representative leaf | Status | Audited contract | +| --- | --- | --- | +| `array.append(value)` | ✅ Supported | Array receiver; mutating; arrays are extended. | +| `array.concat(value)` | ✅ Supported | Array receiver; returns a copy. | +| `array.filter(lambda)` | ✅ Supported | Lambda result selects elements; optional index binder. | +| `array.map(lambda)` | ✅ Supported | Lambda result replaces each element. | +| `array.all([lambda])` / `array.any([lambda])` | ✅ Supported | Optional lambda defaults to element truthiness. | +| `array[index]` and `array.slice(start, count)` | ✅ Supported | Indexing and slicing have different arguments. | +| `string.format(...)` | 🚧 Coming soon | Variadic formatting remains incomplete. | +| `player.setStatusEffect(player, assister, status, duration)` | 🚧 Coming soon | Receiver plus four ordered explicit arguments. | +| `vector.x`, `.y`, `.z` | ✅ Supported | Property-like vector access; numeric result. | +| `self` in member macros | 🚧 Coming soon | Dispatch target is the macro receiver. | + +## Constants, enums and contextual dispatch + +| Feature | Status | Notes | +| --- | --- | --- | +| `Hero`, `Map`, `Gamemode`, `Team`, `Slot`, `Color`, `Button` domains | 🚧 Coming soon | Membership and spelling are domain-specific. | +| `Vector.UP/DOWN/LEFT/RIGHT/FORWARD/BACKWARD` | ✅ Supported | Constants are separate from arbitrary vectors. | +| `Math.PI`, `Math.E`, `Math.INFINITY`, `Math.EPSILON` | 🚧 Coming soon | Numeric constants are distinct leaves. | +| User enum assignment and inferred increments | ✅ Supported | Separate from Workshop catalog domains. | +| Contextual `None`/reevaluation enum dispatch | 🚧 Coming soon | `ChaseTimeReeval`, `ChaseRateReeval` and `Invis` differ. | +| Alias resolution (`getCurrentHero`, `hasStatusEffect`, `ChaseReeval`) | ✅ Supported | Non-contextual and call-context aliases differ. | + +The pinned `functions.ts`, `actions.ts`, `memberFunctions.ts` and `values.ts` +registries contain the complete callable surface. The audit keeps families +separate because action/value position, receiver type, defaults, overloads and +return behavior differ. A name in the current internal manifest is evidence +for `opy-rs` only; it does not expand this inventory or make an incomplete +callable green. diff --git a/docs/overpy-support/syntax-and-projects.md b/docs/overpy-support/syntax-and-projects.md new file mode 100644 index 0000000..dc4f9b3 --- /dev/null +++ b/docs/overpy-support/syntax-and-projects.md @@ -0,0 +1,72 @@ +# OverPy audited inventory: syntax and project composition + +Source: pinned OverPy `9.7.10`, content commit +`889d9749d1def17f146548cbddb94ea1ab015847`. The source surfaces used are +`README.md`, `src/compiler/tokenizer.ts`, `parser.ts`, `astParser.ts`, +`src/data/opy/keywords.ts`, `annotations.ts`, `preprocessing.ts`, +`modules.ts`, `macros.ts`, and the upstream files under `src/tests/`. + +## Lexical and expression surface + +| Feature | Status | Notes | +| --- | --- | --- | +| `#` line comments and `/* ... */` block comments | ✅ Supported | Source parsing is covered by the native pipeline and corpus. | +| Identifiers, indentation and rule/subroutine blocks | ✅ Supported | Includes `rule "name":` and `def name():`. | +| Boolean, integer, float and `null` literals | ✅ Supported | Numeric edge cases remain conformance work. | +| Strings, escaped strings and implicit concatenation | ✅ Supported | String modifiers are separate rows. | +| f-string/interpolated strings | ✅ Supported | Supported formatting subset is fixture-covered. | +| String modifiers `f`, `w`, `l`, `b`, `c`, `t` | ✅ Supported | Each modifier is a distinct lexical form. | +| Array literals and indexing | ✅ Supported | Includes nested arrays. | +| Dictionary literals and keyed access | 🚧 Coming soon | Source analysis exists; full compilation is incomplete. | +| List comprehensions | ✅ Supported | Mapping and filtering are separate behaviors. | +| `lambda` with element/index binders | ✅ Supported | Valid positions are contextual. | +| Member access, calls and postfix expressions | ✅ Supported | Receiver and dispatch checks are contract-sensitive. | +| `del` array element statement | 🚧 Coming soon | Audited upstream keyword; compilation support is incomplete. | +| Conditional value `a if condition else b` | 🚧 Coming soon | Distinct from statement `if`. | +| `in` and `not in` membership | ✅ Supported | String containment uses `strContains`. | +| Arithmetic, comparison, boolean and unary operators | ✅ Supported | Augmented forms are separate rows below. | +| `++` and `--` postfix modifiers | 🚧 Coming soon | Audited upstream operator surface. | +| `0x`/`0X` hexadecimal literals | ✅ Supported | Case variants are one semantic capability. | + +## Assignments and declarations + +| Feature | Status | Notes | +| --- | --- | --- | +| Simple assignment `=` | ✅ Supported | Global, player and indexed forms differ at lowering. | +| `+=`, `-=`, `*=`, `/=`, `%=` | ✅ Supported | Each spelling is independently audited. | +| `**=` augmented assignment | ✅ Supported | Separate from `**`; uses Raise To Power. | +| `min=` and `max=` modification forms | 🚧 Coming soon | Recognized by the audit; Workshop support is not claimed. | +| `globalvar name [index]` | ✅ Supported | Explicit and implicit index forms are distinct. | +| `playervar name [index]` | ✅ Supported | Explicit and implicit index forms are distinct. | +| Variable initializer `globalvar/playervar name = value` | ✅ Supported | Constant-zero behavior is observable. | +| `enum` declarations and inferred member values | ✅ Supported | Contextual enum use is separate. | +| `macro` constants and function macros | ✅ Supported | Member/default-parameter forms are separate contracts. | +| `def` subroutines, calls and `return` | ✅ Supported | Upstream subroutines have no parameters or returns. | + +## Rules, control flow and project composition + +| Feature | Status | Notes | +| --- | --- | --- | +| Rule events: `global`, `eachPlayer`, team/hero/slot domains | ✅ Supported | Event and domain arguments are distinct. | +| `@Condition` and multiple conditions | ✅ Supported | | +| `@Name`, `@Disabled`, `@Delimiter`, `@NewPage`, `@SuppressWarnings` | ✅ Supported | Each annotation has independent effects. | +| `if` / `elif` / `else` statements | ✅ Supported | Inline conditional values are separate. | +| `for ... in range(start, stop, step)` | ✅ Supported | Global/player binders are separate. | +| `while` and `do ... while` loops | ✅ Supported | Distinct entry-condition behavior. | +| `switch` / `case` / `default` | ✅ Supported | Fall-through and `break` are separate. | +| `break` in loops and switch arms | ✅ Supported | | +| `continue` in loops | 🚧 Coming soon | Upstream keyword exists; end-to-end support is incomplete. | +| `goto`, labels and dynamic `loc+` targets | 🚧 Coming soon | Audited from keyword registry and `src/tests/gotos.opy`. | +| `pass` and `return` statements | ✅ Supported | Context restrictions remain conformance work. | +| `#!include` root-relative composition | ✅ Supported | Missing files and cycles have distinct failures. | +| Nested include closure and main-file selection | ✅ Supported | Project behavior is not inferred from one-file tests. | + +## Settings, strings and translations + +| Feature | Status | Notes | +| --- | --- | --- | +| `settings { ... }` custom-game-settings block | 🚧 Coming soon | Typed source representation exists; complete behavior is incomplete. | +| Schema keys, enum values and map/hero list settings | 🚧 Coming soon | Audited against upstream schema/data. | +| `#!translations` and `.po` translation sources | 🚧 Coming soon | Declaration and output lifecycle are separate. | +| `_`, `__`, `___` translation functions | 🚧 Coming soon | One- and two-argument modes differ. | +| Localized output language selection | 🚧 Coming soon | Upstream supports all in-game languages. | diff --git a/docs/overpy-support/tooling-and-backend.md b/docs/overpy-support/tooling-and-backend.md new file mode 100644 index 0000000..531f11b --- /dev/null +++ b/docs/overpy-support/tooling-and-backend.md @@ -0,0 +1,47 @@ +# OverPy audited inventory: tooling and backend behavior + +Source: pinned OverPy `9.7.10`, content commit +`889d9749d1def17f146548cbddb94ea1ab015847`. Evidence surfaces are the +upstream README, `overpy.d.ts`, `cli.js`, compiler/decompiler sources, +`runTests.mjs`, `runCliTests.mjs`, QuickJS fixtures and the executable oracle. + +## Preprocessing, macros and hooks + +| Feature | Status | Notes | +| --- | --- | --- | +| `#!define` object/function macros and `#!undef` | ✅ Supported | Expansion, precedence and recursion are distinct checks. | +| `#!allowMacroRedeclaration` | 🚧 Coming soon | Changes duplicate-definition failure behavior. | +| `#!mainFile`, `#!include`, `#!excludeVariablesInCompilation` | ✅ Supported | Selection and output filtering have separate effects. | +| Optimization controls (`#!enableOptimizations`, `#!disableOptimizations`, `#!optimize*`) | 🚧 Coming soon | Recognition is not backend-effect support. | +| Replacement directives (`#!replace0By*`, team/string replacements) | 🚧 Coming soon | Each replacement target has its own output contract. | +| `#!rulePrefix` and `#!rulePrefixTemplate` | 🚧 Coming soon | Prefix text and placeholders are separate. | +| `#!extension` and extension-point accounting | 🚧 Coming soon | Output metadata is part of the contract. | +| `macro name(params)` function/constant macros | ✅ Supported | Defaults, keywords and member macros differ. | +| `__script__` JavaScript macros | 🚧 Coming soon | QuickJS return ABI and limits are observable. | +| `#!postCompileHook` | 🚧 Coming soon | Parsing is not execution against final Workshop text. | + +## Compilation, CLI and API + +| Feature | Status | Notes | +| --- | --- | --- | +| Standalone `.opy` compiler library | ✅ Supported | Supported within the documented source/compile scope. | +| CLI compile/check invocation and structured diagnostics | ✅ Supported | Exit behavior and source attribution are contractual. | +| Upstream JS `compile(content, language, rootPath, mainFileName)` API | 🚧 Coming soon | API shape audited; Rust parity is incomplete. | +| Compile metadata: variables, subroutines, warnings, translations, element count | 🚧 Coming soon | Fields have independent completeness requirements. | +| Localized Workshop text and custom settings emission | 🚧 Coming soon | Canonical Workshop semantics remain in `workshop-rs`. | +| Observable optimization/replacement effects | 🚧 Coming soon | Formatting is not a target unless observable. | + +## Decompilation and round trips + +| Feature | Status | Notes | +| --- | --- | --- | +| `decompileAllRules` Workshop-to-OPY reconstruction | ❌ Unsupported | Outside the current `opy-rs` contract. | +| `decompileActions` and `decompileConditions` | ❌ Unsupported | Same boundary as full decompilation. | +| Workshop settings decompilation | ❌ Unsupported | No claim of recovering original source abstractions. | +| Compile/decompile round trip preserving source identity | ❌ Unsupported | Comments, macros, names and formatting are not promised. | + +The upstream source is GPL-3.0-only and is used as an external audit +reference/oracle. Its implementation and data are not copied into `opy-rs`. +The exhaustive conformance follow-up should derive stable leaf cases from the +audited registries, preserve negative behavior, and compare observable +semantics rather than Workshop formatting or internal compiler structure.