From a1ed3106c9a17d5acbc96e2c59a2c6dbc4c46a67 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 31 Aug 2026 12:05:34 +0800 Subject: [PATCH 1/2] feat(wir): preserve player loop target spans Carry the exact player-variable identifier span through ForPlayerVariable without changing Workshop emission. --- crates/workshop-rs/src/parser.rs | 3 ++- crates/workshop-rs/src/wir/dump.rs | 1 + crates/workshop-rs/src/wir/mod.rs | 2 ++ crates/workshop-rs/tests/action_layout.rs | 1 + crates/workshop-rs/tests/parser.rs | 30 +++++++++++++++++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/workshop-rs/src/parser.rs b/crates/workshop-rs/src/parser.rs index 7793a25..6b8e82c 100644 --- a/crates/workshop-rs/src/parser.rs +++ b/crates/workshop-rs/src/parser.rs @@ -1976,7 +1976,7 @@ impl Parser<'_> { )?; let player = self.value()?; self.expect(TokenKind::Comma, "expected ',' after loop player")?; - let (name, _, _) = self.phrase()?; + let (name, target_start, target_end) = self.phrase()?; let variable = self.player_by_name(&name)?; self.expect(TokenKind::Comma, "expected ',' after loop variable")?; let start_value = self.value()?; @@ -2004,6 +2004,7 @@ impl Parser<'_> { step, body, span: Some(Span::new(self.file(), start, end_span.1)), + target_span: Some(Span::new(self.file(), target_start, target_end)), }; Ok(self.target.actions.push(action)) } diff --git a/crates/workshop-rs/src/wir/dump.rs b/crates/workshop-rs/src/wir/dump.rs index 15fc2c1..27dd016 100644 --- a/crates/workshop-rs/src/wir/dump.rs +++ b/crates/workshop-rs/src/wir/dump.rs @@ -295,6 +295,7 @@ fn render_action(program: &Program, id: super::ActionId, out: &mut String, level step, body, span, + .. } => { out.push_str(&format!("{}forPlayerVariable ", indent(level))); render_value(program, *player, out); diff --git a/crates/workshop-rs/src/wir/mod.rs b/crates/workshop-rs/src/wir/mod.rs index c7afd56..531dda9 100644 --- a/crates/workshop-rs/src/wir/mod.rs +++ b/crates/workshop-rs/src/wir/mod.rs @@ -396,6 +396,8 @@ pub enum Action { step: ValueId, body: Vec, span: Option, + /// The exact span of the loop variable identifier. + target_span: Option, }, /// Any other action call with side effects. Call { diff --git a/crates/workshop-rs/tests/action_layout.rs b/crates/workshop-rs/tests/action_layout.rs index 2a078ed..1e967d4 100644 --- a/crates/workshop-rs/tests/action_layout.rs +++ b/crates/workshop-rs/tests/action_layout.rs @@ -68,6 +68,7 @@ fn program_with_structured_actions() -> (wir::Program, Vec) { step: number, body: vec![nested_if_body], span: None, + target_span: None, }); let trailing_leaf = leaf(&mut program, number); let actions = vec![ diff --git a/crates/workshop-rs/tests/parser.rs b/crates/workshop-rs/tests/parser.rs index 419ed4c..795b917 100644 --- a/crates/workshop-rs/tests/parser.rs +++ b/crates/workshop-rs/tests/parser.rs @@ -390,6 +390,36 @@ fn spans_are_preserved() { assert!(action.span().is_some()); } +#[test] +fn player_variable_loop_preserves_target_span() { + let text = r#"variables { + player: + 0: I +} + +rule ("r") { + event { + Ongoing - Global; + } + actions { + For Player Variable(Host Player, I, 0, 3, 1); + End; + } +} +"#; + let program = parser::parse(&text, &catalog(), &Locale::new("en-US")).unwrap(); + let rule = program.rules.iter().next().unwrap(); + let action = program.actions.get(rule.actions[0]).unwrap(); + let wir::Action::ForPlayerVariable { target_span, .. } = action else { + panic!("expected For Player Variable"); + }; + let target_span = target_span.expect("player loop target span"); + assert_eq!(target_span.start.line, 11); + assert_eq!(target_span.start.col, 42); + assert_eq!(target_span.end.line, 11); + assert_eq!(target_span.end.col, 43); +} + #[test] fn malformed_input_is_reported_as_malformed() { // A rule-final If without `End;` is the oracle's valid spelling; an If From 4348ca5d913df744afcda8beb03e87431ebfcdfd Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 31 Aug 2026 12:10:08 +0800 Subject: [PATCH 2/2] test(parser): satisfy clippy in target span coverage Remove the needless borrow in the player loop target span test. --- crates/workshop-rs/tests/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workshop-rs/tests/parser.rs b/crates/workshop-rs/tests/parser.rs index 795b917..449e0e6 100644 --- a/crates/workshop-rs/tests/parser.rs +++ b/crates/workshop-rs/tests/parser.rs @@ -407,7 +407,7 @@ rule ("r") { } } "#; - let program = parser::parse(&text, &catalog(), &Locale::new("en-US")).unwrap(); + let program = parser::parse(text, &catalog(), &Locale::new("en-US")).unwrap(); let rule = program.rules.iter().next().unwrap(); let action = program.actions.get(rule.actions[0]).unwrap(); let wir::Action::ForPlayerVariable { target_span, .. } = action else {