From c30821e03348825b2dd723e6562c4603767f3172 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Wed, 2 Sep 2026 16:57:56 +0800 Subject: [PATCH] fix(opy): converge semantic failure frontiers Move invalid range binders and four-dimensional assignments to the source semantic boundary, update conformance ownership, and retain canonical-WIR residuals for later work. Fixes #162 --- compatibility/compiler-expectations.json | 20 +++--- compatibility/conformance-manifest.json | 26 +++++--- compatibility/conformance.py | 2 + compatibility/differential-expectations.json | 4 +- compatibility/fixtures/README.md | 4 +- compatibility/support-matrix.json | 6 +- compatibility/tests/test_diff.py | 25 -------- .../opy-rs/src/compiler/integration_tests.rs | 2 + .../src/compiler/tests/issue_145_lowering.rs | 21 ------- .../src/compiler/tests/issue_162_semantic.rs | 62 +++++++++++++++++++ .../tests/issue_65_player_variable.rs | 16 ----- crates/opy-rs/src/lower.rs | 42 +++++++++++-- crates/opy-rs/support-matrix.json | 6 +- crates/opy-rs/tests/differential.rs | 10 +-- 14 files changed, 143 insertions(+), 103 deletions(-) create mode 100644 crates/opy-rs/src/compiler/tests/issue_162_semantic.rs diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index be9ef4e..53a7e4c 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -310,12 +310,12 @@ "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" + "test:opy-rs::compiler::integration_tests::issue_162_semantic::semantic_boundary_errors_are_reported_before_lowering" ], - "owner": "opy-rs#65", - "note": "The pinned oracle and native source boundary reject a non-variable range binder with source-attributed diagnostics.", - "failureClass": "integration", - "diagnosticCode": "unsupported-integration-surface" + "owner": "opy-rs#162", + "note": "The pinned oracle and native source semantic boundary reject a non-variable range binder with a source-attributed diagnostic before canonical lowering.", + "failureClass": "frontend", + "diagnosticCode": "invalid-range-binder" }, { "fixture": "synthetic/issue-113-is-dummy", @@ -791,12 +791,12 @@ "evidence": [ "oracle:synthetic/issue-60-4d-negative/oracle.json", "provenance:synthetic/issue-60-4d-negative/fixture.json", - "test:opy-rs::compiler::integration_tests::issue_145_lowering::four_dimensional_indexed_assignment_is_rejected_at_the_pinned_boundary" + "test:opy-rs::compiler::integration_tests::issue_162_semantic::semantic_boundary_errors_are_reported_before_lowering" ], - "owner": "opy-rs#145", - "note": "The pinned OverPy implementation rejects four-dimensional indexed assignments; native lowering preserves this boundary with a source-attributed integration diagnostic.", - "failureClass": "integration", - "diagnosticCode": "unsupported-integration-surface" + "owner": "opy-rs#162", + "note": "The pinned OverPy implementation rejects four-dimensional indexed assignments; native source semantics now preserves that boundary before canonical lowering.", + "failureClass": "frontend", + "diagnosticCode": "four-dimensional-assignment" }, { "fixture": "synthetic/issue-33-f-string", diff --git a/compatibility/conformance-manifest.json b/compatibility/conformance-manifest.json index d966e1b..b5306b8 100644 --- a/compatibility/conformance-manifest.json +++ b/compatibility/conformance-manifest.json @@ -217,7 +217,7 @@ "id": "callable-signatures", "claim": "Catalog-backed callable signatures accept the documented argument and return shapes.", "probeKinds": ["positive", "contextual"], - "probes": ["synthetic/chase-condition-agentlab", "synthetic/chase-keywords", "synthetic/receiver-calls"] + "probes": ["synthetic/chase-condition-agentlab", "synthetic/receiver-calls"] }, { "id": "receiver-dispatch", @@ -229,7 +229,7 @@ "id": "enum-domain-context", "claim": "Enum and keyword domains are resolved in the semantic context that consumes them.", "probeKinds": ["positive", "contextual"], - "probes": ["synthetic/chase-enums", "synthetic/chase-keywords", "synthetic/issue-131-spec-visibility"] + "probes": ["synthetic/issue-131-spec-visibility"] }, { "id": "aliases-and-hud-surface", @@ -239,24 +239,22 @@ }, { "id": "semantic-boundary-errors", - "claim": "Invalid binders and duplicate declarations stop at a semantic diagnostic frontier.", + "claim": "Invalid binders and source-owned assignment boundaries stop at a semantic diagnostic frontier.", "probeKinds": ["negative"], - "probes": ["synthetic/issue-65-invalid-binder", "synthetic/issue-29-invalid"] + "probes": ["synthetic/issue-65-invalid-binder", "synthetic/issue-60-4d-negative"] } ], "probeFixtures": [ "synthetic/chase-condition-agentlab", - "synthetic/chase-enums", - "synthetic/chase-keywords", "synthetic/issue-113-is-dummy", "synthetic/issue-114-hud-subheader", "synthetic/issue-130-horizontal-facing-angle", "synthetic/issue-131-spec-visibility", "synthetic/issue-65-invalid-binder", + "synthetic/issue-60-4d-negative", "synthetic/issue-65-player-range", "synthetic/receiver-calls", - "synthetic/receiver-playervar", - "synthetic/issue-29-invalid" + "synthetic/receiver-playervar" ] }, { @@ -292,6 +290,12 @@ "probeKinds": ["positive"], "probes": ["synthetic/issue-113-is-dummy", "synthetic/issue-114-hud-subheader", "synthetic/issue-130-horizontal-facing-angle", "synthetic/issue-131-spec-visibility"] }, + { + "id": "semantic-to-wir-boundary", + "claim": "Source-semantic callable and enum forms are compared at the canonical-WIR boundary.", + "probeKinds": ["positive", "contextual"], + "probes": ["synthetic/chase-enums", "synthetic/chase-keywords"] + }, { "id": "settings-lowering", "claim": "Settings declarations participate in canonical-WIR comparison.", @@ -330,6 +334,8 @@ "synthetic/issue-114-hud-subheader", "synthetic/issue-130-horizontal-facing-angle", "synthetic/issue-131-spec-visibility", + "synthetic/chase-enums", + "synthetic/chase-keywords", "synthetic/settings", "census/workshop-feature-census" ] @@ -353,7 +359,7 @@ "id": "parse-frontier", "claim": "Parser failures identify the first unsupported or malformed syntactic construct.", "probeKinds": ["negative"], - "probes": ["synthetic/diagnostics", "synthetic/issue-28-invalid-syntax", "synthetic/issue-33-lambda-negative", "synthetic/issue-59-embedded-postfix-negative", "synthetic/issue-59-postfix-negative"] + "probes": ["synthetic/diagnostics", "synthetic/issue-28-invalid-syntax", "synthetic/issue-29-invalid", "synthetic/issue-33-lambda-negative", "synthetic/issue-59-embedded-postfix-negative", "synthetic/issue-59-postfix-negative"] }, { "id": "preprocess-frontier", @@ -365,7 +371,7 @@ "id": "semantic-frontier", "claim": "Semantic failures identify the first invalid member, placement, or binder construct.", "probeKinds": ["negative", "contextual"], - "probes": ["real-world/6v6-adjustments", "synthetic/issue-29-invalid", "synthetic/issue-47-do-while-invalid-placement", "synthetic/issue-65-invalid-binder"] + "probes": ["real-world/6v6-adjustments", "synthetic/issue-47-do-while-invalid-placement", "synthetic/issue-65-invalid-binder"] }, { "id": "diagnostic-provenance", diff --git a/compatibility/conformance.py b/compatibility/conformance.py index 9f2ff46..942396d 100644 --- a/compatibility/conformance.py +++ b/compatibility/conformance.py @@ -26,6 +26,8 @@ "script-not-found": "preprocess", "w_already_imported": "preprocess", "do-while-placement": "semantic", + "invalid-range-binder": "semantic", + "four-dimensional-assignment": "semantic", } FRONTIER_CONSTRUCTS = { "lambda-context": "parse-error", diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index aa720ae..09d52e1 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -53,12 +53,12 @@ {"fixture": "synthetic/issue-46-primitives", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-46-primitives/oracle.json", "implementation-invariant:opy-rs::compiler-primitive-lowering", "test:opy-rs::compiler-issue-46-native-wir-equivalence"], "note": "The pinned oracle records assignment and modification lowering (including **= and single-level indexed forms), value expressions, array indexing (including firstOf index-0 normalization), not-comparison negation, implicit default variables at fixed slots, and non-null variable initializers; #145 additionally covers null-default omission, float-zero initialization, and nested indexed writes. The compiler test compares native lowered WIR directly with the parsed oracle WIR and asserts structural equivalence."}, {"fixture": "synthetic/issue-46-unsupported", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-46-unsupported/oracle.json", "implementation-invariant:opy-rs::compiler-literal-dict-lookup"], "note": "The source implementation and pinned oracle compile literal dict hits and misses; native lowering folds the selected key to its value or Null. Dictionary-indexed assignment targets remain a separate canonical-WIR boundary."}, {"fixture": "synthetic/issue-60-nested-index", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-60-nested-index/oracle.json", "provenance:synthetic/issue-60-nested-index/fixture.json", "test:opy-rs::compiler::integration_tests::issue_145_lowering::nested_indexed_assignments_match_the_pinned_oracle"], "note": "Nested global indexed assignments, including compound modification and three-level replacement, are rebuilt through canonical array slices and match the pinned oracle WIR."}, - {"fixture": "synthetic/issue-60-4d-negative", "nativeStatus": "success", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:synthetic/issue-60-4d-negative/oracle.json", "provenance:synthetic/issue-60-4d-negative/fixture.json", "test:opy-rs::compiler::integration_tests::issue_145_lowering::four_dimensional_indexed_assignment_is_rejected_at_the_pinned_boundary"], "note": "The frontend intentionally resolves the source so the compiler can preserve the pinned four-dimensional assignment rejection; the compiler-level negative contract is checked separately."}, + {"fixture": "synthetic/issue-60-4d-negative", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-60-4d-negative/oracle.json", "provenance:synthetic/issue-60-4d-negative/fixture.json", "test:opy-rs::compiler::integration_tests::issue_162_semantic::semantic_boundary_errors_are_reported_before_lowering"], "note": "The pinned OverPy implementation and native source semantics reject four-dimensional indexed assignments at the same semantic failure frontier."}, {"fixture": "synthetic/issue-59-postfix-assignment", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-59-postfix-assignment/oracle.json", "implementation-invariant:opy-rs::compiler-postfix-assignment", "test:opy-rs::compiler-issue-59-postfix-oracle-equivalence"], "note": "The pinned oracle accepts postfix ++/-- assignments on global, player, and single-level indexed variables; native HIR resolves the forms and the compiler matches the canonical WIR."}, {"fixture": "synthetic/issue-59-postfix-negative", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-59-postfix-negative/oracle.json", "implementation-invariant:opy-rs::postfix-assignment-diagnostics"], "note": "The pinned oracle rejects prefix ++; native reports the stable source-attributed parse-error diagnostic, while prefix --x remains valid consecutive unary-minus syntax."}, {"fixture": "synthetic/issue-59-embedded-postfix-negative", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-59-embedded-postfix-negative/oracle.json", "implementation-invariant:opy-rs::postfix-assignment-diagnostics"], "note": "The pinned oracle independently rejects the embedded postfix form; native reports the stable source-attributed parse-error diagnostic."}, {"fixture": "synthetic/issue-65-player-range", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/issue-65-player-range/oracle.json", "provenance:synthetic/issue-65-player-range/fixture.json", "test:opy-rs::compiler::integration_tests::issue_65_player_variable::player_variable_range_binder_matches_the_pinned_oracle"], "note": "The pinned hostPlayer.I range binder lowers to the existing canonical For Player Variable WIR contract and matches the oracle."}, - {"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-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_162_semantic::semantic_boundary_errors_are_reported_before_lowering"], "note": "The pinned oracle and native source semantics reject a non-variable range binder with a source-attributed diagnostic before canonical lowering."}, {"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."}, diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index 3f3281e..a29b836 100644 --- a/compatibility/fixtures/README.md +++ b/compatibility/fixtures/README.md @@ -58,11 +58,11 @@ repository: | `issue-46-unsupported` | #145 literal dictionary lookup probe retained from #46: the compiler folds literal key hits and misses to the oracle's selected value or `Null`; dictionary-indexed assignment targets remain a separate boundary | | `issue-59-postfix-assignment` | #59 oracle-backed postfix `++`/`--` assignment probe for global, player, and single-level indexed variables; the snapshot constrains native lowering through canonical WIR equivalence | | `issue-60-nested-index` | #145 oracle-backed nested indexed-assignment probe for two- and three-level global targets, including compound modification | -| `issue-60-4d-negative` | #145 pinned negative probe preserving OverPy's structured rejection of four-dimensional indexed assignments | +| `issue-60-4d-negative` | #162 pinned negative probe preserving OverPy's structured rejection of four-dimensional indexed assignments at the source semantic boundary | | `issue-59-postfix-negative` | #59 pinned negative probe for rejected prefix `++` with a stable source-attributed parse diagnostic; prefix `--x` remains valid consecutive unary-minus syntax | | `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-65-invalid-binder` | #162 pinned negative probe for a non-variable range binder with a stable source-attributed semantic 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 | diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index ca5999f..24c086c 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -676,11 +676,11 @@ "test:opy-rs::compiler-primitive-lowering", "test:opy-rs::compiler::integration_tests::issue_59_oracle::issue_59_postfix_assignments_match_the_pinned_oracle", "test:opy-rs::compiler::integration_tests::issue_145_lowering::nested_indexed_assignments_match_the_pinned_oracle", - "test:opy-rs::compiler::integration_tests::issue_145_lowering::four_dimensional_indexed_assignment_is_rejected_at_the_pinned_boundary", + "test:opy-rs::compiler::integration_tests::issue_162_semantic::semantic_boundary_errors_are_reported_before_lowering", "test:opy-rs::compiler::integration_tests::issue_145_lowering::null_initializers_are_dropped_but_float_zero_is_preserved", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #46/#59/#60/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level and nested indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Nested targets are rebuilt through canonical array slices, while four-dimensional assignments preserve the pinned structured rejection boundary; null and integer-zero declaration initializers are omitted while float zero remains observable. Direct global/player modifications carry full statement and target-variable span provenance; indexed forms lower to canonical Call actions, which carry only the statement-level source span (the canonical WIR Call shape has no separate target-span field)." + "notes": "Issue #46/#59/#60/#145/#162. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level and nested indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Nested targets are rebuilt through canonical array slices, while four-dimensional assignments are rejected by source semantics at the pinned diagnostic frontier before lowering; null and integer-zero declaration initializers are omitted while float zero remains observable. Direct global/player modifications carry full statement and target-variable span provenance; indexed forms lower to canonical Call actions, which carry only the statement-level source span (the canonical WIR Call shape has no separate target-span field)." }, { "id": "compilation/opy-implicit-default-variables", @@ -737,7 +737,7 @@ "test:opy-rs::compiler-issue-47-residual-native-wir-gaps", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #47/#65. If/elif/else, while, global- and player-variable range-for, do-while expansion, source-ordered switch fallthrough/default, and direct loop/switch/do-while break lower into canonical workshop-rs WIR. Player-variable binders preserve their receiver and source span through HIR and use the existing For Player Variable contract. Switch offsets and do-while distances use workshop-rs v0.1.16 canonical emitted action widths; direct, conditional, and nested do-while shapes are pinned and oracle-equivalent. Residual native-WIR differences in switch-break and switch-target shapes remain explicit compiler gaps, while nested conditional switch break and multi-break switch cases remain stable source-attributed unsupported-integration-surface diagnostics because OPY has no lossless lowering for those source shapes." + "notes": "Issue #47/#65/#162. If/elif/else, while, global- and player-variable range-for, do-while expansion, source-ordered switch fallthrough/default, and direct loop/switch/do-while break lower into canonical workshop-rs WIR. Player-variable binders preserve their receiver and source span through HIR and use the existing For Player Variable contract; non-variable range binders are rejected by source semantics before lowering. Switch offsets and do-while distances use workshop-rs v0.1.16 canonical emitted action widths; direct, conditional, and nested do-while shapes are pinned and oracle-equivalent. Residual native-WIR differences in switch-break and switch-target shapes remain explicit compiler gaps, while nested conditional switch break and multi-break switch cases remain stable source-attributed unsupported-integration-surface diagnostics because OPY has no lossless lowering for those source shapes." }, { "id": "compilation/workshop-lowering", diff --git a/compatibility/tests/test_diff.py b/compatibility/tests/test_diff.py index 43005f2..5a1a019 100644 --- a/compatibility/tests/test_diff.py +++ b/compatibility/tests/test_diff.py @@ -119,31 +119,6 @@ def test_reference_success_native_failure_is_not_match(self): self.assertEqual(report_result["status"], "unexpected-divergence") self.assertTrue(report_result["referenceGap"]) - def test_declared_reference_gap_is_reported_as_known_gap(self): - oracle_path = ( - COMPATIBILITY_DIR - / "fixtures" - / "synthetic" - / "issue-60-4d-negative" - / "oracle.json" - ) - result = json.loads(oracle_path.read_text(encoding="utf-8")) - result["compile"]["status"] = "success" - result["compile"]["exitCode"] = 0 - result["compile"]["diagnostics"] = [] - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - self.write_result(root, result) - report_result = diff.compare_fixture( - COMPATIBILITY_DIR / "fixtures", - "synthetic/issue-60-4d-negative", - root, - None, - ) - self.assertEqual(report_result["status"], "known-gap") - self.assertTrue(report_result["referenceGap"]) - self.assertIn("compile-status", report_result["regressionStages"]) - def test_diagnostic_difference_is_regression(self): result = copy.deepcopy(self.oracle) result["compile"]["diagnostics"] = [ diff --git a/crates/opy-rs/src/compiler/integration_tests.rs b/crates/opy-rs/src/compiler/integration_tests.rs index 6d85638..d90e153 100644 --- a/crates/opy-rs/src/compiler/integration_tests.rs +++ b/crates/opy-rs/src/compiler/integration_tests.rs @@ -26,6 +26,8 @@ mod issue_144_builtin_surface; mod issue_145_lowering; #[path = "tests/issue_161_preprocessing.rs"] mod issue_161_preprocessing; +#[path = "tests/issue_162_semantic.rs"] +mod issue_162_semantic; #[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_145_lowering.rs b/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs index a850ab1..066399a 100644 --- a/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs +++ b/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs @@ -95,27 +95,6 @@ fn nested_indexed_assignments_match_the_pinned_oracle() { assert_fixture_matches_oracle("issue-60-nested-index"); } -#[test] -fn four_dimensional_indexed_assignment_is_rejected_at_the_pinned_boundary() { - let dir = fixture_dir("issue-60-4d-negative"); - let source = std::fs::read_to_string(dir.join("source.opy")).expect("source must be readable"); - let hir = crate::compile(&source, "source.opy", &dir).expect("fixture must resolve"); - let error = match Compiler::new() - .expect("released workshop contract must load") - .compile_hir(&hir) - { - Ok(_) => panic!("four-dimensional assignment must remain rejected"), - Err(error) => error, - }; - let oracle: serde_json::Value = serde_json::from_str( - &std::fs::read_to_string(dir.join("oracle.json")).expect("oracle must be readable"), - ) - .expect("oracle must parse"); - assert_eq!(oracle["compile"]["status"], "failure"); - assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); - assert_eq!(error.diagnostic.span.as_ref().unwrap().start.line, 5); -} - #[test] fn translated_implicit_subroutine_fixture_matches_the_pinned_oracle() { assert_fixture_matches_oracle("issue-31-positive"); diff --git a/crates/opy-rs/src/compiler/tests/issue_162_semantic.rs b/crates/opy-rs/src/compiler/tests/issue_162_semantic.rs new file mode 100644 index 0000000..33f4408 --- /dev/null +++ b/crates/opy-rs/src/compiler/tests/issue_162_semantic.rs @@ -0,0 +1,62 @@ +//! Source-semantic failure-frontier evidence for issue #162. + +use std::path::Path; + +use crate::{CompileFailureClass, CompileStatus, Compiler}; +use workshop_rs::catalog::Locale; + +#[test] +fn semantic_boundary_errors_are_reported_before_lowering() { + let cases = [ + ( + "invalid-range-binder.opy", + "rule \"invalid binder\":\n @Event global\n for 1 in range(3):\n pass\n", + "invalid-range-binder", + "Expected variable for 1st argument", + 3, + 9, + ), + ( + "four-dimensional-assignment.opy", + "globalvar nested\n\nrule \"four-dimensional indexed assignment\":\n @Event global\n nested[0][0][0][0] = 1\n", + "four-dimensional-assignment", + "Cannot assign to 4d array", + 5, + 5, + ), + ]; + let compiler = Compiler::new().expect("released Workshop contract must load"); + + for (source_name, source, code, message, line, col) in cases { + let report = compiler.compile_source_report_with_locale( + source, + source_name, + Path::new("."), + &Locale::new("en-US"), + ); + assert_eq!( + report.compile.status, + CompileStatus::Failure, + "{source_name}" + ); + assert_eq!( + report.compile.failure_class, + Some(CompileFailureClass::Frontend), + "{source_name}" + ); + let diagnostic = report + .compile + .diagnostics + .first() + .unwrap_or_else(|| panic!("missing diagnostic for {source_name}")); + assert_eq!(diagnostic.code, code, "{source_name}"); + assert!(diagnostic.message.contains(message), "{source_name}"); + let span = diagnostic + .span + .as_ref() + .unwrap_or_else(|| panic!("missing source span for {source_name}")); + assert_eq!(span.path, source_name, "{source_name}"); + assert_eq!(span.start.line, line, "{source_name}"); + assert_eq!(span.start.col, col, "{source_name}"); + } +} diff --git a/crates/opy-rs/src/compiler/tests/issue_65_player_variable.rs b/crates/opy-rs/src/compiler/tests/issue_65_player_variable.rs index 91438af..24a1bbc 100644 --- a/crates/opy-rs/src/compiler/tests/issue_65_player_variable.rs +++ b/crates/opy-rs/src/compiler/tests/issue_65_player_variable.rs @@ -79,19 +79,3 @@ fn player_variable_range_binder_matches_the_pinned_oracle() { Value::Call { ref name, ref args } if name == "hostPlayer" && args.is_empty() )); } - -#[test] -fn invalid_range_binder_has_a_stable_source_diagnostic() { - let dir = fixture_dir("issue-65-invalid-binder"); - let source = std::fs::read_to_string(dir.join("source.opy")).expect("source must be readable"); - let hir = crate::compile(&source, "source.opy", &dir).expect("fixture must resolve to HIR"); - let error = match Compiler::new() - .expect("released workshop contract must load") - .compile_hir(&hir) - { - Ok(_) => panic!("non-variable range binders must be rejected"), - Err(error) => error, - }; - assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); - assert_eq!(error.diagnostic.span.unwrap().start.line, 3); -} diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index fcd6aa1..9c685c7 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -813,11 +813,20 @@ impl Lowerer { target, value, span, - } => HirStmt::Assign { - target: Box::new(self.lower_expr(target, macro_params, CallPosition::Value)), - value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)), - span: Some(span.into()), - }, + } => { + if indexed_expr_depth(target) >= 4 { + self.error_at( + "four-dimensional-assignment", + "Cannot assign to 4d array".to_string(), + target.span(), + ); + } + HirStmt::Assign { + target: Box::new(self.lower_expr(target, macro_params, CallPosition::Value)), + value: Box::new(self.lower_expr(value, macro_params, CallPosition::Value)), + span: Some(span.into()), + } + } Stmt::If { branches, r#else, @@ -989,7 +998,21 @@ impl Lowerer { span: Some((*span).into()), }; } - self.lower_expr(variable, macro_params, CallPosition::Value) + let lowered = self.lower_expr(variable, macro_params, CallPosition::Value); + if !matches!( + lowered, + HirExpr::GlobalVar { .. } | HirExpr::PlayerVar { .. } + ) { + self.error_at( + "invalid-range-binder", + format!( + "Expected variable for 1st argument of function 'for', but got {}", + lowered.kind_name() + ), + variable.span(), + ); + } + lowered } fn lower_macro_body(&mut self, body: &[Stmt], params: &[String]) -> Vec { @@ -2307,6 +2330,13 @@ fn assignable_receiver(receiver: &Expr) -> bool { } } +fn indexed_expr_depth(expr: &Expr) -> usize { + match expr { + Expr::Index { array, .. } => 1 + indexed_expr_depth(array), + _ => 0, + } +} + impl From for HirSpan { fn from(span: Span) -> HirSpan { HirSpan { diff --git a/crates/opy-rs/support-matrix.json b/crates/opy-rs/support-matrix.json index ca5999f..24c086c 100644 --- a/crates/opy-rs/support-matrix.json +++ b/crates/opy-rs/support-matrix.json @@ -676,11 +676,11 @@ "test:opy-rs::compiler-primitive-lowering", "test:opy-rs::compiler::integration_tests::issue_59_oracle::issue_59_postfix_assignments_match_the_pinned_oracle", "test:opy-rs::compiler::integration_tests::issue_145_lowering::nested_indexed_assignments_match_the_pinned_oracle", - "test:opy-rs::compiler::integration_tests::issue_145_lowering::four_dimensional_indexed_assignment_is_rejected_at_the_pinned_boundary", + "test:opy-rs::compiler::integration_tests::issue_162_semantic::semantic_boundary_errors_are_reported_before_lowering", "test:opy-rs::compiler::integration_tests::issue_145_lowering::null_initializers_are_dropped_but_float_zero_is_preserved", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #46/#59/#60/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level and nested indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Nested targets are rebuilt through canonical array slices, while four-dimensional assignments preserve the pinned structured rejection boundary; null and integer-zero declaration initializers are omitted while float zero remains observable. Direct global/player modifications carry full statement and target-variable span provenance; indexed forms lower to canonical Call actions, which carry only the statement-level source span (the canonical WIR Call shape has no separate target-span field)." + "notes": "Issue #46/#59/#60/#145/#162. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level and nested indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Nested targets are rebuilt through canonical array slices, while four-dimensional assignments are rejected by source semantics at the pinned diagnostic frontier before lowering; null and integer-zero declaration initializers are omitted while float zero remains observable. Direct global/player modifications carry full statement and target-variable span provenance; indexed forms lower to canonical Call actions, which carry only the statement-level source span (the canonical WIR Call shape has no separate target-span field)." }, { "id": "compilation/opy-implicit-default-variables", @@ -737,7 +737,7 @@ "test:opy-rs::compiler-issue-47-residual-native-wir-gaps", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #47/#65. If/elif/else, while, global- and player-variable range-for, do-while expansion, source-ordered switch fallthrough/default, and direct loop/switch/do-while break lower into canonical workshop-rs WIR. Player-variable binders preserve their receiver and source span through HIR and use the existing For Player Variable contract. Switch offsets and do-while distances use workshop-rs v0.1.16 canonical emitted action widths; direct, conditional, and nested do-while shapes are pinned and oracle-equivalent. Residual native-WIR differences in switch-break and switch-target shapes remain explicit compiler gaps, while nested conditional switch break and multi-break switch cases remain stable source-attributed unsupported-integration-surface diagnostics because OPY has no lossless lowering for those source shapes." + "notes": "Issue #47/#65/#162. If/elif/else, while, global- and player-variable range-for, do-while expansion, source-ordered switch fallthrough/default, and direct loop/switch/do-while break lower into canonical workshop-rs WIR. Player-variable binders preserve their receiver and source span through HIR and use the existing For Player Variable contract; non-variable range binders are rejected by source semantics before lowering. Switch offsets and do-while distances use workshop-rs v0.1.16 canonical emitted action widths; direct, conditional, and nested do-while shapes are pinned and oracle-equivalent. Residual native-WIR differences in switch-break and switch-target shapes remain explicit compiler gaps, while nested conditional switch break and multi-break switch cases remain stable source-attributed unsupported-integration-surface diagnostics because OPY has no lossless lowering for those source shapes." }, { "id": "compilation/workshop-lowering", diff --git a/crates/opy-rs/tests/differential.rs b/crates/opy-rs/tests/differential.rs index f849509..6acd414 100644 --- a/crates/opy-rs/tests/differential.rs +++ b/crates/opy-rs/tests/differential.rs @@ -223,11 +223,11 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { false, "Issue #60 nested indexed assignment probe; global targets resolve and are constrained by the pinned canonical-WIR oracle.", ); - resolve( + diagnostic( &mut cases, "synthetic/issue-60-4d-negative", - false, - "Issue #60 four-dimensional negative probe; the frontend resolves the source while the compiler preserves the pinned rejection boundary.", + Some("four-dimensional-assignment"), + "Issue #60 four-dimensional negative probe; source semantics reject the assignment at a stable, source-attributed diagnostic frontier before canonical lowering.", ); diagnostic( &mut cases, @@ -250,8 +250,8 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { diagnostic( &mut cases, "synthetic/issue-65-invalid-binder", - Some("invalid-structure"), - "Issue #65 non-variable range binder is rejected by HIR validation with a stable source-attributed diagnostic.", + Some("invalid-range-binder"), + "Issue #65 non-variable range binder is rejected by source semantics with a stable source-attributed diagnostic before canonical lowering.", ); resolve( &mut cases,