diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index 4e9c555..479d7ee 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -346,6 +346,20 @@ "note": "The shared hudSubheader action lowers to canonical Create HUD Text WIR with the pinned argument order and visibility semantics.", "semanticEquivalent": true }, + { + "fixture": "synthetic/issue-131-spec-visibility", + "nativeStatus": "success", + "classification": "match", + "comparison": "semantic-wir", + "evidence": [ + "oracle:synthetic/issue-131-spec-visibility/oracle.json", + "provenance:synthetic/issue-131-spec-visibility/fixture.json", + "test:opy-rs::compiler::integration_tests::issue_131_spec_visibility::never_maps_to_visible_never_in_canonical_wir" + ], + "owner": "opy-rs#131", + "note": "The OPY SpecVisibility.NEVER spelling lowers to the canonical VISIBLE_NEVER enum member and is equivalent to the pinned oracle WIR; unrelated SpecVisibility members remain unchanged.", + "semanticEquivalent": true + }, { "fixture": "synthetic/issue-47-unsupported", "nativeStatus": "failure", @@ -468,9 +482,9 @@ "provenance:real-world/overpy-crosshair/fixture.json" ], "owner": "opy-rs#88", - "note": "The full real-world project now passes the implicit-concatenation and hudSubheader boundaries; the first remaining native gap is the unsupported SpecVisibility.NEVER enum member, while the byte-string modifier remains a separate documented gap.", + "note": "The full real-world project now passes the implicit-concatenation, hudSubheader, and SpecVisibility.NEVER boundaries; the remaining native gap is the separate unsupported hudSubtext action.", "failureClass": "frontend", - "diagnosticCode": "unknown-enum-member" + "diagnosticCode": "unknown-action" }, { "fixture": "real-world/overpy-inputhud", diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index 9e4e602..7e0896f 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -40,7 +40,7 @@ {"fixture": "real-world/overpy-cronch", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-cronch/oracle.json", "regression:real-world/overpy-cronch/fixture.json"], "note": "The postfix increment regression now resolves; the full project reaches the next unsupported createDummy action at cronch.opy:103."}, {"fixture": "real-world/overpy-broken-weapons", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-broken-weapons/oracle.json", "regression:real-world/overpy-broken-weapons/fixture.json"], "note": "The numeric-range setting regression now resolves; the full project advances to the next known native gap at `isAssemblingHeroes`."}, {"fixture": "real-world/overpy-client-to-server", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-client-to-server/oracle.json", "regression:real-world/overpy-client-to-server/fixture.json"], "note": "The chained ternary and isDummy regressions now resolve; the full project remains an explicit known gap at the next unsupported member (`getHorizontalFacingAngle`)."}, - {"fixture": "real-world/overpy-crosshair", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-crosshair/oracle.json", "regression:real-world/overpy-crosshair/fixture.json"], "note": "Implicit string concatenation and hudSubheader now lower; the first remaining native gap is the unsupported SpecVisibility.NEVER enum member, while the byte-string modifier remains a separate documented gap."}, + {"fixture": "real-world/overpy-crosshair", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-crosshair/oracle.json", "regression:real-world/overpy-crosshair/fixture.json"], "note": "The SpecVisibility.NEVER and hudSubheader boundaries now lower; the full project reaches the separate unsupported hudSubtext action."}, {"fixture": "real-world/overpy-inputhud", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-inputhud/oracle.json", "regression:real-world/overpy-inputhud/fixture.json"], "note": "Implicit string concatenation now parses; the full project advances to the separate unsupported conditional-value expression gap."}, {"fixture": "real-world/overpy-parabola", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-parabola/oracle.json", "regression:real-world/overpy-parabola/fixture.json"], "note": "The numeric enum member regression now resolves; the full project reaches the next multiline parenthesized expression gap at parabola.opy:50."}, {"fixture": "real-world/overpy-meipocalypse", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/overpy-meipocalypse/oracle.json", "provenance:real-world/overpy-meipocalypse/fixture.json"], "note": "The reference rejects the missing script hook; native rejection is retained with a different earlier diagnostic."}, @@ -58,6 +58,7 @@ {"fixture": "synthetic/issue-65-invalid-binder", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-65-invalid-binder/oracle.json", "provenance:synthetic/issue-65-invalid-binder/fixture.json", "test:opy-rs::compiler::integration_tests::issue_65_player_variable::invalid_range_binder_has_a_stable_source_diagnostic"], "note": "The pinned oracle and native HIR validation reject a non-variable range binder with source-attributed diagnostics; the compiler test also pins the canonical lowering boundary."}, {"fixture": "synthetic/issue-113-is-dummy", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-113-is-dummy/oracle.json", "provenance:synthetic/issue-113-is-dummy/fixture.json", "test:opy-rs::compiler::integration_tests::issue_113_is_dummy::is_dummy_member_lowers_to_the_catalog_value_in_canonical_wir"], "note": "The catalog-backed eventPlayer.isDummy() member predicate resolves and lowers to the canonical Workshop value call."}, {"fixture": "synthetic/issue-114-hud-subheader", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-114-hud-subheader/oracle.json", "provenance:synthetic/issue-114-hud-subheader/fixture.json", "test:opy-rs::compiler::integration_tests::issue_114_hud_subheader::hud_subheader_matches_the_pinned_canonical_wir"], "note": "The shared hudSubheader action resolves and lowers to canonical Create HUD Text WIR with its argument and visibility contract."}, + {"fixture": "synthetic/issue-131-spec-visibility", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-131-spec-visibility/oracle.json", "provenance:synthetic/issue-131-spec-visibility/fixture.json", "test:opy-rs::compiler::integration_tests::issue_131_spec_visibility::never_maps_to_visible_never_in_canonical_wir"], "note": "The OPY SpecVisibility.NEVER spelling resolves and lowers to the canonical VISIBLE_NEVER enum member."}, {"fixture": "synthetic/issue-47-control-flow", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-control-flow/oracle.json", "implementation-invariant:issue-47-control-flow-hir"], "note": "The #47 control-flow slice resolves in the source implementation and its native lowered WIR is directly equivalent to the pinned oracle."}, {"fixture": "synthetic/issue-47-unsupported", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-47-unsupported/oracle.json", "test:opy-rs::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 OPY has no lossless lowering to the canonical workshop-rs v0.1.16 control-flow contract."}, {"fixture": "synthetic/issue-47-switch-order", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-47-switch-order/oracle.json", "implementation-invariant:issue-47-authored-switch-order"], "note": "The #47 default-before-case probe preserves authored arm order and fallthrough in the source implementation, and the native lowered WIR is directly equivalent to the pinned oracle."}, diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index ab917b4..a3d7bdf 100644 --- a/compatibility/fixtures/README.md +++ b/compatibility/fixtures/README.md @@ -61,6 +61,7 @@ repository: | `issue-59-embedded-postfix-negative` | #59 pinned negative probe for the rejected embedded postfix form with a stable source-attributed parse diagnostic | | `issue-65-player-range` | #65 pinned oracle-backed player-variable range binder probe for canonical `For Player Variable` lowering | | `issue-65-invalid-binder` | #65 pinned negative probe for a non-variable range binder with a stable source-attributed diagnostic | +| `issue-131-spec-visibility` | #131 `SpecVisibility.NEVER` to canonical `VISIBLE_NEVER` enum mapping | | `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 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 | diff --git a/compatibility/fixtures/synthetic/issue-131-spec-visibility/fixture.json b/compatibility/fixtures/synthetic/issue-131-spec-visibility/fixture.json new file mode 100644 index 0000000..c6154c4 --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-131-spec-visibility/fixture.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "id": "synthetic/issue-131-spec-visibility", + "category": "synthetic", + "features": [ + "compilation/canonical-enum-mapping", + "hudSubheader spectator visibility" + ], + "source": "source.opy", + "expectedStatus": "success", + "provenance": { + "kind": "minimized-regression", + "origin": "wrightkit/opy-rs#131 acceptance reproduction", + "license": "AGPL-3.0-or-later", + "redistributable": true + } +} diff --git a/compatibility/fixtures/synthetic/issue-131-spec-visibility/oracle.json b/compatibility/fixtures/synthetic/issue-131-spec-visibility/oracle.json new file mode 100644 index 0000000..4383781 --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-131-spec-visibility/oracle.json @@ -0,0 +1,33 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "rule (\"SpecVisibility.NEVER residual\") {\n event {\n Ongoing - Global;\n }\n actions {\n Create HUD Text(All Players(All Teams), Null, Custom String(\"text\"), Null, Top, 0, Null, Color(White), Null, Visible To, Visible Never);\n }\n}\n", + "workshopExact": "rule (\"SpecVisibility.NEVER residual\") {\n event {\n Ongoing - Global;\n }\n actions {\n Create HUD Text(All Players(All Teams), Null, Custom String(\"text\"), Null, Top, 0, Null, Color(White), Null, Visible To, Visible Never);\n }\n}\n\n", + "workshopSha256": "b0c3e0907fa1d7ae3fa7b5e49d1840750e0cf63fd7fe14643825daccb5a78708" + }, + "fixture": "synthetic/issue-131-spec-visibility", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "19bafa849c45605d4d7c4e2ffb90b8ce00dcf121bd18303441a51ef22d324ad2" + } + ], + "sha256": "7ed4742471fee5c71f909d71dbbf15a3333524ec8f70817c8eef1e284d88e432", + "source": "source.opy" + }, + "oracle": { + "gitHead": "1e2688954302a402d076944b46db07efb14d7b61", + "integrity": "sha512-oX17nauJcPTaKIrRFY/rD0Rl8atqFUVv9Hg2TKH+A68/fC8+ZO344Mkd1A/Y0oOVp1hr5tktMBjzMEDDnMEYUw==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.10.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.10" + }, + "schemaVersion": 1 +} diff --git a/compatibility/fixtures/synthetic/issue-131-spec-visibility/source.opy b/compatibility/fixtures/synthetic/issue-131-spec-visibility/source.opy new file mode 100644 index 0000000..1374208 --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-131-spec-visibility/source.opy @@ -0,0 +1,3 @@ +rule "SpecVisibility.NEVER residual": + @Event global + hudSubheader(getAllPlayers(), "text", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.NEVER) diff --git a/crates/opy-rs/src/compiler/integration_tests.rs b/crates/opy-rs/src/compiler/integration_tests.rs index 1eb6627..f7be810 100644 --- a/crates/opy-rs/src/compiler/integration_tests.rs +++ b/crates/opy-rs/src/compiler/integration_tests.rs @@ -14,6 +14,8 @@ mod issue_114_hud_subheader; mod issue_115_string_continuation; #[path = "tests/issue_129_included_main_file.rs"] mod issue_129_included_main_file; +#[path = "tests/issue_131_spec_visibility.rs"] +mod issue_131_spec_visibility; #[path = "tests/issue_42_oracle.rs"] mod issue_42_oracle; #[path = "tests/issue_46_oracle.rs"] diff --git a/crates/opy-rs/src/compiler/tests/issue_131_spec_visibility.rs b/crates/opy-rs/src/compiler/tests/issue_131_spec_visibility.rs new file mode 100644 index 0000000..604b0c9 --- /dev/null +++ b/crates/opy-rs/src/compiler/tests/issue_131_spec_visibility.rs @@ -0,0 +1,87 @@ +//! Canonical WIR lowering coverage for issue #131. + +use std::path::{Path, PathBuf}; + +use crate::Compiler; +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::roundtrip::equivalent; +use workshop_rs::wir::{Action, Value}; + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../compatibility/fixtures/synthetic/issue-131-spec-visibility") +} + +fn oracle_workshop() -> String { + let value: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(fixture_dir().join("oracle.json")) + .expect("oracle must be readable"), + ) + .expect("oracle must parse"); + value["compile"]["workshop"] + .as_str() + .expect("oracle must contain Workshop output") + .to_string() +} + +#[test] +fn never_maps_to_visible_never_in_canonical_wir() { + let dir = fixture_dir(); + let source = std::fs::read_to_string(dir.join("source.opy")).expect("source is readable"); + let hir = crate::compile(&source, "source.opy", &dir).expect("source must resolve"); + let artifact = Compiler::new() + .expect("released Workshop contract must load") + .compile_hir(&hir) + .expect("SpecVisibility.NEVER must lower"); + + let catalog = Catalog::builtin().expect("catalog must load"); + let oracle = workshop_rs::parser::parse(&oracle_workshop(), &catalog, &Locale::new("en-US")) + .expect("oracle output must reparse"); + assert!( + equivalent(&artifact.wir, &oracle), + "SpecVisibility.NEVER WIR diverged from the pinned oracle\n--- native ---\n{}\n--- oracle ---\n{}", + artifact.emitted, + oracle_workshop() + ); + + let rule = artifact + .wir + .rules + .get(workshop_rs::wir::RuleId::from_index(0)) + .expect("fixture has one rule"); + let Action::Call { args, .. } = artifact.wir.actions.get(rule.actions[0]).unwrap() else { + panic!("hudSubheader must lower to a canonical action call"); + }; + assert!(matches!( + &artifact.wir.values.get(args[10]).unwrap().value, + Value::Enum { value_type, value } + if value_type == "SpecVisibility" && value == "VISIBLE_NEVER" + )); +} + +#[test] +fn unrelated_always_member_keeps_its_existing_diagnostic() { + let source = "rule \"r\":\n @Event global\n hudSubheader(getAllPlayers(), \"text\", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.ALWAYS)\n"; + let error = crate::compile(source, "source.opy", Path::new(".")) + .expect_err("SpecVisibility.ALWAYS is outside issue #131 scope"); + assert_eq!(error.code, "unknown-enum-member"); + assert_eq!( + error.message, + "enum 'SpecVisibility' has no member 'ALWAYS'" + ); +} + +#[test] +fn invalid_spec_visibility_members_keep_source_attributed_diagnostics() { + let source = "rule \"r\":\n @Event global\n hudSubheader(getAllPlayers(), \"text\", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.INVALID)\n"; + let error = + crate::compile(source, "source.opy", Path::new(".")).expect_err("invalid member must fail"); + assert_eq!(error.code, "unknown-enum-member"); + assert_eq!( + error.message, + "enum 'SpecVisibility' has no member 'INVALID'" + ); + let span = error.span.expect("diagnostic provenance"); + assert_eq!(span.start.line, 3); + assert_eq!(span.start.col, 98); +} diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index dcf03d2..a09e73c 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -1161,6 +1161,10 @@ impl Lowerer { // against the canonical Workshop catalog. if self.manifest.domain_identity(name) { let locale = Locale::new("en-US"); + let catalog_member = match (name.as_str(), member) { + ("SpecVisibility", "NEVER") => "VISIBLE_NEVER", + _ => member, + }; let canonical_member = self .catalog .enum_domain(name) @@ -1168,7 +1172,7 @@ impl Lowerer { domain .members .iter() - .find(|candidate| candidate.member == member) + .find(|candidate| candidate.member == catalog_member) .map(|candidate| candidate.member.clone()) }) .or_else(|| { diff --git a/crates/opy-rs/tests/differential.rs b/crates/opy-rs/tests/differential.rs index e5f3a0d..1c64d22 100644 --- a/crates/opy-rs/tests/differential.rs +++ b/crates/opy-rs/tests/differential.rs @@ -253,6 +253,12 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { true, "Issue #114 shared hudSubheader action; canonical WIR lowering is constrained by the dedicated compiler test.", ); + resolve( + &mut cases, + "synthetic/issue-131-spec-visibility", + true, + "Issue #131 maps SpecVisibility.NEVER to the canonical VISIBLE_NEVER member; canonical WIR lowering is constrained by the dedicated compiler test.", + ); resolve( &mut cases, "synthetic/issue-47-control-flow", @@ -469,8 +475,8 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { diagnostic( &mut cases, "real-world/overpy-crosshair", - Some("parse-error"), - "reference accepts; the native frontend rejects the `b\"…\"` byte-string modifier (baseline category 1b, legacy-quirk/demand-driven, explicit rejection). Gap: reference accepts, native rejects (documented).", + Some("unknown-action"), + "the SpecVisibility.NEVER and hudSubheader boundaries now resolve; the full project reaches the separate unsupported hudSubtext action. Gap: reference accepts, native rejects (documented).", ); diagnostic( &mut cases,