diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index 479d7ee..eb3a29a 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-130-horizontal-facing-angle", + "nativeStatus": "success", + "classification": "match", + "comparison": "semantic-wir", + "evidence": [ + "oracle:synthetic/issue-130-horizontal-facing-angle/oracle.json", + "provenance:synthetic/issue-130-horizontal-facing-angle/fixture.json", + "test:opy-rs::compiler::integration_tests::issue_130_horizontal_facing_angle::horizontal_facing_angle_member_matches_the_pinned_canonical_wir" + ], + "owner": "opy-rs#130", + "note": "The catalog-backed eventPlayer.getHorizontalFacingAngle() member value lowers to canonical WIR equivalent to the pinned oracle.", + "semanticEquivalent": true + }, { "fixture": "synthetic/issue-131-spec-visibility", "nativeStatus": "success", @@ -454,9 +468,9 @@ "provenance:real-world/overpy-client-to-server/fixture.json" ], "owner": "opy-rs#88", - "note": "The chained ternary and isDummy regressions now lower; the full real-world project remains outside the #38 compiler baseline and currently stops at the next unsupported member (`getHorizontalFacingAngle`).", + "note": "The chained ternary, isDummy, and getHorizontalFacingAngle regressions now lower; the full real-world project remains outside the #38 compiler baseline and currently stops at the next unknown value (`horizontalAngleOfDirection`).", "failureClass": "frontend", - "diagnosticCode": "unknown-member" + "diagnosticCode": "unknown-value" }, { "fixture": "real-world/overpy-cronch", diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index 7e0896f..0511a18 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -33,13 +33,14 @@ {"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 source implementation regression."}, + {"fixture": "synthetic/issue-130-horizontal-facing-angle", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-130-horizontal-facing-angle/oracle.json", "provenance:synthetic/issue-130-horizontal-facing-angle/fixture.json", "test:opy-rs::compiler::integration_tests::issue_130_horizontal_facing_angle::horizontal_facing_angle_member_matches_the_pinned_canonical_wir"], "note": "The catalog-backed eventPlayer.getHorizontalFacingAngle() member value resolves through the OPY manifest and matches the pinned canonical WIR."}, {"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."}, {"fixture": "real-world/overpy-santa", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-santa/oracle.json", "regression:real-world/overpy-santa/fixture.json"], "note": "The player-variable range binder now resolves; the full project advances to the next unsupported multiline conditional expression at santa.opy:359."}, {"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-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, isDummy, and getHorizontalFacingAngle regressions now resolve; the full project remains an explicit known gap at its next unsupported construct."}, {"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."}, diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index a3d7bdf..38976b1 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-130-horizontal-facing-angle` | #130 catalog-backed `eventPlayer.getHorizontalFacingAngle()` member-value lowering | | `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 | diff --git a/compatibility/fixtures/real-world/6v6-adjustments/fixture.json b/compatibility/fixtures/real-world/6v6-adjustments/fixture.json index 64b8ec7..8e0ad1d 100644 --- a/compatibility/fixtures/real-world/6v6-adjustments/fixture.json +++ b/compatibility/fixtures/real-world/6v6-adjustments/fixture.json @@ -141,5 +141,15 @@ }, "runtimeSeconds": 9.7, "schemaVersion": 1, - "source": "main.opy" + "source": "main.opy", + "regressions": [ + { + "id": "real-world/6v6-adjustments/get-horizontal-facing-angle", + "source": "regressions/get-horizontal-facing-angle.opy", + "derivedFrom": "heroes/juno/orbital_ray.opy", + "expectedReferenceStatus": "success", + "kind": "minimized-regression", + "provenance": "oracle:real-world/6v6-adjustments/oracle.json; source paths 20-24 in heroes/juno/orbital_ray.opy" + } + ] } diff --git a/compatibility/fixtures/real-world/6v6-adjustments/regressions/get-horizontal-facing-angle.opy b/compatibility/fixtures/real-world/6v6-adjustments/regressions/get-horizontal-facing-angle.opy new file mode 100644 index 0000000..f5475fa --- /dev/null +++ b/compatibility/fixtures/real-world/6v6-adjustments/regressions/get-horizontal-facing-angle.opy @@ -0,0 +1,5 @@ +globalvar horizontalAngle + +rule "horizontal facing angle residual": + @Event eachPlayer + horizontalAngle = eventPlayer.getHorizontalFacingAngle() diff --git a/compatibility/fixtures/real-world/overpy-client-to-server/fixture.json b/compatibility/fixtures/real-world/overpy-client-to-server/fixture.json index 009d6df..005a366 100644 --- a/compatibility/fixtures/real-world/overpy-client-to-server/fixture.json +++ b/compatibility/fixtures/real-world/overpy-client-to-server/fixture.json @@ -42,6 +42,14 @@ "expectedReferenceStatus": "success", "kind": "minimized-regression", "provenance": "oracle:real-world/overpy-client-to-server/oracle.json; source path 42" + }, + { + "id": "real-world/overpy-client-to-server/get-horizontal-facing-angle", + "source": "regressions/get-horizontal-facing-angle.opy", + "derivedFrom": "clientToServer.opy", + "expectedReferenceStatus": "success", + "kind": "minimized-regression", + "provenance": "oracle:real-world/overpy-client-to-server/oracle.json; source paths 46, 58, 60, 62, 64" } ] } diff --git a/compatibility/fixtures/real-world/overpy-client-to-server/regressions/get-horizontal-facing-angle.opy b/compatibility/fixtures/real-world/overpy-client-to-server/regressions/get-horizontal-facing-angle.opy new file mode 100644 index 0000000..f5475fa --- /dev/null +++ b/compatibility/fixtures/real-world/overpy-client-to-server/regressions/get-horizontal-facing-angle.opy @@ -0,0 +1,5 @@ +globalvar horizontalAngle + +rule "horizontal facing angle residual": + @Event eachPlayer + horizontalAngle = eventPlayer.getHorizontalFacingAngle() diff --git a/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/fixture.json b/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/fixture.json new file mode 100644 index 0000000..40d746a --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/fixture.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "id": "synthetic/issue-130-horizontal-facing-angle", + "category": "synthetic", + "features": [ + "semantics/receiver-members", + "compilation/catalog-backed-member-values" + ], + "source": "source.opy", + "expectedStatus": "success", + "provenance": { + "kind": "minimized-regression", + "origin": "wrightkit/opy-rs#130 acceptance reproduction", + "license": "AGPL-3.0-or-later", + "redistributable": true + } +} diff --git a/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/oracle.json b/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/oracle.json new file mode 100644 index 0000000..857282e --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/oracle.json @@ -0,0 +1,33 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "variables {\n global:\n 0: horizontalAngle\n}\n\nrule (\"horizontal facing angle\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(horizontalAngle, Horizontal Facing Angle Of(Event Player));\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: horizontalAngle\n}\n\nrule (\"horizontal facing angle\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(horizontalAngle, Horizontal Facing Angle Of(Event Player));\n }\n}\n\n", + "workshopSha256": "cfa82e04854ba3836ac997c475a095afa044b8f5c1ca803fa32f3c1b909c35c9" + }, + "fixture": "synthetic/issue-130-horizontal-facing-angle", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "114a0e69c9c49618e3233a5b76eb32948edd0a1478a1d24303db5f910343c949" + } + ], + "sha256": "b70bd3106b5293de1ec9408833a8e1a8744579238ddd0f5400246826c3d5f2dd", + "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-130-horizontal-facing-angle/source.opy b/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/source.opy new file mode 100644 index 0000000..3270395 --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-130-horizontal-facing-angle/source.opy @@ -0,0 +1,5 @@ +globalvar horizontalAngle + +rule "horizontal facing angle": + @Event eachPlayer + horizontalAngle = eventPlayer.getHorizontalFacingAngle() diff --git a/crates/opy-rs/src/compiler/integration_tests.rs b/crates/opy-rs/src/compiler/integration_tests.rs index f7be810..9b46e93 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_130_horizontal_facing_angle.rs"] +mod issue_130_horizontal_facing_angle; #[path = "tests/issue_131_spec_visibility.rs"] mod issue_131_spec_visibility; #[path = "tests/issue_42_oracle.rs"] diff --git a/crates/opy-rs/src/compiler/tests/issue_130_horizontal_facing_angle.rs b/crates/opy-rs/src/compiler/tests/issue_130_horizontal_facing_angle.rs new file mode 100644 index 0000000..e7f6e9f --- /dev/null +++ b/crates/opy-rs/src/compiler/tests/issue_130_horizontal_facing_angle.rs @@ -0,0 +1,99 @@ +//! Canonical-WIR lowering evidence for issue #130. + +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-130-horizontal-facing-angle") +} + +#[test] +fn horizontal_facing_angle_member_matches_the_pinned_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("compiler initializes") + .compile_hir(&hir) + .expect("getHorizontalFacingAngle lowers to canonical WIR"); + + let oracle: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(dir.join("oracle.json")).expect("oracle is readable"), + ) + .expect("oracle is valid JSON"); + let oracle_wir = workshop_rs::parser::parse( + oracle["compile"]["workshop"] + .as_str() + .expect("oracle workshop text is present"), + &Catalog::builtin().expect("catalog is available"), + &Locale::new("en-US"), + ) + .expect("oracle workshop text parses"); + assert!(equivalent(&artifact.wir, &oracle_wir)); + + let rule = artifact + .wir + .rules + .get(workshop_rs::wir::RuleId::from_index(0)) + .expect("fixture has one rule"); + let Action::SetGlobalVariable { + variable, value, .. + } = artifact + .wir + .actions + .get(rule.actions[0]) + .expect("rule captures the member value") + else { + panic!("expected direct global assignment"); + }; + assert_eq!( + artifact + .wir + .global_variables + .get(*variable) + .expect("global variable") + .name, + "horizontalAngle" + ); + let Value::Call { name, args } = &artifact + .wir + .values + .get(*value) + .expect("horizontal facing angle value") + .value + else { + panic!("expected catalog member value call"); + }; + assert_eq!(name, "getHorizontalFacingAngle"); + assert_eq!(args.len(), 1); + assert!(matches!( + &artifact + .wir + .values + .get(args[0]) + .expect("receiver value") + .value, + Value::EventPlayer + )); +} + +#[test] +fn horizontal_facing_angle_arity_failure_keeps_source_provenance() { + let source = "rule \"r\":\n @Event eachPlayer\n @Condition eventPlayer.getHorizontalFacingAngle(1) == 0\n"; + let error = crate::compile(source, "source.opy", Path::new(".")) + .expect_err("member arity must be rejected"); + assert_eq!(error.code, "invalid-arity"); + assert_eq!( + error + .span + .expect("diagnostic is source-attributed") + .start + .line, + 3 + ); +} diff --git a/crates/opy-rs/src/manifest/data/manifest.json b/crates/opy-rs/src/manifest/data/manifest.json index e220783..9e40bbc 100644 --- a/crates/opy-rs/src/manifest/data/manifest.json +++ b/crates/opy-rs/src/manifest/data/manifest.json @@ -641,6 +641,16 @@ "params": [], "receiver": "Player" }, + { + "catalogId": "getHorizontalFacingAngle", + "evidence": [ + "get-horizontal-facing-angle" + ], + "id": "getHorizontalFacingAngle", + "kind": "memberValue", + "params": [], + "receiver": "Player" + }, { "catalogId": "isDummy", "evidence": [ diff --git a/crates/opy-rs/src/manifest/probes/get-horizontal-facing-angle.opy b/crates/opy-rs/src/manifest/probes/get-horizontal-facing-angle.opy new file mode 100644 index 0000000..e67cd70 --- /dev/null +++ b/crates/opy-rs/src/manifest/probes/get-horizontal-facing-angle.opy @@ -0,0 +1,5 @@ +globalvar horizontalAngle + +rule "r": + @Event eachPlayer + horizontalAngle = eventPlayer.getHorizontalFacingAngle() diff --git a/crates/opy-rs/src/manifest/probes/probes.json b/crates/opy-rs/src/manifest/probes/probes.json index b55bc14..8ec40e5 100644 --- a/crates/opy-rs/src/manifest/probes/probes.json +++ b/crates/opy-rs/src/manifest/probes/probes.json @@ -134,6 +134,13 @@ "sha256": "f8cc098038f525941ebccac6fddaed85de3972804c76f97438606a3dee101901", "source": "generic-builtins.opy" }, + { + "expect": "success", + "id": "get-horizontal-facing-angle", + "outputSha256": "e14857154460461eb274daa9853e02578cff3b85e445eccc218f1cf212c1f322", + "sha256": "0a333d935710b4de21ad1a2af0809cc519fdffd1be533539e7b8ffd0a0fefb2e", + "source": "get-horizontal-facing-angle.opy" + }, { "diagnosticContains": "Unknown function 'setMoveSpeed'", "expect": "failure", diff --git a/crates/opy-rs/tests/differential.rs b/crates/opy-rs/tests/differential.rs index 1c64d22..6d765b5 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-130-horizontal-facing-angle", + true, + "Issue #130 catalog-backed eventPlayer.getHorizontalFacingAngle() member value; canonical WIR lowering is constrained by the dedicated compiler test.", + ); resolve( &mut cases, "synthetic/issue-131-spec-visibility", @@ -470,7 +476,7 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { &mut cases, "real-world/overpy-client-to-server", Some("parse-error"), - "the chained ternary and isDummy regressions resolve; the full project reaches the next unsupported member (`getHorizontalFacingAngle`). Gap: reference accepts, native rejects (documented).", + "the chained ternary, isDummy, and getHorizontalFacingAngle regressions resolve; the full project reaches the next unknown value (`horizontalAngleOfDirection`). Gap: reference accepts, native rejects (documented).", ); diagnostic( &mut cases,