Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/workshop-rs/src/values/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,13 @@ fn value_matches_type(
let Some(node) = program.values.get(value_id) else {
return false;
};
// ADR-0014: every value is accepted in a Boolean parameter; the client
// decides at runtime, and structural checks stay with the caller. Only a
// union led by `Boolean` (`Boolean|Number`) is a Boolean parameter;
// `Number|Boolean` is a Number position with a Boolean alias.
if expected.split('|').next() == Some("Boolean") {
return true;
}
if let wir::Value::Call { name, .. } = &node.value {
if name == wir::AMBIGUOUS_ENUM_CALL {
return expected.split('|').any(|alternative| {
Expand Down
25 changes: 22 additions & 3 deletions crates/workshop-rs/tests/contextual_semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,28 @@ fn wait_until_keeps_numeric_exception_without_global_truthiness() {
));

let vector = program("Wait Until(Vector(1, 2, 3), 2);");
let error = validate::validate_canonical_ids_wir(&vector, &catalog())
.expect_err("Wait Until must not accept arbitrary values as conditions");
assert!(format!("{error:?}").contains("semantic type"));
validate_program(&vector);
}

#[test]
fn boolean_parameters_accept_any_value_but_keep_structural_checks() {
for condition in [
"Position Of(Event Player)",
"Vector(1, 2, 3)",
"Custom String(\"x\")",
"Global Variable(probe)",
"Event Player",
] {
validate_program(&program(&format!("Wait Until({condition}, 2);")));
}

let arity = program("Wait Until(Position Of(Event Player), 2, 3);");
validate::validate_canonical_ids_wir(&arity, &catalog())
.expect_err("arity is still checked with a non-Boolean value");

let inner = program("Wait Until(Position Of(Event Player, 1), 2);");
validate::validate_canonical_ids_wir(&inner, &catalog())
.expect_err("the accepted value itself is still validated structurally");
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/workshop-rs/tests/language_conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ fn sample_argument(catalog: &Catalog, parameter_type: &str) -> String {
}

fn is_concrete_type(value_type: &str) -> bool {
!value_type.contains('|') && !matches!(value_type, "Any" | "Object" | "Array")
!value_type.contains('|') && !matches!(value_type, "Any" | "Object" | "Array" | "Boolean")
}

fn incompatible_argument(value_type: &str) -> String {
Expand Down
20 changes: 8 additions & 12 deletions crates/workshop-rs/tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,16 +512,16 @@ fn remaining_value_contracts_are_canonical_and_type_checked() {
#[test]
fn canonical_validation_enforces_literal_types_and_value_return_types() {
let catalog = catalog();
let wrong_literal = r#"rule ("type") { event { Ongoing - Global; } actions { Set Crouch Enabled(All Players(All Teams), Color(White)); } }"#;
let wrong_literal = r#"rule ("type") { event { Ongoing - Global; } actions { Teleport(Event Player, Color(White)); } }"#;
let program =
parser::parse_wir_with_context(wrong_literal, &catalog, &Locale::new("en-US"), &catalog)
.expect("parser preserves a typed call for canonical validation");
let error = validate::validate_canonical_ids_wir(&program, &catalog)
.expect_err("a Color is not a Boolean action parameter");
.expect_err("a Color is not a Vector action parameter");
assert!(
error
.to_string()
.contains("must have semantic type 'Boolean'")
.contains("must have semantic type 'Vector'")
);

let wrong_return = r#"rule ("return") { event { Ongoing - Global; } actions { Teleport(Event Player, Max Health(Event Player)); } }"#;
Expand Down Expand Up @@ -565,7 +565,7 @@ fn canonical_validation_preserves_first_error_ordering_on_multi_error_input() {
let multi_arg_source = r#"rule ("multi-arg-error") {
event { Ongoing - Global; }
actions {
Set Crouch Enabled(Color(White), Color(White));
Teleport(Color(White), Color(White));
}
}"#;
let program =
Expand All @@ -574,17 +574,15 @@ fn canonical_validation_preserves_first_error_ordering_on_multi_error_input() {
let error = validate::validate_canonical_ids_wir(&program, &catalog)
.expect_err("multi-arg error must fail with first argument error");
assert!(
error
.to_string()
.contains("action 'setCrouchEnabled' argument 1"),
error.to_string().contains("action 'teleport' argument 1"),
"expected error for argument 1 (first argument), got: {error}"
);

// Test 2: Rule with multiple invalid actions must report the first action's error
let multi_action_source = r#"rule ("multi-action-error") {
event { Ongoing - Global; }
actions {
Set Crouch Enabled(All Players(All Teams), Color(White));
Teleport(Event Player, Color(White));
Teleport(Event Player, Max Health(Event Player));
}
}"#;
Expand All @@ -598,10 +596,8 @@ fn canonical_validation_preserves_first_error_ordering_on_multi_error_input() {
let error = validate::validate_canonical_ids_wir(&program, &catalog)
.expect_err("multi-action error must fail validation with first action error");
assert!(
error
.to_string()
.contains("action 'setCrouchEnabled' argument 2"),
"expected first error for setCrouchEnabled, got: {error}"
error.to_string().contains("action 'teleport' argument 2"),
"expected first error for teleport, got: {error}"
);
}

Expand Down
4 changes: 4 additions & 0 deletions docs/language-support/control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ The reviewed contract includes these examples:
condition as a documented exception, but does not normalize it to Boolean;
non-zero numbers are not treated as true by that action. Callers should use
an explicit comparison when Boolean behavior is intended.
- A parameter whose primary declared type is `Boolean` (`Boolean` or
`Boolean|Number`, not `Number|Boolean`) accepts any value in
canonical validation. This is a typed rule, not per-parameter catalog data;
identity, arity, enum-domain, and reference-category checks still apply.

These distinctions are cross-checked against
[OverPy's replacement metadata](https://github.com/Zezombye/overpy/blob/2002431649cbdd7ddc3aa70bd184b598ec2820e5/src/types.d.ts),
Expand Down
1 change: 1 addition & 0 deletions docs/source-attribution/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ is embedded in the dataset and surfaced by `workshop-rs-cli version --json`.
| Rule event identities and filters (`global`, `eachPlayer`, player events, `subroutine`, `EventTeam`, and `EventPlayer`) | User-provided Workshop export at commit `d854bf01fc7bbf3b2169f67408c07a8da8989ad6` (`other.events`, `other.eventTeams`, `other.eventPlayers`, and computed `other.eventSlots`), cross-checked against documented raw Workshop event blocks. |
| OSTW-exercised params/spellings and enum domains (CreateEffect, Workshop Setting, Hero/Map/Button/Icon/Operation/Rounding/InworldTextRev, …) | Pinned OSTW v3.4.0 reference probe emissions (P4/P5/P6/P6b) and the protect-ban entry-point reachable closure. |
| Parameter metadata (`paramDomains`, `paramDefaults`, `paramCoercions`) | Pinned-reference probe data, never copied from upstream game data. Contextual literal substitutions are recorded per parameter position and cross-checked against [OverPy's replacement metadata](https://github.com/Zezombye/overpy/blob/2002431649cbdd7ddc3aa70bd184b598ec2820e5/src/types.d.ts), [Workshop emission path](https://github.com/Zezombye/overpy/blob/2002431649cbdd7ddc3aa70bd184b598ec2820e5/src/compiler/astToWorkshop.ts), and [Wait Until handling](https://github.com/Zezombye/overpy/blob/2002431649cbdd7ddc3aa70bd184b598ec2820e5/src/compiler/functions/waitUntil.ts); they do not establish global type coercions. |
| Boolean parameter acceptance (typed rule in `values/validate.rs`, not catalog data) | [ADR-0014](../adr/0014-validation-evidence-for-slot-acceptance.md): OverPy wraps values in `First Of` keyed on the `Boolean` parameter type ([`astToWorkshop.ts`](https://github.com/Zezombye/overpy/blob/1e2688954302a402d076944b46db07efb14d7b61/src/compiler/astToWorkshop.ts)), and the fix for Zezombye/overpy#182 states every value can be put in a Boolean field. The values the client rejects there are not modelled. |
| Localized parameter labels (`paramAliases`) | Separately reviewed owner mappings, stored per declared parameter position and resolved only through the catalog API. The initial `Wait` `zh-CN` labels are `时间` and `等待行为`, as identified by the documented Chinese Workshop action reference at https://overwatch.huijiwiki.com/wiki/%E5%9C%B0%E5%9B%BE%E5%B7%A5%E5%9D%8A/%E5%8A%A8%E4%BD%9C (accessed 2026-09-11); the canonical parameter order and descriptions are cross-checked against the pinned `workshop-data` action definition. They are not generated by the general zh-CN corpus pipeline. |
| Action/Value parameter and return signatures | Workshop.codes structured article properties (Returns, Parameters, Type, and Default), cross-checked against the pinned OverPy metadata and static OSTW data; entries without convergent sources remain explicitly unresolved. Representative article links are recorded with the catalog entries. |
| `Create Dummy Bot` hero parameter union (`Hero|Array`) | Workshop behavior accepts a single Hero or Hero array and selects randomly when multiple heroes are provided; pinned OverPy 9.7.10 texture-tag setup emits `getAllHeroes()` into `Create Dummy Bot`. |
Expand Down
Loading