From 139e7c52fe1cba5e3f66f08e1766b32fecdc55ee Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sat, 29 Aug 2026 18:48:54 +0800 Subject: [PATCH 1/2] fix(parser): disambiguate canonical enum members Fixes #124 --- README.md | 2 +- crates/workshop-rs/src/parser.rs | 12 ++++---- crates/workshop-rs/src/signatures.rs | 6 ++-- crates/workshop-rs/tests/parser.rs | 44 ++++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2b0dc46..80fff36 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ use workshop_rs::emitter::emit; let catalog = Catalog::builtin()?; let locale = Locale::new("en-US"); -let program = workshop_rs::parser::parse_with_context(text, &catalog, &locale, &catalog)?; +let program = workshop_rs::parser::parse(text, &catalog, &locale)?; let emitted = emit(&program, &catalog, &locale)?; let converted = convert( diff --git a/crates/workshop-rs/src/parser.rs b/crates/workshop-rs/src/parser.rs index 9435bfb..ab83ced 100644 --- a/crates/workshop-rs/src/parser.rs +++ b/crates/workshop-rs/src/parser.rs @@ -11,7 +11,7 @@ use std::sync::OnceLock; use crate::settings::table::{self, KeyKind, PathPart}; use crate::settings::{Settings, SettingsListElement, SettingsNode}; -use crate::signatures::{ExpectedDomain, NoExpectedDomain}; +use crate::signatures::ExpectedDomain; use crate::source::{Position, SourceFile, Span}; use crate::wir::{ self, Action, Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind, Value, ValueNode, @@ -39,11 +39,13 @@ enum AssignmentOperator { Modify(ModifyOp), } -/// Parse localized Workshop text into Workshop IR with no signature context: -/// ambiguous bare enum members (e.g. the `None` shared by several domains) -/// stay rejected. See [`parse_with_context`] for the context-sensitive form. +/// Parse localized Workshop text into Workshop IR using the catalog's +/// canonical call-signature context. Ambiguous bare enum members resolve when +/// their enclosing call pins one matching domain; unpinned ambiguity remains a +/// structured unsupported diagnostic. See [`parse_with_context`] when a +/// consumer needs to provide additional signature context. pub fn parse(input: &str, catalog: &Catalog, locale: &Locale) -> Result { - parse_with_context(input, catalog, locale, &NoExpectedDomain) + parse_with_context(input, catalog, locale, catalog) } /// Parse localized Workshop text into Workshop IR, resolving ambiguous bare diff --git a/crates/workshop-rs/src/signatures.rs b/crates/workshop-rs/src/signatures.rs index 13c5a57..a7e1583 100644 --- a/crates/workshop-rs/src/signatures.rs +++ b/crates/workshop-rs/src/signatures.rs @@ -24,10 +24,8 @@ pub trait ExpectedDomain { fn expected_domain(&self, catalog_id: &str, arg_index: usize) -> Option<&str>; } -/// The default context: no signature pins any argument domain, so ambiguous -/// bare enum members stay rejected. Used by the plain -/// [`crate::parser::parse`] entry point and callers without signature -/// metadata. +/// A context with no signature metadata. Ambiguous bare enum members stay +/// rejected. Used by callers that intentionally need context-free parsing. #[derive(Debug, Clone, Copy, Default)] pub struct NoExpectedDomain; diff --git a/crates/workshop-rs/tests/parser.rs b/crates/workshop-rs/tests/parser.rs index a90ede1..8873344 100644 --- a/crates/workshop-rs/tests/parser.rs +++ b/crates/workshop-rs/tests/parser.rs @@ -591,8 +591,8 @@ fn unsupported_construct_is_distinct_from_malformed() { #[test] fn bare_chase_reevaluation_none_is_ambiguous_across_domains() { // Both reference reevaluation domains spell their NONE member "None". - // Without a signature pin the flat parser rejects the bare spelling - // with a structured Unsupported diagnostic. + // Without a signature pin the catalog-backed parser rejects the bare + // spelling with a structured Unsupported diagnostic. let text = "variables { global: 0: g }\nrule (\"x\") { event { Ongoing - Global; } actions { Set Global Variable(g, None); } }"; let error = parser::parse(text, &catalog(), &Locale::new("en-US")).unwrap_err(); assert!( @@ -656,6 +656,46 @@ fn context_pinned_ambiguous_none_resolves_for_set_invisible() { ); } +#[test] +fn released_parse_contract_resolves_pinned_oracle_members() { + let catalog = catalog(); + let locale = Locale::new("en-US"); + + let cake = parser::parse(&corpus_workshop_text("overpy-cake"), &catalog, &locale) + .expect("the pinned cake Workshop output must parse through the released contract"); + assert!(cake.values.iter().any(|node| matches!( + node.value, + wir::Value::Enum { ref value_type, ref value } + if value_type == "EffectReeval" && value == "VISIBILITY" + ))); + + let chase = r#"variables { + global: + 0: Round_Attack_Time +} + +rule ("chase and condition") { + event { + Ongoing - Global; + } + conditions { + Is Game In Progress == True; + Global.Round_Attack_Time != 0; + } + actions { + Chase Global Variable Over Time(Round_Attack_Time, 0, 30, None); + } +} +"#; + let chase = parser::parse(chase, &catalog, &locale) + .expect("the pinned chase Workshop output must parse through the released contract"); + assert!(chase.values.iter().any(|node| matches!( + node.value, + wir::Value::Enum { ref value_type, ref value } + if value_type == "ChaseTimeReeval" && value == "NONE" + ))); +} + #[test] fn wrong_domain_context_keeps_the_ambiguity_rejected() { // A signature pinning a *different* domain than the ambiguous member's From 5e97124aa61d1ab3139b5966c1f502c64960c2da Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sat, 29 Aug 2026 19:16:28 +0800 Subject: [PATCH 2/2] test(parser): cover zh-CN enum disambiguation --- crates/workshop-rs/tests/parser.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/workshop-rs/tests/parser.rs b/crates/workshop-rs/tests/parser.rs index 8873344..9bd5885 100644 --- a/crates/workshop-rs/tests/parser.rs +++ b/crates/workshop-rs/tests/parser.rs @@ -694,6 +694,28 @@ rule ("chase and condition") { wir::Value::Enum { ref value_type, ref value } if value_type == "ChaseTimeReeval" && value == "NONE" ))); + + let chase_zh = r#"variables { + global: + 0: Round_Attack_Time +} + +rule ("chase and condition") { + event { + 持续 - 全局; + } + actions { + 持续追踪全局变量(Round_Attack_Time, 0, 30, 全部禁用); + } +} +"#; + let chase_zh = parser::parse(chase_zh, &catalog, &Locale::new("zh-CN")) + .expect("the pinned chase Workshop output must parse in zh-CN"); + assert!(chase_zh.values.iter().any(|node| matches!( + node.value, + wir::Value::Enum { ref value_type, ref value } + if value_type == "ChaseTimeReeval" && value == "NONE" + ))); } #[test]