From 72c1173626c8643597132a1d3833a36e80d64f88 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 03:25:36 +0800 Subject: [PATCH 1/4] deps: bump workshop-rs to 0.9.1 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba0c581b..8463899b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,9 +631,9 @@ dependencies = [ [[package]] name = "workshop-rs" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee5e37a14f5f59d7948eb87537494472509a3446f18cb55eebbdfca3b74425c" +checksum = "cbf0bbc9b33b87951f863effc905440833d9efcc116f051a116059f0a08610b9" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e94f5702..afcf53ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ libquickjs-ng-sys = "0.13.0" serde = "1" serde_json = "1" sha2 = "0.10" -workshop-rs = "=0.8.0" +workshop-rs = "=0.9.1" [workspace.lints.rust] unsafe_op_in_unsafe_fn = "deny" From 32b53fc9e91c44a5303344a1b3acc7f01072de58 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 03:41:25 +0800 Subject: [PATCH 2/4] test(compiler): assert canonical Workshop structure --- crates/opy-rs/src/compiler/mod.rs | 21 ++++-- .../src/compiler/tests/callable_surface.rs | 43 ++++++++--- .../src/compiler/tests/catalog_lowering.rs | 49 ++++++++++++- .../src/compiler/tests/indexed_assignments.rs | 72 ++++++++++++++++--- .../src/compiler/tests/numeric_settings.rs | 30 +++++--- .../src/compiler/tests/string_literals.rs | 27 +++++-- 6 files changed, 200 insertions(+), 42 deletions(-) diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index 4c875c8d..f0ac7195 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -1824,11 +1824,22 @@ rule "assignments": .emitted .contains("Modify Player Variable(Event Player, p1, Multiply, 2);") ); - assert!( - artifact - .emitted - .contains("Set Player Variable At Index((Event Player).p2, 2, 7);") - ); + assert!(artifact.wir.rules[1].actions.iter().any(|action| matches!( + action, + workshop_rs::Action::Call { name, args } + if name == "setPlayerVariableAtIndex" + && matches!( + args.as_slice(), + [ + workshop_rs::Value::PlayerVariable { player, variable }, + workshop_rs::Value::Number(index), + workshop_rs::Value::Number(value), + ] if matches!(player.as_ref(), workshop_rs::Value::EventPlayer) + && variable == "p2" + && *index == 2.0 + && *value == 7.0 + ) + ))); assert!( artifact .emitted diff --git a/crates/opy-rs/src/compiler/tests/callable_surface.rs b/crates/opy-rs/src/compiler/tests/callable_surface.rs index bd80f046..a5a8a354 100644 --- a/crates/opy-rs/src/compiler/tests/callable_surface.rs +++ b/crates/opy-rs/src/compiler/tests/callable_surface.rs @@ -3,6 +3,7 @@ use std::path::Path; use crate::Compiler; +use workshop_rs::{Action, Value}; #[test] fn translation_calls_use_the_existing_translation_helper() { @@ -267,19 +268,45 @@ rule "event values": result = eventWasEnvironment result = eventWasHealthPack "#; - let emitted = Compiler::new() + let artifact = Compiler::new() .unwrap() .compile_source_artifact(source, "event-values.opy", Path::new(".")) - .expect("bare event values and the hero setting must compile") - .emitted; + .expect("bare event values and the hero setting must compile"); + assert!( + artifact + .wir + .rules + .iter() + .flat_map(|rule| &rule.actions) + .any(|action| { + matches!( + action, + Action::SetGlobalVariable { + variable, + value: Value::Call { name, .. }, + } if variable == "heroSetting" && name == "createWorkshopSettingHero" + ) + }) + ); + let rule = artifact + .wir + .rules + .iter() + .find(|rule| rule.name == "event values") + .expect("event rule must lower"); for expected in [ - "Workshop Setting Hero(Custom String(\"cat\"), Custom String(\"hero\"), Ana, 0)", - "Set Global Variable(result, Event Direction);", - "Set Global Variable(result, Event Was Environment);", - "Set Global Variable(result, Event Was Health Pack);", + "eventDirection", + "eventWasEnvironment", + "eventWasHealthPack", ] { - assert!(emitted.contains(expected), "missing {expected}\n{emitted}"); + assert!(rule.actions.iter().any(|action| matches!( + action, + Action::SetGlobalVariable { + variable, + value: Value::Call { name, .. }, + } if variable == "result" && name == expected + ))); } } diff --git a/crates/opy-rs/src/compiler/tests/catalog_lowering.rs b/crates/opy-rs/src/compiler/tests/catalog_lowering.rs index 45888a78..95739506 100644 --- a/crates/opy-rs/src/compiler/tests/catalog_lowering.rs +++ b/crates/opy-rs/src/compiler/tests/catalog_lowering.rs @@ -5,6 +5,35 @@ use std::path::{Path, PathBuf}; use crate::Compiler; use workshop_rs::catalog::{Catalog, Locale}; use workshop_rs::roundtrip::equivalent; +use workshop_rs::{Action, Value}; + +fn collect_calls<'a>(value: &'a Value, name: &str, calls: &mut Vec<&'a [Value]>) { + match value { + Value::Call { + name: call_name, + args, + } => { + if call_name == name { + calls.push(args); + } + for argument in args { + collect_calls(argument, name, calls); + } + } + Value::Array(values) => { + for value in values { + collect_calls(value, name, calls); + } + } + Value::Vector { x, y, z } => { + collect_calls(x, name, calls); + collect_calls(y, name, calls); + collect_calls(z, name, calls); + } + Value::PlayerVariable { player, .. } => collect_calls(player, name, calls), + _ => {} + } +} fn fixture_dir(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) @@ -219,8 +248,24 @@ fn pinned_texture_members_lower_with_texture_tag_setup() { .emitted .contains("Create Dummy Bot(All Heroes, If-Then-Else(") ); - assert!(artifact.emitted.contains("Number Of Slots(Team(Team 1))")); - assert!(artifact.emitted.contains("Number Of Slots(Team(Team 2))")); + let dummy_team = artifact + .wir + .rules + .iter() + .flat_map(|rule| rule.actions.iter()) + .find_map(|action| match action { + Action::Call { name, args } if name == "createDummy" => args.get(1), + _ => None, + }) + .expect("texture setup must choose a team for its dummy bot"); + let mut slot_queries = Vec::new(); + collect_calls(dummy_team, "getNumberOfSlots", &mut slot_queries); + for team in ["TEAM_1", "TEAM_2"] { + assert!(slot_queries.iter().any(|args| matches!( + *args, + [Value::Enum { value_type, value }] if value_type == "Team" && value == team + ))); + } assert!( artifact .emitted diff --git a/crates/opy-rs/src/compiler/tests/indexed_assignments.rs b/crates/opy-rs/src/compiler/tests/indexed_assignments.rs index 9bcb856f..6e540972 100644 --- a/crates/opy-rs/src/compiler/tests/indexed_assignments.rs +++ b/crates/opy-rs/src/compiler/tests/indexed_assignments.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use crate::Compiler; use workshop_rs::catalog::{Catalog, Locale}; use workshop_rs::roundtrip::equivalent; +use workshop_rs::{Action, Value}; fn fixture_dir(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) @@ -79,9 +80,26 @@ rule "delete nested player value": "#; let hir = crate::compile(source, "nested-player-delete.opy", Path::new(".")).unwrap(); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); - assert!(artifact.emitted.contains( - "Modify Player Variable At Index((Event Player).values, index, Remove From Array By Index, 0);" - )); + let Action::Call { name, args } = &artifact.wir.rules[0].actions[0] else { + panic!("nested deletion must lower to an indexed player-variable action"); + }; + assert_eq!(name, "modifyPlayerVariableAtIndex"); + let [ + Value::PlayerVariable { player, variable }, + Value::GlobalVariable(outer_index), + Value::Call { + name: operation, .. + }, + Value::Number(inner_index), + ] = args.as_slice() + else { + panic!("nested deletion must preserve its variable, indices, and operation"); + }; + assert!(matches!(player.as_ref(), Value::EventPlayer)); + assert_eq!(variable, "values"); + assert_eq!(outer_index, "index"); + assert_eq!(operation, "removeFromArrayByIndex"); + assert_eq!(*inner_index, 0.0); } #[test] @@ -129,13 +147,47 @@ rule "indexed receiver mutations": let hir = crate::compile(source, "indexed-receiver-mutations.opy", Path::new(".")).unwrap(); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); - assert!(artifact.emitted.contains( - "Modify Global Variable At Index(values, Slot Of(Event Player), Append To Array, Current Map);" - )); - assert!(artifact.emitted.contains( - "Modify Player Variable At Index((Event Player).slots, Slot Of(Event Player), Remove From Array, Hero Of(Event Player));" - )); - assert_eq!(artifact.emitted.matches("Slot Of(Event Player)").count(), 2); + let calls = artifact + .wir + .rules + .iter() + .flat_map(|rule| rule.actions.iter()) + .filter_map(|action| match action { + Action::Call { name, args } => Some((name.as_str(), args.as_slice())), + _ => None, + }) + .collect::>(); + assert!(calls.iter().any(|(name, args)| { + *name == "modifyGlobalVariableAtIndex" + && matches!( + *args, + [ + Value::GlobalVariable(variable), + Value::Call { name: index, .. }, + Value::Call { name: operation, .. }, + Value::Call { name: value, .. }, + ] if variable == "values" + && index == "getSlot" + && operation == "appendToArray" + && value == "getCurrentMap" + ) + })); + assert!(calls.iter().any(|(name, args)| { + *name == "modifyPlayerVariableAtIndex" + && matches!( + *args, + [ + Value::PlayerVariable { player, variable }, + Value::Call { name: index, .. }, + Value::Call { name: operation, .. }, + Value::Call { name: value, .. }, + ] if matches!(player.as_ref(), Value::EventPlayer) + && variable == "slots" + && index == "getSlot" + && operation == "removeFromArray" + && value == "getHero" + ) + })); assert_eq!(artifact.wir.action_span(0, 0).unwrap().start.line, 7); assert_eq!(artifact.wir.action_span(0, 1).unwrap().start.line, 8); diff --git a/crates/opy-rs/src/compiler/tests/numeric_settings.rs b/crates/opy-rs/src/compiler/tests/numeric_settings.rs index d8c91b00..215b7a84 100644 --- a/crates/opy-rs/src/compiler/tests/numeric_settings.rs +++ b/crates/opy-rs/src/compiler/tests/numeric_settings.rs @@ -180,36 +180,44 @@ fn find_initializer(program: &crate::hir::Program) -> &Expr { #[test] fn omitted_setting_sort_order_lowers_to_zero() { let compiler = Compiler::new().expect("released Workshop contract must load"); - for (call, expected) in [ + for (call, function) in [ ( "createWorkshopSettingBool(\"C\", \"N\", true)", - "Workshop Setting Toggle(Custom String(\"C\"), Custom String(\"N\"), True, 0)", + "createWorkshopSettingBool", ), ( "createWorkshopSettingInt(\"C\", \"N\", 1, 0, 10)", - "Workshop Setting Integer(Custom String(\"C\"), Custom String(\"N\"), 1, 0, 10, 0)", + "createWorkshopSettingInt", ), ( "createWorkshopSettingEnum(\"C\", \"N\", 0, [\"a\", \"b\"])", - "Workshop Setting Combo(Custom String(\"C\"), Custom String(\"N\"), 0, Array(Custom String(\"a\"), Custom String(\"b\")), 0)", + "createWorkshopSettingEnum", ), ( "createWorkshopSettingHero(\"C\", \"N\", Hero.ANA)", - "Workshop Setting Hero(Custom String(\"C\"), Custom String(\"N\"), Ana, 0)", + "createWorkshopSettingHero", ), ( "createWorkshopSetting(int[0:10], \"C\", \"N\", 1)", - "Workshop Setting Integer(Custom String(\"C\"), Custom String(\"N\"), 1, 0, 10, 0)", + "createWorkshopSettingInt", ), ] { let source = format!("globalvar g\n\nrule \"p\":\n @Event global\n g = {call}\n"); let artifact = compiler .compile_source_with_locale(&source, "sort.opy", Path::new("."), &Locale::new("en-US")) .unwrap_or_else(|error| panic!("{call} must compile: {error}")); - assert!( - artifact.emitted.contains(expected), - "{call} emitted:\n{}", - artifact.emitted - ); + let setting = artifact + .wir + .rules + .iter() + .flat_map(|rule| rule.actions.iter()) + .flat_map(action_values) + .find_map(|value| find_call(value, function)) + .unwrap_or_else(|| panic!("{call} must lower to its Workshop setting call")); + let Value::Call { name, args } = setting else { + unreachable!("the search above only returns call values"); + }; + assert_eq!(name, function); + assert!(matches!(args.last(), Some(Value::Number(sort_order)) if *sort_order == 0.0)); } } diff --git a/crates/opy-rs/src/compiler/tests/string_literals.rs b/crates/opy-rs/src/compiler/tests/string_literals.rs index 11db2dfe..32ce3a2f 100644 --- a/crates/opy-rs/src/compiler/tests/string_literals.rs +++ b/crates/opy-rs/src/compiler/tests/string_literals.rs @@ -90,12 +90,27 @@ fn format_folds_constant_arguments_without_nested_string_chunks() { let hir = crate::compile(source, "source.opy", Path::new(".")).unwrap(); let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); - assert!( - artifact - .emitted - .contains("Set Global Variable(g, Custom String(\"Hold {0}: 49% 2 3 4 5\", Reload));") - ); - assert!(!artifact.emitted.contains("Custom String(\"{0}{1}\"")); + let rule = artifact + .wir + .rules + .iter() + .find(|rule| rule.name == "r") + .expect("string-format rule must lower"); + assert!(matches!( + rule.actions.first(), + Some(Action::SetGlobalVariable { + variable, + value: Value::Call { name, args }, + }) if variable == "g" + && name == "customString" + && matches!( + args.as_slice(), + [Value::String(text), Value::Enum { value_type, value }] + if text == "Hold {0}: 49% 2 3 4 5" + && value_type == "Button" + && value == "RELOAD" + ) + )); } #[test] From 49cad3e5254cd68a4db1d2197490bb442088e72a Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 03:45:47 +0800 Subject: [PATCH 3/4] test(compiler): use canonical Workshop identifiers --- crates/opy-rs/src/compiler/mod.rs | 23 +++++++++++++++---- .../src/compiler/tests/catalog_lowering.rs | 18 ++++++--------- .../src/compiler/tests/indexed_assignments.rs | 2 +- .../src/compiler/tests/numeric_settings.rs | 8 +++---- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index f0ac7195..c210a53a 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -1840,11 +1840,24 @@ rule "assignments": && *value == 7.0 ) ))); - assert!( - artifact - .emitted - .contains("Modify Player Variable At Index((Event Player).p2, 0, Subtract, 3);") - ); + assert!(artifact.wir.rules[1].actions.iter().any(|action| matches!( + action, + workshop_rs::Action::Call { name, args } + if name == "modifyPlayerVariableAtIndex" + && matches!( + args.as_slice(), + [ + workshop_rs::Value::PlayerVariable { player, variable }, + workshop_rs::Value::Number(index), + workshop_rs::Value::Call { name: operation, .. }, + workshop_rs::Value::Number(value), + ] if matches!(player.as_ref(), workshop_rs::Value::EventPlayer) + && variable == "p2" + && *index == 0.0 + && operation == "subtract" + && *value == 3.0 + ) + ))); let rule = artifact.wir.rules.get(1).unwrap(); assert!(matches!( diff --git a/crates/opy-rs/src/compiler/tests/catalog_lowering.rs b/crates/opy-rs/src/compiler/tests/catalog_lowering.rs index 95739506..0f0d8f00 100644 --- a/crates/opy-rs/src/compiler/tests/catalog_lowering.rs +++ b/crates/opy-rs/src/compiler/tests/catalog_lowering.rs @@ -248,18 +248,14 @@ fn pinned_texture_members_lower_with_texture_tag_setup() { .emitted .contains("Create Dummy Bot(All Heroes, If-Then-Else(") ); - let dummy_team = artifact - .wir - .rules - .iter() - .flat_map(|rule| rule.actions.iter()) - .find_map(|action| match action { - Action::Call { name, args } if name == "createDummy" => args.get(1), - _ => None, - }) - .expect("texture setup must choose a team for its dummy bot"); let mut slot_queries = Vec::new(); - collect_calls(dummy_team, "getNumberOfSlots", &mut slot_queries); + for action in artifact.wir.rules.iter().flat_map(|rule| &rule.actions) { + if let Action::Call { args, .. } = action { + for value in args { + collect_calls(value, "getNumberOfSlots", &mut slot_queries); + } + } + } for team in ["TEAM_1", "TEAM_2"] { assert!(slot_queries.iter().any(|args| matches!( *args, diff --git a/crates/opy-rs/src/compiler/tests/indexed_assignments.rs b/crates/opy-rs/src/compiler/tests/indexed_assignments.rs index 6e540972..dcb461be 100644 --- a/crates/opy-rs/src/compiler/tests/indexed_assignments.rs +++ b/crates/opy-rs/src/compiler/tests/indexed_assignments.rs @@ -169,7 +169,7 @@ rule "indexed receiver mutations": ] if variable == "values" && index == "getSlot" && operation == "appendToArray" - && value == "getCurrentMap" + && value == "currentMap" ) })); assert!(calls.iter().any(|(name, args)| { diff --git a/crates/opy-rs/src/compiler/tests/numeric_settings.rs b/crates/opy-rs/src/compiler/tests/numeric_settings.rs index 215b7a84..f8ec0364 100644 --- a/crates/opy-rs/src/compiler/tests/numeric_settings.rs +++ b/crates/opy-rs/src/compiler/tests/numeric_settings.rs @@ -183,15 +183,15 @@ fn omitted_setting_sort_order_lowers_to_zero() { for (call, function) in [ ( "createWorkshopSettingBool(\"C\", \"N\", true)", - "createWorkshopSettingBool", + "workshopSettingToggle", ), ( "createWorkshopSettingInt(\"C\", \"N\", 1, 0, 10)", - "createWorkshopSettingInt", + "workshopSettingInteger", ), ( "createWorkshopSettingEnum(\"C\", \"N\", 0, [\"a\", \"b\"])", - "createWorkshopSettingEnum", + "workshopSettingCombo", ), ( "createWorkshopSettingHero(\"C\", \"N\", Hero.ANA)", @@ -199,7 +199,7 @@ fn omitted_setting_sort_order_lowers_to_zero() { ), ( "createWorkshopSetting(int[0:10], \"C\", \"N\", 1)", - "createWorkshopSettingInt", + "workshopSettingInteger", ), ] { let source = format!("globalvar g\n\nrule \"p\":\n @Event global\n g = {call}\n"); From 2adce1f47a04d58704eed10ee73e0277b6bae73d Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 03:58:08 +0800 Subject: [PATCH 4/4] fix(compiler): preserve Team constants in Color slots --- crates/opy-rs/src/compiler/lowering.rs | 43 +------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 9562a35e..21a6dfb8 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -4695,7 +4695,7 @@ impl<'a> Lowering<'a> { ) })? }; - let mut args = self.normalize_catalog_argument_domains(catalog_id, args); + let mut args = self.normalize_contextual_arguments(catalog_id, args); self.apply_replacements(catalog_id, &mut args, span); self.optimize_wait_duration(catalog_id, &mut args, span); Ok(self.push_call_action_with_spans(catalog_id, &args, spans)) @@ -4729,47 +4729,6 @@ impl<'a> Lowering<'a> { } } - fn normalize_catalog_argument_domains( - &mut self, - catalog_id: &str, - mut args: Vec, - ) -> Vec { - if self - .compiler - .catalog - .entry(Kind::Action, catalog_id) - .is_none() - { - return args; - } - let mut index = 0; - while index < args.len() { - args[index] = self.normalize_contextual_argument(catalog_id, index, args[index]); - let Some(domain) = self - .compiler - .catalog - .entry(Kind::Action, catalog_id) - .and_then(|entry| entry.param_domain(index)) - .map(str::to_string) - else { - index += 1; - continue; - }; - let Some(Value::Enum { value_type, value }) = self.values.get(args[index]) else { - index += 1; - continue; - }; - if value_type == "Team" && domain == "Color" { - args[index] = self.push_value(Value::Enum { - value_type: domain, - value: value.clone(), - }); - } - index += 1; - } - args - } - fn lower_hud_text( &mut self, args: &[Expr],