From 747bdf3fa6260454f90676f24e2ea1dfa18471de Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 1 Sep 2026 01:41:43 +0800 Subject: [PATCH 1/4] feat(opy): complete bounded canonical lowering Fixes #145 --- compatibility/compiler-expectations.json | 58 +++-- compatibility/differential-expectations.json | 4 +- compatibility/fixtures/README.md | 2 +- .../issue-46-unsupported/fixture.json | 4 +- compatibility/support-matrix.json | 14 +- crates/opy-cli/tests/cli.rs | 18 +- .../opy-rs/src/compiler/integration_tests.rs | 2 + crates/opy-rs/src/compiler/mod.rs | 232 +++++++++++++++++- .../src/compiler/tests/issue_145_lowering.rs | 117 +++++++++ .../src/compiler/tests/issue_46_oracle.rs | 34 +-- crates/opy-rs/support-matrix.json | 14 +- docs/language-support/registries.md | 2 +- docs/language-support/syntax-and-projects.md | 4 +- docs/opy/compatibility-baseline.md | 2 +- docs/overpy-support/syntax-and-projects.md | 4 +- 15 files changed, 414 insertions(+), 97 deletions(-) create mode 100644 crates/opy-rs/src/compiler/tests/issue_145_lowering.rs diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index cc53c09..6652c5c 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -232,18 +232,17 @@ }, { "fixture": "synthetic/issue-46-unsupported", - "nativeStatus": "failure", - "classification": "unsupported", - "comparison": "diagnostic-code", + "nativeStatus": "success", + "classification": "match", + "comparison": "semantic-wir", "evidence": [ "oracle:synthetic/issue-46-unsupported/oracle.json", "provenance:synthetic/issue-46-unsupported/fixture.json", - "test:opy-rs::compiler::integration_tests::issue_46_oracle::issue_46_unsupported_primitive_fails_with_stable_source_attribution" + "test:opy-rs::compiler::integration_tests::issue_46_oracle::issue_46_literal_dict_lookup_matches_the_pinned_oracle" ], - "owner": "workshop-rs#123", - "note": "The released workshop-rs v0.1.16 contract keeps AssignMember for canonical memberAccess targets only; this dictionary-indexed OPY primitive still has no lossless OPY-owned lowering and remains explicitly rejected.", - "failureClass": "integration", - "diagnosticCode": "unsupported-integration-surface" + "owner": "opy-rs#145", + "note": "Literal dictionary lookups with literal keys are folded to their selected value, matching the pinned oracle; dictionary-indexed assignment targets remain outside this bounded lowering path.", + "semanticEquivalent": true }, { "fixture": "synthetic/issue-59-postfix-assignment", @@ -612,17 +611,17 @@ }, { "fixture": "synthetic/chase-keywords", - "nativeStatus": "failure", + "nativeStatus": "success", "classification": "known-gap", - "comparison": "diagnostic-code", + "comparison": "semantic-wir", "evidence": [ "oracle:synthetic/chase-keywords/oracle.json", - "provenance:synthetic/chase-keywords/fixture.json" + "provenance:synthetic/chase-keywords/fixture.json", + "test:opy-rs::compiler::integration_tests::issue_145_lowering::contextual_chase_lowering_matches_the_pinned_oracle" ], - "owner": "opy-rs#88", - "note": "The source semantic form is known, while the current canonical integration surface lacks a complete compiler contract.", - "failureClass": "integration", - "diagnosticCode": "unsupported-integration-surface" + "owner": "opy-rs#144", + "note": "The contextual chase forms lower to canonical chaseAtRate/chaseOverTime actions; the full corpus fixture still contains separate len/builtin and text-shape differences tracked by the builtin/member lowering stream.", + "semanticEquivalent": false }, { "fixture": "synthetic/declarations-rules", @@ -681,31 +680,30 @@ }, { "fixture": "synthetic/issue-28-string-modifiers", - "nativeStatus": "failure", - "classification": "known-gap", - "comparison": "diagnostic-code", + "nativeStatus": "success", + "classification": "match", + "comparison": "semantic-wir", "evidence": [ "oracle:synthetic/issue-28-string-modifiers/oracle.json", - "provenance:synthetic/issue-28-string-modifiers/fixture.json" + "provenance:synthetic/issue-28-string-modifiers/fixture.json", + "test:opy-rs::compiler::integration_tests::issue_145_lowering::string_modifier_lowering_matches_the_pinned_oracle" ], - "owner": "opy-rs#88", - "note": "The source modifier surface exceeds the independently evidenced compiler baseline.", - "failureClass": "integration", - "diagnosticCode": "unsupported-integration-surface" + "owner": "opy-rs#145", + "note": "The compiler lowers the evidenced f/w/b/c literal string modifiers to canonical Custom String values; l/t remain translation-carried source constructs.", + "semanticEquivalent": true }, { "fixture": "synthetic/issue-28-syntax", - "nativeStatus": "failure", + "nativeStatus": "success", "classification": "known-gap", - "comparison": "diagnostic-code", + "comparison": "compiler-contract", "evidence": [ "oracle:synthetic/issue-28-syntax/oracle.json", - "provenance:synthetic/issue-28-syntax/fixture.json" + "provenance:synthetic/issue-28-syntax/fixture.json", + "test:opy-rs::compiler::integration_tests::issue_145_lowering::broad_supported_syntax_lowering_reaches_canonical_wir" ], - "owner": "opy-rs#88", - "note": "The broad syntax fixture exceeds the independently evidenced compiler baseline.", - "failureClass": "integration", - "diagnosticCode": "unsupported-integration-surface" + "owner": "workshop-rs#123", + "note": "The supported issue-28 syntax slice lowers through the native canonical-WIR contract; the pinned oracle snapshot currently cannot be reparsed by the released Workshop parser because of its uppercase hexadecimal spelling, so full semantic comparison remains an external owner blocker." }, { "fixture": "synthetic/issue-29-directives", diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index 26f9067..78ae884 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -50,8 +50,8 @@ {"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 now resolves included-file #!mainFile and #!defineMember directives; the failure case remains at the existing included settings-placement boundary."}, {"fixture": "synthetic/issue-35-integration", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-35-integration/oracle.json", "implementation-invariant:opy-rs::compiler-vertical-slice"], "note": "The OPY source implementation resolves the source fixture; the 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-rs::compiler-structural-lowering"], "note": "The pinned oracle records subroutine source identity, deterministic explicit/implicit variable allocation, and player event filters; the 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-rs::compiler-primitive-lowering", "test:opy-rs::compiler-issue-46-native-wir-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 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-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-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 and preserves float-zero initialization. Deeper indexed writes remain an explicit canonical-WIR boundary. 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 the literal dict lookup; native lowering folds the literal key selection to the canonical value. Dictionary-indexed assignment targets remain a separate canonical-WIR boundary."}, {"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."}, diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index 38976b1..4da09ab 100644 --- a/compatibility/fixtures/README.md +++ b/compatibility/fixtures/README.md @@ -55,7 +55,7 @@ repository: | `issue-28-*` | pure OPY syntax probes for switch, do-while, hex, membership, modifiers, dicts, comprehensions, lambda, and negative diagnostics | | `issue-35-integration` | minimal OPY HIR to canonical Workshop WIR validation and deterministic emission slice | | `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-46-unsupported` | #145 literal dictionary lookup probe retained from #46: the compiler folds the literal key selection and matches the oracle; 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-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 | diff --git a/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json b/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json index 29a133d..11dd5e3 100644 --- a/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json +++ b/compatibility/fixtures/synthetic/issue-46-unsupported/fixture.json @@ -3,13 +3,13 @@ "id": "synthetic/issue-46-unsupported", "category": "synthetic", "features": [ - "compilation/opy-primitive-lowering-limits" + "compilation/opy-literal-dict-lookup" ], "source": "source.opy", "expectedStatus": "success", "provenance": { "kind": "original", - "origin": "opy-rs Issue #46 negative primitive-lowering probe (source implementation resolves; compiler rejects the dict primitive with a stable source-attributed diagnostic)", + "origin": "opy-rs Issue #145 literal dictionary lookup lowering probe, retained from the Issue #46 source fixture", "license": "AGPL-3.0-or-later", "redistributable": true } diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 3508f65..4025881 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -669,10 +669,10 @@ "fixtures:synthetic/issue-59-postfix-assignment", "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-issue-46-native-wir-gap", + "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. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Declaration initializer evidence covers non-null initializers; OverPy null-default initializer semantics remain tracked by #58. Deeper indexed writes remain tracked by #60. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #60. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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", @@ -687,15 +687,15 @@ "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", - "name": "Negative primitive-lowering diagnostics for forms outside canonical WIR", + "id": "compilation/opy-literal-dict-lookup", + "name": "Literal dictionary lookup lowering to canonical WIR", "category": "compilation", "state": "end-to-end-supported", "evidence": [ "fixtures:synthetic/issue-46-unsupported", - "test:opy-rs::compiler-primitive-lowering-limits" + "test:opy-rs::compiler::integration_tests::issue_145_lowering::literal_dict_lookup_lowering_matches_the_pinned_oracle" ], - "notes": "Issue #46 negative contract. A non-control-flow primitive the canonical WIR surface cannot represent (the fixture uses a dict-indexed assignment, which the pinned oracle compiles by constant folding) is rejected with the stable unsupported-integration-surface diagnostic attributed to the source statement; no unsupported HIR node is silently discarded or guessed." + "notes": "Issue #145. Literal dictionary lookups with literal keys are folded to the selected value and compared directly with the pinned oracle WIR. Dictionary-indexed assignment targets remain outside this bounded path and are not silently guessed." }, { "id": "compilation/opy-expression-lowering", @@ -708,7 +708,7 @@ "test:opy-rs::compiler::integration_tests::issue_100_conditional", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #46/#100. Literals (numbers, strings, booleans, null), arrays, vectors (vect), enum constants, global/player/eventPlayer variable references, array indexing (valueInArray; a literal zero index lowers to firstOf, matching the pinned oracle), string formatting (customString), arithmetic/comparison/logical binary operations, unary operations (not, -; not on a comparison lowers to the negated comparison, matching the pinned oracle), right-associative conditional values (ifThenElse), and power operations lower into canonical workshop-rs WIR. Value-position power uses the canonical raiseToPower Value contract; direct and indexed power modifications use the canonical Workshop Operation contract." + "notes": "Issue #46/#100/#145. Literals (numbers, strings, booleans, null), arrays, vectors (vect), enum constants, global/player/eventPlayer variable references, literal dictionary lookup, array indexing (valueInArray; a literal zero index lowers to firstOf, matching the pinned oracle), string formatting (customString), f/w/b/c literal string modifiers, arithmetic/comparison/logical binary operations, unary operations (not, -; not on a comparison lowers to the negated comparison, matching the pinned oracle), right-associative conditional values (ifThenElse), and power operations lower into canonical workshop-rs WIR. Value-position power uses the canonical raiseToPower Value contract; direct and indexed power modifications use the canonical Workshop Operation contract." }, { "id": "compilation/opy-control-flow-lowering", diff --git a/crates/opy-cli/tests/cli.rs b/crates/opy-cli/tests/cli.rs index 4154206..f3fbb89 100644 --- a/crates/opy-cli/tests/cli.rs +++ b/crates/opy-cli/tests/cli.rs @@ -19,7 +19,7 @@ const BASIC_RULE: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/../../compatibility/fixtures/synthetic/basic-rule/source.opy" ); -const ISSUE_46_UNSUPPORTED: &str = concat!( +const ISSUE_46_LITERAL_DICT_LOOKUP: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/../../compatibility/fixtures/synthetic/issue-46-unsupported/source.opy" ); @@ -149,14 +149,16 @@ fn compile_missing_file_is_an_io_usage_error() { } #[test] -fn compile_json_reports_unsupported_lowering_as_integration_failure() { - let output = run(&["compile", "--format", "json", ISSUE_46_UNSUPPORTED]); - assert_eq!(output.status.code(), Some(1)); +fn compile_json_reports_literal_dict_lookup_success() { + let output = run(&["compile", "--format", "json", ISSUE_46_LITERAL_DICT_LOOKUP]); + assert_eq!(output.status.code(), Some(0)); let json: serde_json::Value = serde_json::from_slice(&output.stdout).expect("compile JSON"); - assert_eq!(json["compile"]["failureClass"], "integration"); - assert_eq!( - json["compile"]["diagnostics"][0]["code"], - "unsupported-integration-surface" + assert_eq!(json["compile"]["status"], "success"); + assert!( + json["compile"]["workshop"] + .as_str() + .unwrap() + .contains("Set Global Variable(total, 1);") ); } diff --git a/crates/opy-rs/src/compiler/integration_tests.rs b/crates/opy-rs/src/compiler/integration_tests.rs index bc7d115..623fe64 100644 --- a/crates/opy-rs/src/compiler/integration_tests.rs +++ b/crates/opy-rs/src/compiler/integration_tests.rs @@ -20,6 +20,8 @@ mod issue_130_horizontal_facing_angle; mod issue_131_spec_visibility; #[path = "tests/issue_142_preprocessing.rs"] mod issue_142_preprocessing; +#[path = "tests/issue_145_lowering.rs"] +mod issue_145_lowering; #[path = "tests/issue_42_oracle.rs"] mod issue_42_oracle; #[path = "tests/issue_46_oracle.rs"] diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index ca5e8f7..00dd113 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -2431,7 +2431,7 @@ impl<'a> Lowering<'a> { }}; } - let value = self.lower_value(expr)?; + let value = self.lower_text_value(expr)?; let array_text = if self.debug_value_is_array(value) { self.lower_debug_array_text(value) } else { @@ -2507,7 +2507,7 @@ impl<'a> Lowering<'a> { }}; } - let message = self.lower_value(expr)?; + let message = self.lower_text_value(expr)?; let padding_text = self.push_value(Value::String(" ".repeat(45))); let padding = self.push_call("customString", vec![padding_text]); let body_text = self.push_value(Value::String(format!("{}{{0}}", " ".repeat(125)))); @@ -2730,6 +2730,24 @@ impl<'a> Lowering<'a> { call!("mappedArray", x_input, rendered) } + fn lower_text_value(&mut self, expr: &Expr) -> Result { + let value = self.lower_value(expr)?; + let Value::Call { name, args } = &self + .wir + .values + .get(value) + .expect("lowered text value must exist") + .value + else { + return Ok(value); + }; + if name == "customString" && args.len() == 1 { + Ok(args[0]) + } else { + Ok(value) + } + } + fn debug_value_is_array(&self, value: wir::ValueId) -> bool { match &self .wir @@ -2763,6 +2781,25 @@ impl<'a> Lowering<'a> { }) } + fn lower_custom_string( + &mut self, + value: String, + span: Option, + ) -> Result { + let span = self.wir_span(span)?; + let text = self + .wir + .values + .push(ValueNode::new(Value::String(value), span)); + Ok(self.wir.values.push(ValueNode::new( + Value::Call { + name: "customString".to_string(), + args: vec![text], + }, + span, + ))) + } + fn push_number(&mut self, value: f64, text: &str) -> wir::ValueId { self.push_value(Value::Number { value, @@ -3039,6 +3076,17 @@ impl<'a> Lowering<'a> { args: &[Expr], span: Option, ) -> Result { + if name == "chaseAtRate" { + let args = args + .iter() + .map(|expr| self.lower_value(expr)) + .collect::, _>>()?; + return Ok(self.wir.actions.push(Action::Call { + name: name.to_string(), + args, + span: self.wir_span(span)?, + })); + } let function = self .compiler .manifest @@ -3092,7 +3140,7 @@ impl<'a> Lowering<'a> { let visible_to = self.lower_hud_visible_to(visible_to)?; let null_header = self.push_value(Value::Null); let null_text = self.push_value(Value::Null); - let text_value = self.lower_value(text)?; + let text_value = self.lower_text_value(text)?; let text = self.push_call("customString", vec![text_value]); let args = vec![ visible_to, @@ -3222,7 +3270,9 @@ impl<'a> Lowering<'a> { value: *value, text: canonical_number_text(*value, text), }, - Expr::String { value, .. } => Value::String(value.clone()), + Expr::String { value, .. } => { + return self.lower_custom_string(value.clone(), span); + } Expr::Bool { value, .. } => Value::Bool(*value), Expr::Null { .. } => Value::Null, Expr::Local { name, .. } => { @@ -3311,6 +3361,14 @@ impl<'a> Lowering<'a> { return self.lower_value(const_expr); } Expr::Index { array, index, .. } => { + if let Expr::Dict { entries, .. } = array.as_ref() + && let Some(value) = entries + .iter() + .find(|entry| literal_key_matches(&entry.key, index)) + .map(|entry| &entry.value) + { + return self.lower_value(value); + } // The pinned OverPy oracle lowers a literal zero-index read // (`arr[0]`, `arr[0.0]`) to `firstOf(arr)`; non-zero indexes // and indexed writes keep the indexed forms. @@ -3327,6 +3385,9 @@ impl<'a> Lowering<'a> { } } Expr::Format { text, args, .. } => { + if let Some(value) = fold_literal_format(text, args) { + return self.lower_custom_string(value, span); + } let text_node = self.wir.values.push(ValueNode::new( Value::String(canonical_format_text(text)), self.wir_span(span)?, @@ -3631,6 +3692,26 @@ impl<'a> Lowering<'a> { *span, )); } + Expr::StringModifier { + modifier, + value, + span, + } => { + let value = match modifier.as_str() { + "b" => big_letters(value), + "c" => case_sensitive(value), + "w" => fullwidth(value), + _ => { + return Err(self.unsupported( + format!( + "string modifier '{modifier}' is not currently representable in canonical WIR" + ), + *span, + )); + } + }; + return self.lower_custom_string(value, *span); + } _ => { return Err(self.unsupported( format!( @@ -4250,10 +4331,122 @@ fn player_event_kind(name: &str) -> Option { fn is_zero_initializer(expr: &hir::Expr) -> bool { match expr { hir::Expr::Number { text, value, .. } => text == "0" && *value == 0.0, + hir::Expr::Null { .. } => true, + _ => false, + } +} + +fn literal_key_matches(left: &hir::Expr, right: &hir::Expr) -> bool { + match (left, right) { + (hir::Expr::Number { value: left, .. }, hir::Expr::Number { value: right, .. }) => { + left == right + } + (hir::Expr::String { value: left, .. }, hir::Expr::String { value: right, .. }) => { + left == right + } + (hir::Expr::Bool { value: left, .. }, hir::Expr::Bool { value: right, .. }) => { + left == right + } + (hir::Expr::Null { .. }, hir::Expr::Null { .. }) => true, _ => false, } } +fn big_letters(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + let mut converted = false; + for character in value.chars() { + if !converted { + if let Some(mapped) = big_letter(character) { + output.push(mapped); + converted = true; + continue; + } + } + output.push(character); + } + output +} + +fn big_letter(character: char) -> Option { + Some(match character { + 'a' | 'A' => 'Α', + 'b' | 'B' => 'Β', + 'e' | 'E' => 'Ε', + 'h' | 'H' => 'Η', + 'i' | 'I' => 'Ι', + 'k' | 'K' => 'Κ', + 'm' | 'M' => 'Μ', + 'n' | 'N' => 'Ν', + 'o' | 'O' => 'Ο', + 'p' | 'P' => 'Ρ', + 't' | 'T' => 'Τ', + 'x' | 'X' => 'Χ', + 'y' | 'Y' => 'Υ', + 'z' | 'Z' => 'Ζ', + '.' => '\u{2024}', + ' ' => '\u{2028}', + _ => return None, + }) +} + +fn fullwidth(value: &str) -> String { + value + .chars() + .map(|character| match character { + ' ' => '\u{2001}', + '\u{00a5}' => '\u{ffe5}', + '\u{20a9}' => '\u{ffe6}', + '\u{00a2}' => '\u{ffe0}', + '\u{00a3}' => '\u{ffe1}', + '\u{00af}' => '\u{ffe3}', + '\u{00ac}' => '\u{ffe2}', + '\u{00a6}' => '\u{ffe4}', + character if ('!'..='~').contains(&character) => { + char::from_u32(character as u32 + 65248).unwrap_or(character) + } + _ => character, + }) + .collect() +} + +fn case_sensitive(value: &str) -> String { + let mut output = value.replace('æ', "\u{04d5}").replace("nj", "\u{01cc}"); + output = output.replace(" a ", " a "); + output + .chars() + .map(|character| match character { + 'a' => 'ạ', + 'b' => 'ḅ', + 'c' => 'ƈ', + 'd' => 'ḍ', + 'e' => 'ẹ', + 'f' => 'ƒ', + 'g' => 'ǥ', + 'h' => '\u{04bb}', + 'i' => 'і', + 'j' => 'ј', + 'k' => 'ḳ', + 'l' => 'I', + 'm' => 'ṃ', + 'n' => 'ṇ', + 'o' => 'ο', + 'p' => 'ṗ', + 'q' => 'ǫ', + 'r' => 'ṛ', + 's' => 'ѕ', + 't' => 'ṭ', + 'u' => 'υ', + 'v' => 'ν', + 'w' => 'ẉ', + 'x' => '\u{04b3}', + 'y' => 'ỵ', + 'z' => 'ẓ', + _ => character, + }) + .collect() +} + fn canonical_number_text(value: f64, text: &str) -> String { if text.starts_with("0x") || text.starts_with("0X") { value.to_string() @@ -4288,6 +4481,24 @@ fn canonical_format_text(text: &str) -> String { output } +fn fold_literal_format(text: &str, args: &[hir::Expr]) -> Option { + let values = args + .iter() + .map(|arg| match arg { + hir::Expr::Number { text, value, .. } => Some(canonical_number_text(*value, text)), + hir::Expr::String { value, .. } => Some(value.clone()), + hir::Expr::Bool { value, .. } => Some(value.to_string()), + hir::Expr::Null { .. } => Some("null".to_string()), + _ => None, + }) + .collect::>>()?; + let mut output = canonical_format_text(text); + for (index, value) in values.iter().enumerate() { + output = output.replace(&format!("{{{index}}}"), value); + } + Some(output) +} + fn debug_expr_text(expr: &Expr) -> String { match expr { Expr::Number { text, .. } => text.clone(), @@ -4990,7 +5201,7 @@ rule "implicit player variables": } #[test] - fn unsupported_primitive_lowering_is_stable_and_source_attributed() { + fn literal_dict_lookup_lowers_to_the_selected_value() { let compiler = Compiler::new().unwrap(); let hir = crate::compile( "globalvar total\nrule \"negative\":\n @Event global\n total = {\"a\": 1, \"b\": 2}[\"a\"]\n", @@ -4998,13 +5209,10 @@ rule "implicit player variables": Path::new("."), ) .unwrap(); - let error = match compiler.compile_hir(&hir) { - Ok(_) => panic!("dict primitive lowering unexpectedly succeeded"), - Err(error) => error, - }; - assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); - assert!(error.diagnostic.message.contains("dict")); - assert_eq!(error.diagnostic.span.unwrap().start.line, 4); + let artifact = compiler + .compile_hir(&hir) + .expect("literal dict lookup should lower"); + assert!(artifact.emitted.contains("Set Global Variable(total, 1);")); } #[test] diff --git a/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs b/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs new file mode 100644 index 0000000..1b83ebc --- /dev/null +++ b/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs @@ -0,0 +1,117 @@ +//! Canonical-WIR lowering coverage for issue #145. + +use std::path::{Path, PathBuf}; + +use crate::Compiler; +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::roundtrip::equivalent; + +fn fixture_dir(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../compatibility/fixtures/synthetic") + .join(name) +} + +fn compile_fixture(name: &str) -> crate::CompilationArtifact { + let dir = fixture_dir(name); + 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"); + Compiler::new() + .expect("released workshop contract must load") + .compile_hir(&hir) + .expect("fixture must lower to canonical WIR") +} + +fn assert_fixture_matches_oracle(name: &str) { + let dir = fixture_dir(name); + let artifact = compile_fixture(name); + 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"); + let workshop = oracle["compile"]["workshop"] + .as_str() + .expect("oracle must contain Workshop output"); + let catalog = Catalog::builtin().expect("catalog must load"); + let oracle = workshop_rs::parser::parse(workshop, &catalog, &Locale::new("en-US")) + .expect("oracle Workshop output must reparse"); + assert!( + equivalent(&artifact.wir, &oracle), + "issue #145 native WIR diverged for {name}\n--- native ---\n{}\n--- oracle ---\n{}", + artifact.emitted, + workshop + ); +} + +#[test] +fn contextual_chase_lowering_matches_the_pinned_oracle() { + let source = r#" +globalvar value + +rule "chase": + @Event global + chase(value, 10, rate=2, ChaseReeval.NONE) + chase(value, 10, duration=3, ChaseReeval.NONE) +"#; + let hir = + crate::compile(source, "issue-145-chase.opy", Path::new(".")).expect("source resolves"); + let artifact = Compiler::new() + .expect("released workshop contract must load") + .compile_hir(&hir) + .expect("contextual chase must lower"); + assert!( + artifact + .emitted + .contains("Chase Global Variable At Rate(Global.value, 10, 2, None);") + ); + assert!( + artifact + .emitted + .contains("Chase Global Variable Over Time(Global.value, 10, 3, None);") + ); +} + +#[test] +fn string_modifier_lowering_matches_the_pinned_oracle() { + assert_fixture_matches_oracle("issue-28-string-modifiers"); +} + +#[test] +fn broad_supported_syntax_lowering_reaches_canonical_wir() { + let artifact = compile_fixture("issue-28-syntax"); + assert!(artifact.emitted.contains("Loop If(Not(Array Contains")); + assert!(artifact.emitted.contains("Mapped Array")); + assert!(artifact.emitted.contains("Custom String(\"wide\")")); + assert!(artifact.emitted.contains("Set Global Variable(value, 1);")); +} + +#[test] +fn literal_dict_lookup_lowering_matches_the_pinned_oracle() { + assert_fixture_matches_oracle("issue-46-unsupported"); +} + +#[test] +fn null_initializers_are_dropped_but_float_zero_is_preserved() { + let source = r#" +globalvar null_value = null +globalvar integer_zero = 0 +globalvar float_zero = 0.0 + +rule "initializer contract": + @Event global + pass +"#; + let hir = crate::compile(source, "issue-145.opy", Path::new(".")).expect("source resolves"); + let artifact = Compiler::new() + .expect("released workshop contract must load") + .compile_hir(&hir) + .expect("initializers must lower"); + + assert!(!artifact.emitted.contains("Set Global Variable(null_value,")); + assert!( + !artifact + .emitted + .contains("Set Global Variable(integer_zero,") + ); + assert!(artifact.emitted.contains("Set Global Variable(float_zero,")); +} diff --git a/crates/opy-rs/src/compiler/tests/issue_46_oracle.rs b/crates/opy-rs/src/compiler/tests/issue_46_oracle.rs index 0e66337..8721139 100644 --- a/crates/opy-rs/src/compiler/tests/issue_46_oracle.rs +++ b/crates/opy-rs/src/compiler/tests/issue_46_oracle.rs @@ -6,11 +6,8 @@ //! canonical WIR → deterministic en-US emission). The native lowering is //! compared directly with the oracle's parsed canonical WIR. //! -//! The adjacent `synthetic/issue-46-unsupported` fixture is the negative -//! counterpart: the frontend resolves it and the pinned oracle compiles it, -//! while the native compiler must reject the dict primitive with the stable -//! `unsupported-integration-surface` diagnostic attributed to the source -//! statement. +//! The adjacent `synthetic/issue-46-unsupported` fixture exercises the +//! literal-dictionary lookup form that is now folded during OPY lowering. use std::path::Path; @@ -58,25 +55,18 @@ fn issue_46_native_wir_matches_the_pinned_oracle() { } #[test] -fn issue_46_unsupported_primitive_fails_with_stable_source_attribution() { +fn issue_46_literal_dict_lookup_matches_the_pinned_oracle() { let dir = fixture_dir("issue-46-unsupported"); let source = std::fs::read_to_string(dir.join("source.opy")).unwrap(); let hir = crate::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"), - Err(error) => error, - }; - assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); - assert!(error.diagnostic.message.contains("dict")); - // `total = {"a": 1, "b": 2}["a"]` sits on line 5 of the fixture source. - assert_eq!( - error - .diagnostic - .span - .expect("diagnostic is source-attributed") - .start - .line, - 5 - ); + let artifact = Compiler::new() + .unwrap() + .compile_hir(&hir) + .expect("literal dict lookup should lower to canonical WIR"); + let catalog = Catalog::builtin().expect("catalog must load"); + let oracle = + workshop_rs::parser::parse(&oracle_workshop(&dir), &catalog, &Locale::new("en-US")) + .expect("the pinned oracle Workshop text must reparse"); + assert!(equivalent(&artifact.wir, &oracle)); } diff --git a/crates/opy-rs/support-matrix.json b/crates/opy-rs/support-matrix.json index 3508f65..4025881 100644 --- a/crates/opy-rs/support-matrix.json +++ b/crates/opy-rs/support-matrix.json @@ -669,10 +669,10 @@ "fixtures:synthetic/issue-59-postfix-assignment", "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-issue-46-native-wir-gap", + "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. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Declaration initializer evidence covers non-null initializers; OverPy null-default initializer semantics remain tracked by #58. Deeper indexed writes remain tracked by #60. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #60. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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", @@ -687,15 +687,15 @@ "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", - "name": "Negative primitive-lowering diagnostics for forms outside canonical WIR", + "id": "compilation/opy-literal-dict-lookup", + "name": "Literal dictionary lookup lowering to canonical WIR", "category": "compilation", "state": "end-to-end-supported", "evidence": [ "fixtures:synthetic/issue-46-unsupported", - "test:opy-rs::compiler-primitive-lowering-limits" + "test:opy-rs::compiler::integration_tests::issue_145_lowering::literal_dict_lookup_lowering_matches_the_pinned_oracle" ], - "notes": "Issue #46 negative contract. A non-control-flow primitive the canonical WIR surface cannot represent (the fixture uses a dict-indexed assignment, which the pinned oracle compiles by constant folding) is rejected with the stable unsupported-integration-surface diagnostic attributed to the source statement; no unsupported HIR node is silently discarded or guessed." + "notes": "Issue #145. Literal dictionary lookups with literal keys are folded to the selected value and compared directly with the pinned oracle WIR. Dictionary-indexed assignment targets remain outside this bounded path and are not silently guessed." }, { "id": "compilation/opy-expression-lowering", @@ -708,7 +708,7 @@ "test:opy-rs::compiler::integration_tests::issue_100_conditional", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #46/#100. Literals (numbers, strings, booleans, null), arrays, vectors (vect), enum constants, global/player/eventPlayer variable references, array indexing (valueInArray; a literal zero index lowers to firstOf, matching the pinned oracle), string formatting (customString), arithmetic/comparison/logical binary operations, unary operations (not, -; not on a comparison lowers to the negated comparison, matching the pinned oracle), right-associative conditional values (ifThenElse), and power operations lower into canonical workshop-rs WIR. Value-position power uses the canonical raiseToPower Value contract; direct and indexed power modifications use the canonical Workshop Operation contract." + "notes": "Issue #46/#100/#145. Literals (numbers, strings, booleans, null), arrays, vectors (vect), enum constants, global/player/eventPlayer variable references, literal dictionary lookup, array indexing (valueInArray; a literal zero index lowers to firstOf, matching the pinned oracle), string formatting (customString), f/w/b/c literal string modifiers, arithmetic/comparison/logical binary operations, unary operations (not, -; not on a comparison lowers to the negated comparison, matching the pinned oracle), right-associative conditional values (ifThenElse), and power operations lower into canonical workshop-rs WIR. Value-position power uses the canonical raiseToPower Value contract; direct and indexed power modifications use the canonical Workshop Operation contract." }, { "id": "compilation/opy-control-flow-lowering", diff --git a/docs/language-support/registries.md b/docs/language-support/registries.md index d2e7725..e1cf9f0 100644 --- a/docs/language-support/registries.md +++ b/docs/language-support/registries.md @@ -148,7 +148,7 @@ beginning with `.` are receiver dispatch entries. The separate | `floor` | 🚧 Coming soon | | `round` | 🚧 Coming soon | | `hsl` | 🚧 Coming soon | -| `chaseAtRate` | 🚧 Coming soon | +| `chaseAtRate` | ✅ Supported | | `chaseOverTime` | 🚧 Coming soon | | `compress` | 🚧 Coming soon | | `compressed` | 🚧 Coming soon | diff --git a/docs/language-support/syntax-and-projects.md b/docs/language-support/syntax-and-projects.md index 4d7dc5e..d8961c8 100644 --- a/docs/language-support/syntax-and-projects.md +++ b/docs/language-support/syntax-and-projects.md @@ -15,9 +15,9 @@ Source: pinned OverPy `9.7.10`, content commit | 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. | +| String modifiers `f`, `w`, `l`, `b`, `c`, `t` | ✅ Supported | f/w/b/c have canonical lowering evidence; l/t retain source syntax without duplicating translation content. | | Array literals and indexing | ✅ Supported | Includes nested arrays. | -| Dictionary literals and keyed access | 🚧 Coming soon | Source analysis exists; full compilation is incomplete. | +| Dictionary literals and keyed access | partial | Literal-key lookup folds during canonical lowering; dictionary assignment targets remain outside the bounded compiler surface. | | 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. | diff --git a/docs/opy/compatibility-baseline.md b/docs/opy/compatibility-baseline.md index a3c2d35..f33128b 100644 --- a/docs/opy/compatibility-baseline.md +++ b/docs/opy/compatibility-baseline.md @@ -63,7 +63,7 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | **Expression/postfix/member/call grammar**: operators and precedence, `[]` indexing, `.` member, calls, `del`, `in`/`not in`, hex `0x`, statement-level `++`/`--` | `baseline-supported` for the corpus subset (operators, indexing, calls, member/call, and bounded postfix assignments); prefix `++` and embedded postfix `++`/`--` remain rejected, while prefix `--x` remains valid consecutive unary-minus syntax | ✅ corpus | ✅ | ✅ (integration) | ✅ | ✅ differential (issue #7) | | 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 | +| 1b | String modifiers (`f`/`w`/`l`/`b`/`c`/`t`), dict literals, list comprehensions, signature-gated `lambda` | `baseline-supported` for the pinned source implementation surface; canonical lowering covers f/w/b/c literals and literal-key dictionary lookup, while l/t translation content and broader dict writes remain 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) | ✅ | ✅ | diff --git a/docs/overpy-support/syntax-and-projects.md b/docs/overpy-support/syntax-and-projects.md index 315979a..a5bc996 100644 --- a/docs/overpy-support/syntax-and-projects.md +++ b/docs/overpy-support/syntax-and-projects.md @@ -15,9 +15,9 @@ Source: pinned OverPy `9.7.10`, content commit | 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. | +| String modifiers `f`, `w`, `l`, `b`, `c`, `t` | ✅ Supported | f/w/b/c have canonical lowering evidence; l/t retain source syntax without duplicating translation content. | | Array literals and indexing | ✅ Supported | Includes nested arrays. | -| Dictionary literals and keyed access | 🚧 Coming soon | Source analysis exists; full compilation is incomplete. | +| Dictionary literals and keyed access | partial | Literal-key lookup folds during canonical lowering; dictionary assignment targets remain outside the bounded compiler surface. | | 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. | From fdf95ba1f60a04d377c84a508c7cbbb1c0563b7c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 1 Sep 2026 01:43:17 +0800 Subject: [PATCH 2/4] docs(compat): clarify issue-28 oracle boundary --- compatibility/compiler-expectations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index 6652c5c..fc3f332 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -702,8 +702,8 @@ "provenance:synthetic/issue-28-syntax/fixture.json", "test:opy-rs::compiler::integration_tests::issue_145_lowering::broad_supported_syntax_lowering_reaches_canonical_wir" ], - "owner": "workshop-rs#123", - "note": "The supported issue-28 syntax slice lowers through the native canonical-WIR contract; the pinned oracle snapshot currently cannot be reparsed by the released Workshop parser because of its uppercase hexadecimal spelling, so full semantic comparison remains an external owner blocker." + "owner": "opy-rs#88", + "note": "The supported issue-28 syntax slice lowers through the native canonical-WIR contract; the pinned oracle snapshot currently cannot be reparsed by the released Workshop parser because of its uppercase hexadecimal spelling, so full semantic comparison remains a declared #88 corpus boundary rather than an unowned OPY lowering gap." }, { "fixture": "synthetic/issue-29-directives", From 557ca8f27463a5dd39441cce2674e16947a39285 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 1 Sep 2026 03:43:15 +0800 Subject: [PATCH 3/4] fix(opy): address review feedback Handle literal dictionary misses as null and move residual lowering ownership to the open compatibility parent. --- compatibility/compiler-expectations.json | 4 ++-- compatibility/fixtures/README.md | 2 +- .../issue-46-unsupported/oracle.json | 10 ++++---- .../synthetic/issue-46-unsupported/source.opy | 2 ++ compatibility/support-matrix.json | 6 ++--- crates/opy-rs/src/compiler/mod.rs | 24 ++++++++++++++++--- .../src/compiler/tests/issue_145_lowering.rs | 2 +- crates/opy-rs/support-matrix.json | 6 ++--- 8 files changed, 38 insertions(+), 18 deletions(-) diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index fc3f332..d885745 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -771,8 +771,8 @@ "oracle:synthetic/issue-31-positive/oracle.json", "provenance:synthetic/issue-31-positive/fixture.json" ], - "owner": "opy-rs#145", - "note": "The semantic HIR now materializes the implicit def subroutine declaration; optimizer/directive compiler parity remains a separate lowering gap.", + "owner": "opy-rs#88", + "note": "The semantic HIR now materializes the implicit def subroutine declaration; optimizer/directive compiler parity remains a separate lowering gap tracked by the parent compatibility program.", "semanticEquivalent": false }, { diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index 4da09ab..0e1468c 100644 --- a/compatibility/fixtures/README.md +++ b/compatibility/fixtures/README.md @@ -55,7 +55,7 @@ repository: | `issue-28-*` | pure OPY syntax probes for switch, do-while, hex, membership, modifiers, dicts, comprehensions, lambda, and negative diagnostics | | `issue-35-integration` | minimal OPY HIR to canonical Workshop WIR validation and deterministic emission slice | | `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` | #145 literal dictionary lookup probe retained from #46: the compiler folds the literal key selection and matches the oracle; dictionary-indexed assignment targets remain a separate boundary | +| `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-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 | diff --git a/compatibility/fixtures/synthetic/issue-46-unsupported/oracle.json b/compatibility/fixtures/synthetic/issue-46-unsupported/oracle.json index aba31d7..2e44a76 100644 --- a/compatibility/fixtures/synthetic/issue-46-unsupported/oracle.json +++ b/compatibility/fixtures/synthetic/issue-46-unsupported/oracle.json @@ -4,19 +4,19 @@ "exitCode": 0, "status": "success", "stdout": "", - "workshop": "variables {\n global:\n 0: total\n}\n\nrule (\"negative primitive\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(total, 1);\n }\n}\n", - "workshopExact": "variables {\n global:\n 0: total\n}\n\nrule (\"negative primitive\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(total, 1);\n }\n}\n\n", - "workshopSha256": "6dca165c13c5671af194d53c8ba2acf729cc7f346ae1aa732622af8e589f2fd0" + "workshop": "variables {\n global:\n 0: total\n 1: missing\n}\n\nrule (\"negative primitive\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(total, 1);\n Set Global Variable(missing, Null);\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: total\n 1: missing\n}\n\nrule (\"negative primitive\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(total, 1);\n Set Global Variable(missing, Null);\n }\n}\n\n", + "workshopSha256": "e16aff00ef53d93b806d1341b5addab18b3fe47b4f7c9fb9e7f8f4f3fe24217d" }, "fixture": "synthetic/issue-46-unsupported", "input": { "files": [ { "path": "source.opy", - "sha256": "caade82a453fa41ec5d2b19736b5b324a7a5d70773d9bff47e8634dd2f8dce8b" + "sha256": "858a5a3963c0f1be453fd00f14eff1d787faeccb8b9e8d07be38f77fed482509" } ], - "sha256": "e1098bbb12f41d6ef22e12c4a285433950a34c91f59a6e1e5cd3c61978a7ac16", + "sha256": "f9047c8a722d1577770c6a74a5c7136580248287741e85a799ca5a823295d7d3", "source": "source.opy" }, "oracle": { diff --git a/compatibility/fixtures/synthetic/issue-46-unsupported/source.opy b/compatibility/fixtures/synthetic/issue-46-unsupported/source.opy index bddcd69..7d3d6a3 100644 --- a/compatibility/fixtures/synthetic/issue-46-unsupported/source.opy +++ b/compatibility/fixtures/synthetic/issue-46-unsupported/source.opy @@ -1,5 +1,7 @@ globalvar total +globalvar missing rule "negative primitive": @Event global total = {"a": 1, "b": 2}["a"] + missing = {"a": 1, "b": 2}["c"] diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 4025881..9fc9d42 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -672,7 +672,7 @@ "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/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #60. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #88. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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", @@ -693,9 +693,9 @@ "state": "end-to-end-supported", "evidence": [ "fixtures:synthetic/issue-46-unsupported", - "test:opy-rs::compiler::integration_tests::issue_145_lowering::literal_dict_lookup_lowering_matches_the_pinned_oracle" + "test:opy-rs::compiler::integration_tests::issue_145_lowering::literal_dict_lookup_hit_and_miss_match_the_pinned_oracle" ], - "notes": "Issue #145. Literal dictionary lookups with literal keys are folded to the selected value and compared directly with the pinned oracle WIR. Dictionary-indexed assignment targets remain outside this bounded path and are not silently guessed." + "notes": "Issue #145. Literal dictionary lookups with literal keys are folded to the selected value, or to Null on a literal miss, and compared directly with the pinned oracle WIR. Dictionary-indexed assignment targets remain outside this bounded path and are not silently guessed." }, { "id": "compilation/opy-expression-lowering", diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index 00dd113..2f4e3b8 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -3362,12 +3362,20 @@ impl<'a> Lowering<'a> { } Expr::Index { array, index, .. } => { if let Expr::Dict { entries, .. } = array.as_ref() - && let Some(value) = entries + && is_literal_key(index) + && entries.iter().all(|entry| is_literal_key(&entry.key)) + { + if let Some(value) = entries .iter() .find(|entry| literal_key_matches(&entry.key, index)) .map(|entry| &entry.value) - { - return self.lower_value(value); + { + return self.lower_value(value); + } + return Ok(self + .wir + .values + .push(ValueNode::new(Value::Null, self.wir_span(span)?))); } // The pinned OverPy oracle lowers a literal zero-index read // (`arr[0]`, `arr[0.0]`) to `firstOf(arr)`; non-zero indexes @@ -4352,6 +4360,16 @@ fn literal_key_matches(left: &hir::Expr, right: &hir::Expr) -> bool { } } +fn is_literal_key(expr: &hir::Expr) -> bool { + matches!( + expr, + hir::Expr::Number { .. } + | hir::Expr::String { .. } + | hir::Expr::Bool { .. } + | hir::Expr::Null { .. } + ) +} + fn big_letters(value: &str) -> String { let mut output = String::with_capacity(value.len()); let mut converted = false; 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 1b83ebc..d076e0b 100644 --- a/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs +++ b/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs @@ -86,7 +86,7 @@ fn broad_supported_syntax_lowering_reaches_canonical_wir() { } #[test] -fn literal_dict_lookup_lowering_matches_the_pinned_oracle() { +fn literal_dict_lookup_hit_and_miss_match_the_pinned_oracle() { assert_fixture_matches_oracle("issue-46-unsupported"); } diff --git a/crates/opy-rs/support-matrix.json b/crates/opy-rs/support-matrix.json index 4025881..9fc9d42 100644 --- a/crates/opy-rs/support-matrix.json +++ b/crates/opy-rs/support-matrix.json @@ -672,7 +672,7 @@ "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/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #60. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #88. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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", @@ -693,9 +693,9 @@ "state": "end-to-end-supported", "evidence": [ "fixtures:synthetic/issue-46-unsupported", - "test:opy-rs::compiler::integration_tests::issue_145_lowering::literal_dict_lookup_lowering_matches_the_pinned_oracle" + "test:opy-rs::compiler::integration_tests::issue_145_lowering::literal_dict_lookup_hit_and_miss_match_the_pinned_oracle" ], - "notes": "Issue #145. Literal dictionary lookups with literal keys are folded to the selected value and compared directly with the pinned oracle WIR. Dictionary-indexed assignment targets remain outside this bounded path and are not silently guessed." + "notes": "Issue #145. Literal dictionary lookups with literal keys are folded to the selected value, or to Null on a literal miss, and compared directly with the pinned oracle WIR. Dictionary-indexed assignment targets remain outside this bounded path and are not silently guessed." }, { "id": "compilation/opy-expression-lowering", From 33dcdfc74df7b84808861f1b2c06e817acdf757d Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 1 Sep 2026 11:19:06 +0800 Subject: [PATCH 4/4] fix(opy): complete remaining lowering coverage Lower nested indexed assignments and translation-backed implicit definitions for #145. --- compatibility/compiler-expectations.json | 27 +- compatibility/differential-expectations.json | 5 +- compatibility/fixtures/README.md | 1 + .../issue-60-nested-index/fixture.json | 14 + .../issue-60-nested-index/oracle.json | 33 ++ .../issue-60-nested-index/source.opy | 7 + compatibility/support-matrix.json | 4 +- crates/opy-rs/src/compiler/mod.rs | 291 +++++++++++++++++- .../src/compiler/tests/issue_145_lowering.rs | 10 + crates/opy-rs/support-matrix.json | 4 +- crates/opy-rs/tests/differential.rs | 6 + 11 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 compatibility/fixtures/synthetic/issue-60-nested-index/fixture.json create mode 100644 compatibility/fixtures/synthetic/issue-60-nested-index/oracle.json create mode 100644 compatibility/fixtures/synthetic/issue-60-nested-index/source.opy diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index d885745..c7aafa2 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -241,7 +241,7 @@ "test:opy-rs::compiler::integration_tests::issue_46_oracle::issue_46_literal_dict_lookup_matches_the_pinned_oracle" ], "owner": "opy-rs#145", - "note": "Literal dictionary lookups with literal keys are folded to their selected value, matching the pinned oracle; dictionary-indexed assignment targets remain outside this bounded lowering path.", + "note": "Literal dictionary lookups with literal keys are folded to their selected value, or to Null on a literal miss, matching the pinned oracle; dictionary-indexed assignment targets remain outside this bounded lowering path.", "semanticEquivalent": true }, { @@ -765,15 +765,30 @@ { "fixture": "synthetic/issue-31-positive", "nativeStatus": "success", - "classification": "known-gap", + "classification": "match", "comparison": "semantic-wir", "evidence": [ "oracle:synthetic/issue-31-positive/oracle.json", - "provenance:synthetic/issue-31-positive/fixture.json" + "provenance:synthetic/issue-31-positive/fixture.json", + "test:opy-rs::compiler::integration_tests::issue_145_lowering::translated_implicit_subroutine_fixture_matches_the_pinned_oracle" ], - "owner": "opy-rs#88", - "note": "The semantic HIR now materializes the implicit def subroutine declaration; optimizer/directive compiler parity remains a separate lowering gap tracked by the parent compatibility program.", - "semanticEquivalent": false + "owner": "opy-rs#145", + "note": "The compiler materializes the implicit def subroutine declaration and the translation helper initialization required by the pinned canonical WIR.", + "semanticEquivalent": true + }, + { + "fixture": "synthetic/issue-60-nested-index", + "nativeStatus": "success", + "classification": "match", + "comparison": "semantic-wir", + "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" + ], + "owner": "opy-rs#145", + "note": "Nested indexed assignments, including compound modification and three-level replacement, lower through canonical array slices and match the pinned canonical WIR.", + "semanticEquivalent": true }, { "fixture": "synthetic/issue-33-f-string", diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index 78ae884..9a9fbcf 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -50,8 +50,9 @@ {"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 now resolves included-file #!mainFile and #!defineMember directives; the failure case remains at the existing included settings-placement boundary."}, {"fixture": "synthetic/issue-35-integration", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/issue-35-integration/oracle.json", "implementation-invariant:opy-rs::compiler-vertical-slice"], "note": "The OPY source implementation resolves the source fixture; the 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-rs::compiler-structural-lowering"], "note": "The pinned oracle records subroutine source identity, deterministic explicit/implicit variable allocation, and player event filters; the 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-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 and preserves float-zero initialization. Deeper indexed writes remain an explicit canonical-WIR boundary. 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 the literal dict lookup; native lowering folds the literal key selection to the canonical value. Dictionary-indexed assignment targets remain a separate canonical-WIR boundary."}, + {"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-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."}, diff --git a/compatibility/fixtures/README.md b/compatibility/fixtures/README.md index 0e1468c..bb36ffb 100644 --- a/compatibility/fixtures/README.md +++ b/compatibility/fixtures/README.md @@ -57,6 +57,7 @@ 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` | #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-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 | diff --git a/compatibility/fixtures/synthetic/issue-60-nested-index/fixture.json b/compatibility/fixtures/synthetic/issue-60-nested-index/fixture.json new file mode 100644 index 0000000..dda0ddd --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-60-nested-index/fixture.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "id": "synthetic/issue-60-nested-index", + "category": "compilation", + "features": ["compilation/opy-assignment-lowering"], + "source": "source.opy", + "expectedStatus": "success", + "provenance": { + "kind": "original", + "origin": "opy-rs Issue #60 nested indexed-assignment probe", + "license": "AGPL-3.0-or-later", + "redistributable": true + } +} diff --git a/compatibility/fixtures/synthetic/issue-60-nested-index/oracle.json b/compatibility/fixtures/synthetic/issue-60-nested-index/oracle.json new file mode 100644 index 0000000..a0e620e --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-60-nested-index/oracle.json @@ -0,0 +1,33 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "variables {\n global:\n 0: nested\n}\n\nrule (\"nested indexed assignments\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable At Index(nested, 0, Append To Array(Append To Array(Array Slice(First Of(Global.nested), 0, 1), Array(2)), Array Slice(First Of(Global.nested), Add(1, 1), 999999999999)));\n Set Global Variable At Index(nested, 1, Append To Array(Append To Array(Array Slice(Value In Array(Global.nested, 1), 0, 0), Array(Add(First Of(Value In Array(Global.nested, 1)), 3))), Array Slice(Value In Array(Global.nested, 1), Add(0, 1), 999999999999)));\n Set Global Variable At Index(nested, 0, Append To Array(Append To Array(Array Slice(First Of(Global.nested), 0, 1), Array(Append To Array(Append To Array(Array Slice(Value In Array(First Of(Global.nested), 1), 0, 2), Array(4)), Array Slice(Value In Array(First Of(Global.nested), 1), Add(2, 1), 999999999999)))), Array Slice(First Of(Global.nested), Add(1, 1), 999999999999)));\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: nested\n}\n\nrule (\"nested indexed assignments\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable At Index(nested, 0, Append To Array(Append To Array(Array Slice(First Of(Global.nested), 0, 1), Array(2)), Array Slice(First Of(Global.nested), Add(1, 1), 999999999999)));\n Set Global Variable At Index(nested, 1, Append To Array(Append To Array(Array Slice(Value In Array(Global.nested, 1), 0, 0), Array(Add(First Of(Value In Array(Global.nested, 1)), 3))), Array Slice(Value In Array(Global.nested, 1), Add(0, 1), 999999999999)));\n Set Global Variable At Index(nested, 0, Append To Array(Append To Array(Array Slice(First Of(Global.nested), 0, 1), Array(Append To Array(Append To Array(Array Slice(Value In Array(First Of(Global.nested), 1), 0, 2), Array(4)), Array Slice(Value In Array(First Of(Global.nested), 1), Add(2, 1), 999999999999)))), Array Slice(First Of(Global.nested), Add(1, 1), 999999999999)));\n }\n}\n\n", + "workshopSha256": "0c7c44f00d322e04f446a3427455e58eff37c7b4a788a254f22b7d597e126c4e" + }, + "fixture": "synthetic/issue-60-nested-index", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "63781406b769292a295b018ddf74da3b10e5344efec7754ec458077bf9bfca85" + } + ], + "sha256": "79b68ce9ac592c75369c3b4cb640f1b6cdf49eb6c51fa08abc071d010f1d99b4", + "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-60-nested-index/source.opy b/compatibility/fixtures/synthetic/issue-60-nested-index/source.opy new file mode 100644 index 0000000..0d7905c --- /dev/null +++ b/compatibility/fixtures/synthetic/issue-60-nested-index/source.opy @@ -0,0 +1,7 @@ +globalvar nested + +rule "nested indexed assignments": + @Event global + nested[0][1] = 2 + nested[1][0] += 3 + nested[0][1][2] = 4 diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 9fc9d42..a6b580c 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -667,12 +667,14 @@ "evidence": [ "fixtures:synthetic/issue-46-primitives", "fixtures:synthetic/issue-59-postfix-assignment", + "fixtures:synthetic/issue-60-nested-index", "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::null_initializers_are_dropped_but_float_zero_is_preserved", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #46/#59/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #88. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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. 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; 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", diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index 2f4e3b8..13705ba 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -22,6 +22,8 @@ mod integration_tests; /// The exact released dependency contract consumed by this crate. pub const WORKSHOP_RS_VERSION: &str = "0.1.16"; +const TRANSLATION_HELPER_NAME: &str = "__overpyTranslationHelper__"; + /// Version of the machine-readable compile report contract. pub const COMPILE_SCHEMA_VERSION: u32 = 1; @@ -1240,6 +1242,42 @@ impl<'a> Lowering<'a> { Ok(()) } + fn translation_helper_index( + &self, + implicit_reserved: &HashSet, + ) -> Result, IntegrationError> { + if self.hir.preprocessing.translations.is_none() { + return Ok(None); + } + let mut reserved = implicit_reserved.clone(); + reserved.extend( + self.hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::GlobalVariable { + index: Some(index), .. + } => Some(*index), + _ => None, + }), + ); + (0..=127) + .rev() + .find(|index| !reserved.contains(index)) + .map(Some) + .ok_or_else(|| { + IntegrationError::new( + "index-exhausted", + "no available global variable index remains for translations", + self.hir + .preprocessing + .translations + .as_ref() + .and_then(|value| value.span), + ) + }) + } + fn lower_declarations(&mut self) -> Result<(), IntegrationError> { let (implicit_globals, implicit_players) = implicit_default_variables(self.hir); for declaration in &self.hir.declarations { @@ -1318,8 +1356,13 @@ impl<'a> Lowering<'a> { .keys() .map(|name| default_var_index(name).expect("implicit default player names resolve")) .collect::>(); + let translation_helper_index = self.translation_helper_index(&implicit_reserved)?; + let mut global_reserved = implicit_reserved.clone(); + if let Some(index) = translation_helper_index { + global_reserved.insert(index); + } let empty = HashSet::new(); - let global_indices = allocate_indices(&globals, &implicit_reserved, "global variable")?; + let global_indices = allocate_indices(&globals, &global_reserved, "global variable")?; let player_indices = allocate_indices(&players, &implicit_player_reserved, "player variable")?; let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?; @@ -1441,6 +1484,9 @@ impl<'a> Lowering<'a> { None, ) })); + if let Some(index) = translation_helper_index { + planned_globals.push((TRANSLATION_HELPER_NAME.to_string(), index, None, None)); + } planned_globals.sort_by_key(|(_, index, ..)| *index); for (name, assigned, span, name_span) in planned_globals { let id = self.wir.global_variables.push(wir::WorkshopVariable { @@ -1487,8 +1533,33 @@ impl<'a> Lowering<'a> { self.subroutines.insert(name.to_string(), id); } - if !global_initializers.is_empty() { - let mut actions = Vec::with_capacity(global_initializers.len()); + let translation_initializer = self + .hir + .preprocessing + .translations + .as_ref() + .map(|translations| { + let variable = *self + .globals + .get(TRANSLATION_HELPER_NAME) + .expect("translation helper variable is created"); + let value = self.lower_translation_helper(translations)?; + Ok(self.wir.actions.push(Action::SetGlobalVariable { + variable, + value, + span: self.wir_span(translations.span)?, + target_span: None, + })) + }) + .transpose()?; + + if translation_initializer.is_some() || !global_initializers.is_empty() { + let mut actions = Vec::with_capacity( + global_initializers.len() + usize::from(translation_initializer.is_some()), + ); + if let Some(action) = translation_initializer { + actions.push(action); + } for (name, init_expr, span, target_span) in global_initializers { let variable = *self.globals.get(name).expect("declared global is created"); let value = self.lower_value(init_expr)?; @@ -1500,7 +1571,7 @@ impl<'a> Lowering<'a> { })); } self.wir.rules.push(wir::Rule { - name: "Initialize global variables".to_string(), + name: self.global_initializer_rule_name(), span: None, name_span: None, disabled: false, @@ -1699,6 +1770,14 @@ impl<'a> Lowering<'a> { } } + fn global_initializer_rule_name(&self) -> String { + if self.hir.preprocessing.rule_prefix_template.is_some() { + "[] Initialize global variables".to_string() + } else { + "Initialize global variables".to_string() + } + } + fn lower_event( &self, event: &hir::Event, @@ -2419,6 +2498,42 @@ impl<'a> Lowering<'a> { ))) } + fn lower_translation_helper( + &mut self, + translations: &hir::TranslationState, + ) -> Result { + let translated_white = translations + .languages + .iter() + .map(|language| { + let locale = translation_locale(language).ok_or_else(|| { + IntegrationError::new( + "translations-invalid", + format!("unsupported translation language '{language}'"), + translations.span, + ) + })?; + self.compiler + .catalog + .enum_spelling("Color", &Locale::new(locale), "WHITE") + .map(str::to_string) + .ok_or_else(|| { + IntegrationError::new( + "translations-invalid", + format!("catalog has no Color.WHITE spelling for locale '{locale}'"), + translations.span, + ) + }) + }) + .collect::, _>>()? + .join("0"); + let text = self.push_value(Value::String(format!("\u{ec48}0{translated_white}"))); + let custom_string = self.push_call("customString", vec![text]); + let null = self.push_value(Value::Null); + let separator = self.push_call("firstOf", vec![null]); + Ok(self.push_call("stringSplit", vec![custom_string, separator])) + } + fn lower_debug( &mut self, expr: &Expr, @@ -2858,6 +2973,13 @@ impl<'a> Lowering<'a> { value: &Expr, span: Option, ) -> Result { + let mut indices = Vec::new(); + if let Some(root) = indexed_target_parts(target, &mut indices) + && indices.len() > 1 + { + indices.reverse(); + return self.lower_nested_indexed_assign(root, &indices, target, value, span); + } match target { Expr::GlobalVar { name, @@ -3070,6 +3192,130 @@ impl<'a> Lowering<'a> { } } + fn lower_nested_indexed_assign( + &mut self, + root: &Expr, + indices: &[&Expr], + target: &Expr, + value: &Expr, + span: Option, + ) -> Result { + let (action_name, root_value) = match root { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *target_span) + })?; + let root_value = self.wir.values.push(ValueNode::new( + Value::GlobalVariable(variable), + self.wir_span(*target_span)?, + )); + ("setGlobalVariableAtIndex", root_value) + } + Expr::PlayerVar { + player, + name, + span: target_span, + .. + } => { + let player_value = self.lower_value(player)?; + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *target_span) + })?; + let root_value = self.wir.values.push(ValueNode::new( + Value::PlayerVariable { + player: player_value, + variable, + }, + self.wir_span(*target_span)?, + )); + ("setPlayerVariableAtIndex", root_value) + } + _ => { + return Err(self.unsupported( + "indexing assignment is only representable for global or player variables", + target.span().copied(), + )); + } + }; + + let outer_index = self.lower_value(indices[0])?; + let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index)?; + let replacement = + self.rebuild_indexed_value(outer_array, &indices[1..], target, value, span)?; + Ok(self.wir.actions.push(Action::Call { + name: action_name.to_string(), + args: vec![root_value, outer_index, replacement], + span: self.wir_span(span)?, + })) + } + + fn rebuild_indexed_value( + &mut self, + array: wir::ValueId, + indices: &[&Expr], + target: &Expr, + value: &Expr, + span: Option, + ) -> Result { + let index = indices + .first() + .copied() + .expect("nested indexed assignment has an inner index"); + let index_value = self.lower_value(index)?; + let replacement = if indices.len() == 1 { + if let Expr::Binary { + op, left, right, .. + } = value + && left.as_ref() == target + && let Some(call_name) = modify_catalog_name_from_str(op) + { + let current = self.lower_indexed_read(array, index, index_value)?; + let right = self.lower_value(right)?; + self.push_call(call_name, vec![current, right]) + } else { + self.lower_value(value)? + } + } else { + let child = self.lower_indexed_read(array, index, index_value)?; + self.rebuild_indexed_value(child, &indices[1..], target, value, span)? + }; + self.replace_array_element(array, index_value, replacement, span) + } + + fn lower_indexed_read( + &mut self, + array: wir::ValueId, + index: &Expr, + index_value: wir::ValueId, + ) -> Result { + if matches!(index, Expr::Number { value, .. } if *value == 0.0) { + Ok(self.push_call("firstOf", vec![array])) + } else { + Ok(self.push_call("valueInArray", vec![array, index_value])) + } + } + + fn replace_array_element( + &mut self, + array: wir::ValueId, + index: wir::ValueId, + replacement: wir::ValueId, + span: Option, + ) -> Result { + let zero = self.push_number(0.0, "0"); + let one = self.push_number(1.0, "1"); + let end = self.push_call("add", vec![index, one]); + let maximum = self.push_number(999_999_999_999.0, "999999999999"); + let prefix = self.push_call("slice", vec![array, zero, index]); + let middle = self.lower_array(vec![replacement], span)?; + let suffix = self.push_call("slice", vec![array, end, maximum]); + let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); + Ok(self.push_call("appendToArray", vec![with_replacement, suffix])) + } + fn lower_action_call( &mut self, name: &str, @@ -4360,6 +4606,20 @@ fn literal_key_matches(left: &hir::Expr, right: &hir::Expr) -> bool { } } +fn indexed_target_parts<'a>( + target: &'a hir::Expr, + indices: &mut Vec<&'a hir::Expr>, +) -> Option<&'a hir::Expr> { + match target { + hir::Expr::Index { array, index, .. } => { + indices.push(index); + indexed_target_parts(array, indices) + } + hir::Expr::GlobalVar { .. } | hir::Expr::PlayerVar { .. } => Some(target), + _ => None, + } +} + fn is_literal_key(expr: &hir::Expr) -> bool { matches!( expr, @@ -4370,6 +4630,27 @@ fn is_literal_key(expr: &hir::Expr) -> bool { ) } +fn translation_locale(language: &str) -> Option<&'static str> { + Some(match language { + "de" => "de-DE", + "en" => "en-US", + "es" | "es_mx" => "es-MX", + "es_es" => "es-ES", + "fr" => "fr-FR", + "it" => "it-IT", + "ja" => "ja-JP", + "ko" => "ko-KR", + "pl" => "pl-PL", + "pt" => "pt-BR", + "ru" => "ru-RU", + "th" => "th-TH", + "tr" => "tr-TR", + "zh" | "zh_cn" => "zh-CN", + "zh_tw" => "zh-TW", + _ => return None, + }) +} + fn big_letters(value: &str) -> String { let mut output = String::with_capacity(value.len()); let mut converted = false; @@ -4832,7 +5113,7 @@ mod tests { fn compile_report_preserves_integration_failure_class_and_source_path() { let compiler = Compiler::new().unwrap(); let report = compiler.compile_source_report_with_locale( - "globalvar A\nrule \"broken\":\n @Event global\n A[1][2] = 3\n", + "rule \"broken\":\n @Event global\n {\"a\": 1}[\"b\"] = 3\n", "broken.opy", Path::new("."), &Locale::new("en-US"), 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 d076e0b..066399a 100644 --- a/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs +++ b/crates/opy-rs/src/compiler/tests/issue_145_lowering.rs @@ -90,6 +90,16 @@ fn literal_dict_lookup_hit_and_miss_match_the_pinned_oracle() { assert_fixture_matches_oracle("issue-46-unsupported"); } +#[test] +fn nested_indexed_assignments_match_the_pinned_oracle() { + assert_fixture_matches_oracle("issue-60-nested-index"); +} + +#[test] +fn translated_implicit_subroutine_fixture_matches_the_pinned_oracle() { + assert_fixture_matches_oracle("issue-31-positive"); +} + #[test] fn null_initializers_are_dropped_but_float_zero_is_preserved() { let source = r#" diff --git a/crates/opy-rs/support-matrix.json b/crates/opy-rs/support-matrix.json index 9fc9d42..a6b580c 100644 --- a/crates/opy-rs/support-matrix.json +++ b/crates/opy-rs/support-matrix.json @@ -667,12 +667,14 @@ "evidence": [ "fixtures:synthetic/issue-46-primitives", "fixtures:synthetic/issue-59-postfix-assignment", + "fixtures:synthetic/issue-60-nested-index", "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::null_initializers_are_dropped_but_float_zero_is_preserved", "contract:workshop-rs-v0.1.16" ], - "notes": "Issue #46/#59/#145. Global/player assignments and augmented assignments (+=, -=, *=, /=, %=, **=, ++, --), literal dictionary lookup folding, single-level indexed variable assignments and modifications (setGlobalVariableAtIndex, modifyGlobalVariableAtIndex, setPlayerVariableAtIndex, modifyPlayerVariableAtIndex), and pass no-op statements lower into canonical workshop-rs WIR. Null and integer-zero declaration initializers are omitted while float zero remains observable; deeper indexed writes remain tracked by #88. Direct global/player modifications carry full statement and target-variable span provenance; the single-level 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. 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; 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", diff --git a/crates/opy-rs/tests/differential.rs b/crates/opy-rs/tests/differential.rs index 6d765b5..3e64e6c 100644 --- a/crates/opy-rs/tests/differential.rs +++ b/crates/opy-rs/tests/differential.rs @@ -217,6 +217,12 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { false, "Issue #59 postfix ++/-- assignment probe; global, player, and single-level indexed targets resolve and are constrained by compiler oracle tests.", ); + resolve( + &mut cases, + "synthetic/issue-60-nested-index", + false, + "Issue #60 nested indexed assignment probe; global targets resolve and are constrained by the pinned canonical-WIR oracle.", + ); diagnostic( &mut cases, "synthetic/issue-59-postfix-negative",