1use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use crate::settings::table::{self, KeyKind, PathPart};
13use crate::settings::{Settings, SettingsListElement, SettingsNode};
14use crate::signatures::ExpectedDomain;
15use crate::source::{Position, SourceFile, Span};
16use crate::wir::{
17 self, Action, Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind, Value, ValueNode,
18};
19
20use crate::catalog::{Catalog, Kind, Locale};
21use crate::error::{Result, WorkshopError};
22use crate::lexer::{Token, TokenKind, tokenize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26enum Stop {
27 SectionClosed,
29 End,
31 ElseIf,
33 Else,
35}
36
37enum AssignmentOperator {
38 Set,
39 Modify(ModifyOp),
40}
41
42pub fn parse(input: &str, catalog: &Catalog, locale: &Locale) -> Result<wir::Program> {
48 parse_with_context(input, catalog, locale, catalog)
49}
50
51pub fn parse_with_context(
60 input: &str,
61 catalog: &Catalog,
62 locale: &Locale,
63 context: &dyn ExpectedDomain,
64) -> Result<wir::Program> {
65 let tokens = tokenize(input).map_err(|error| WorkshopError::Malformed {
66 message: error.message,
67 span: Some(synthetic_span(error.position)),
68 })?;
69 Parser {
70 tokens,
71 pos: 0,
72 catalog,
73 locale: locale.clone(),
74 context,
75 expected_domain: None,
76 call_stack: Vec::new(),
77 target: wir::Program::default(),
78 globals: HashMap::new(),
79 players: HashMap::new(),
80 subroutines: HashMap::new(),
81 }
82 .program()
83}
84
85fn synthetic_span(position: Position) -> Span {
87 Span::new(crate::ids::Id::from_index(0), position, position)
88}
89
90struct Parser<'a> {
91 tokens: Vec<Token>,
92 pos: usize,
93 catalog: &'a Catalog,
94 locale: Locale,
95 context: &'a dyn ExpectedDomain,
98 expected_domain: Option<&'a str>,
101 call_stack: Vec<String>,
102 target: wir::Program,
103 globals: HashMap<String, wir::GlobalVarId>,
104 players: HashMap<String, wir::PlayerVarId>,
105 subroutines: HashMap<String, wir::SubroutineId>,
106}
107
108impl Parser<'_> {
109 fn resolve_entry(&self, kind: Kind, spelling: &str) -> Option<crate::catalog::CatalogEntry> {
110 self.catalog
111 .resolve(kind, &self.locale, spelling)
112 .cloned()
113 .or_else(|| {
114 if self.locale != *self.catalog.primary_locale() {
115 self.catalog
116 .resolve(kind, self.catalog.primary_locale(), spelling)
117 .cloned()
118 } else {
119 None
120 }
121 })
122 }
123
124 fn canonical_keyword(&self, spelling: &str) -> String {
125 self.catalog
126 .resolve(Kind::Structural, &self.locale, spelling)
127 .or_else(|| {
128 (self.locale != *self.catalog.primary_locale()).then(|| {
129 self.catalog
130 .resolve(Kind::Structural, self.catalog.primary_locale(), spelling)
131 })?
132 })
133 .map(|entry| entry.id.clone())
134 .unwrap_or_else(|| canonical_keyword(spelling).to_string())
135 }
136
137 fn resolve_enum_domain_mixed(&self, spelling: &str) -> Option<&str> {
138 self.catalog
139 .resolve_enum_domain(&self.locale, spelling)
140 .or_else(|| {
141 if self.locale != *self.catalog.primary_locale() {
142 self.catalog
143 .resolve_enum_domain(self.catalog.primary_locale(), spelling)
144 } else {
145 None
146 }
147 })
148 }
149
150 fn resolve_enum_member_mixed(&self, domain: &str, spelling: &str) -> Option<(String, String)> {
151 let alternate = (!spelling.contains(": ") && spelling.contains(':'))
152 .then(|| spelling.replacen(':', ": ", 1));
153 self.catalog
154 .resolve_enum_member(domain, &self.locale, spelling)
155 .or_else(|| {
156 alternate.as_deref().and_then(|spelling| {
157 self.catalog
158 .resolve_enum_member(domain, &self.locale, spelling)
159 })
160 })
161 .or_else(|| {
162 if self.locale != *self.catalog.primary_locale() {
163 self.catalog.resolve_enum_member(
164 domain,
165 self.catalog.primary_locale(),
166 alternate.as_deref().unwrap_or(spelling),
167 )
168 } else {
169 None
170 }
171 })
172 }
173
174 fn program(mut self) -> Result<wir::Program> {
175 let file = self.target.files.push(SourceFile::new("workshop.txt"));
176 let _ = file;
178
179 loop {
180 let phrase = match self.peek() {
181 Some(Token {
182 kind: TokenKind::Word(word),
183 ..
184 }) => word.clone(),
185 Some(Token {
186 kind: TokenKind::Eof,
187 ..
188 }) => break,
189 Some(token) => {
190 return Err(self.malformed("expected a top-level section", &token));
191 }
192 None => break,
193 };
194 match self.canonical_keyword(&phrase).as_str() {
195 "settings" => self.settings_section()?,
196 "variables" => self.variables_section()?,
197 "subroutines" => self.subroutines_section()?,
198 "rule" => self.rule(false)?,
199 "disabled" => {
200 self.pos += 1;
201 self.rule(true)?;
202 }
203 other => {
204 return Err(self.unknown("top-level section", other));
205 }
206 }
207 }
208 Ok(self.target)
209 }
210
211 fn settings_section(&mut self) -> Result<()> {
212 let start = self.expect_keyword("settings")?;
213 self.expect(TokenKind::LBrace, "expected '{' after 'settings'")?;
214 let mut children = Vec::new();
215 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
216 let (display, child_start, _) = self.phrase()?;
217 let canonical_display = self.canonical_keyword(&display);
218 let name = match canonical_display.as_str() {
219 value
220 if value == "extensions"
221 || display == "扩展"
222 || self.settings_name_matches("labels", "Extensions", &display) =>
223 {
224 "extensions"
225 }
226 value
227 if value == "workshop"
228 || table::localized_name(
229 self.locale.as_str(),
230 "namespaces",
231 "workshop",
232 )
233 .is_some_and(|name| name == display) =>
234 {
235 "workshop"
236 }
237 value => value,
238 };
239 self.expect(TokenKind::LBrace, "expected '{' after settings group")?;
240 let node = match name {
241 "main" | "lobby" => SettingsNode::Group {
242 name: name.to_string(),
243 children: self.settings_members(
244 &[PathPart::Part(if name == "main" {
245 "main"
246 } else {
247 "lobby"
248 })],
249 None,
250 )?,
251 span: Some(self.settings_span(child_start)),
252 },
253 "modes" => self.settings_modes(child_start)?,
254 "heroes" => self.settings_heroes(child_start)?,
255 "extensions" => SettingsNode::Group {
256 name: "extensions".to_string(),
257 children: self.settings_members(&[PathPart::Part("extensions")], None)?,
258 span: Some(self.settings_span(child_start)),
259 },
260 "workshop" => SettingsNode::Workshop {
261 children: self.settings_opaque_members()?,
262 span: Some(self.settings_span(child_start)),
263 },
264 _ => self.settings_opaque_group(name, child_start)?,
265 };
266 children.push(node);
267 }
268 let end = match self.next() {
269 Some(Token {
270 kind: TokenKind::RBrace,
271 end,
272 ..
273 }) => end,
274 _ => unreachable!("settings loop checks for closing brace"),
275 };
276 self.target.settings = Some(Settings {
277 span: Some(Span::new(self.file(), start, end)),
278 children,
279 });
280 Ok(())
281 }
282
283 fn settings_modes(&mut self, start: Position) -> Result<SettingsNode> {
284 let mut children = Vec::new();
285 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
286 let mut disabled = false;
287 if let Some(Token {
288 kind: TokenKind::Word(word),
289 ..
290 }) = self.peek()
291 {
292 if self.settings_name_matches("tokens", "disabled", &word) {
293 self.pos += 1;
294 disabled = true;
295 }
296 }
297 let (display, mode_start, _) = self.phrase_on_line()?;
298 let mode = self
299 .resolve_settings_name_extended(
300 table::MODE_NAMES,
301 table::GENERATED_MODE_NAMES,
302 "modes",
303 &display,
304 )
305 .ok();
306 self.expect(TokenKind::LBrace, "expected '{' after game mode")?;
307 let mut mode_children = if let Some(mode) = mode {
308 self.settings_members(&[PathPart::Part("gamemodes"), PathPart::Part(mode)], None)?
309 } else {
310 self.settings_opaque_members()?
311 };
312 if disabled {
313 mode_children.insert(
314 0,
315 SettingsNode::Bool {
316 name: "enabled".to_string(),
317 value: false,
318 span: None,
319 },
320 );
321 }
322 children.push(SettingsNode::Group {
323 name: mode.map(str::to_string).unwrap_or(display),
324 children: mode_children,
325 span: Some(self.settings_span(mode_start)),
326 });
327 }
328 self.expect(TokenKind::RBrace, "expected '}' after modes")?;
329 Ok(SettingsNode::Group {
330 name: "gamemodes".to_string(),
331 children,
332 span: Some(self.settings_span(start)),
333 })
334 }
335
336 fn settings_heroes(&mut self, start: Position) -> Result<SettingsNode> {
337 let mut teams = Vec::new();
338 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
339 let (team_display, team_start, _) = self.phrase_on_line_with_colon()?;
340 let team = self.resolve_settings_name(table::TEAM_NAMES, "teams", &team_display)?;
341 self.expect(TokenKind::LBrace, "expected '{' after team settings group")?;
342 let mut team_children = Vec::new();
343 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
344 let (display, child_start, child_end) = self.phrase_on_line_with_colon()?;
345 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace))
346 && self
347 .resolve_settings_name_extended(
348 table::HERO_NAMES,
349 table::GENERATED_HERO_NAMES,
350 "heroes",
351 &display,
352 )
353 .is_ok()
354 {
355 let hero = self.resolve_settings_name_extended(
356 table::HERO_NAMES,
357 table::GENERATED_HERO_NAMES,
358 "heroes",
359 &display,
360 )?;
361 self.expect(TokenKind::LBrace, "expected '{' after hero settings group")?;
362 let children = self.settings_members(
363 &[PathPart::Part("heroes"), PathPart::Team, PathPart::Hero],
364 Some(hero),
365 )?;
366 team_children.push(SettingsNode::Group {
367 name: hero.to_string(),
368 children,
369 span: Some(self.settings_span(child_start)),
370 });
371 } else {
372 team_children.push(self.settings_member_named(
373 display,
374 child_start,
375 child_end,
376 &[PathPart::Part("heroes"), PathPart::Team],
377 None,
378 )?);
379 }
380 }
381 self.expect(TokenKind::RBrace, "expected '}' after team settings group")?;
382 teams.push(SettingsNode::Group {
383 name: team.to_string(),
384 children: team_children,
385 span: Some(self.settings_span(team_start)),
386 });
387 }
388 self.expect(TokenKind::RBrace, "expected '}' after heroes")?;
389 Ok(SettingsNode::Group {
390 name: "heroes".to_string(),
391 children: teams,
392 span: Some(self.settings_span(start)),
393 })
394 }
395
396 fn settings_members(
397 &mut self,
398 path: &[PathPart<'static>],
399 hero: Option<&str>,
400 ) -> Result<Vec<SettingsNode>> {
401 let mut children = Vec::new();
402 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
403 let (display, start, end) = self.phrase_on_line()?;
404 children.push(self.settings_member_named(display, start, end, path, hero)?);
405 }
406 self.expect(TokenKind::RBrace, "expected '}' after settings group")?;
407 Ok(children)
408 }
409
410 fn settings_member_named(
411 &mut self,
412 display: String,
413 start: Position,
414 _end: Position,
415 path: &[PathPart<'static>],
416 hero: Option<&str>,
417 ) -> Result<SettingsNode> {
418 let entry = table::entries().find(|candidate| {
419 candidate.path.len() == path.len() + 1
420 && candidate.path[..path.len()]
421 .iter()
422 .zip(path.iter())
423 .all(|(left, right)| left == right)
424 && self.settings_name_matches_for_path(candidate, &display, hero)
425 });
426 let Some(entry) = entry else {
427 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace)) {
428 self.pos += 1;
429 return self.settings_opaque_group(&display, start);
430 }
431 return self.settings_raw_member(display, start);
432 };
433 let name = match entry.path.last() {
434 Some(PathPart::Part(name)) => *name,
435 _ => return Err(self.malformed("settings entry has no leaf key", self.previous())),
436 };
437 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace)) {
438 self.expect(TokenKind::LBrace, "expected '{' after settings list")?;
439 let mut elements = Vec::new();
440 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
441 let (value, value_start, value_end) = self.phrase_on_line()?;
442 let canonical = match entry.kind {
443 KeyKind::ListMap => self
444 .resolve_settings_name_extended(
445 table::MAP_NAMES,
446 table::GENERATED_MAP_NAMES,
447 "maps",
448 &value,
449 )
450 .or_else(|_| {
451 value
452 .split_whitespace()
453 .next()
454 .and_then(|name| {
455 self.resolve_settings_name_extended(
456 table::MAP_NAMES,
457 table::GENERATED_MAP_NAMES,
458 "maps",
459 name,
460 )
461 .ok()
462 })
463 .ok_or_else(|| self.unknown("setting", &value))
464 })
465 .unwrap_or(value.as_str()),
466 KeyKind::ListHero => self
467 .resolve_settings_name_extended(
468 table::HERO_NAMES,
469 table::GENERATED_HERO_NAMES,
470 "heroes",
471 &value,
472 )
473 .unwrap_or(value.as_str()),
474 _ => {
475 return Err(
476 self.malformed("only settings lists may use braces", self.previous())
477 );
478 }
479 };
480 elements.push(SettingsListElement {
481 value: canonical.to_string(),
482 span: Some(Span::new(self.file(), value_start, value_end)),
483 });
484 }
485 self.expect(TokenKind::RBrace, "expected '}' after settings list")?;
486 return Ok(SettingsNode::List {
487 name: name.to_string(),
488 elements,
489 span: Some(Span::new(self.file(), start, self.previous_span().1)),
490 });
491 }
492 if matches!(entry.kind, KeyKind::Flag) {
493 return Ok(SettingsNode::Flag {
494 name: name.to_string(),
495 span: Some(Span::new(self.file(), start, self.previous_span().1)),
496 });
497 }
498 self.expect(TokenKind::Colon, "expected ':' after settings key")?;
499 let end = self.previous_span().1;
500 let span = Some(Span::new(self.file(), start, end));
501 match entry.kind {
502 KeyKind::Flag => unreachable!("presence-only settings returned before ':'"),
503 KeyKind::String => Ok(SettingsNode::String {
504 name: name.to_string(),
505 value: self.expect_string("expected a settings string")?,
506 span,
507 }),
508 KeyKind::Number => Ok(SettingsNode::Number {
509 name: name.to_string(),
510 value: self.settings_number(false)?,
511 span,
512 }),
513 KeyKind::Percent => Ok(SettingsNode::Number {
514 name: name.to_string(),
515 value: self.settings_number_percent()?,
516 span,
517 }),
518 KeyKind::Bool => Ok(SettingsNode::Bool {
519 name: name.to_string(),
520 value: self.settings_bool()?,
521 span,
522 }),
523 KeyKind::Enum(domain) => Ok(SettingsNode::String {
524 name: name.to_string(),
525 value: self.resolve_enum_settings_name(domain)?,
526 span,
527 }),
528 KeyKind::ListMap | KeyKind::ListHero => {
529 Err(self.malformed("settings list requires a brace block", self.previous()))
530 }
531 }
532 }
533
534 fn settings_opaque_group(&mut self, name: &str, start: Position) -> Result<SettingsNode> {
535 Ok(SettingsNode::Group {
536 name: name.to_string(),
537 children: self.settings_opaque_members()?,
538 span: Some(self.settings_span(start)),
539 })
540 }
541
542 fn settings_opaque_members(&mut self) -> Result<Vec<SettingsNode>> {
543 let mut children = Vec::new();
544 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
545 let (display, start, _) = self.opaque_name_on_line()?;
546 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace)) {
547 self.pos += 1;
548 children.push(self.settings_opaque_group(&display, start)?);
549 } else {
550 children.push(self.settings_raw_member(display, start)?);
551 }
552 }
553 self.expect(TokenKind::RBrace, "expected '}' after settings group")?;
554 Ok(children)
555 }
556
557 fn settings_raw_member(&mut self, name: String, start: Position) -> Result<SettingsNode> {
558 let mut value = String::new();
559 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::Colon)) {
560 self.pos += 1;
561 value = self.raw_settings_line()?;
562 }
563 let end = self.previous_span().1;
564 Ok(SettingsNode::Raw {
565 name,
566 value,
567 span: Some(Span::new(self.file(), start, end)),
568 })
569 }
570
571 fn opaque_name_on_line(&mut self) -> Result<(String, Position, Position)> {
572 let first = self
573 .peek()
574 .ok_or_else(|| self.malformed("expected an identifier", self.eof()))?;
575 let start = first.start;
576 let line = first.start.line;
577 let mut end = first.end;
578 let mut parts = Vec::new();
579 while let Some(token) = self.peek() {
580 if token.start.line != line
581 || matches!(
582 token.kind,
583 TokenKind::Colon | TokenKind::LBrace | TokenKind::RBrace
584 )
585 {
586 break;
587 }
588 self.pos += 1;
589 end = token.end;
590 parts.push(raw_token_text(&token.kind));
591 }
592 if parts.is_empty() {
593 return Err(self.malformed("expected an identifier", &first));
594 }
595 Ok((
596 parts
597 .join(" ")
598 .replace(" : ", ":")
599 .replace(" .", ".")
600 .replace(". ", "."),
601 start,
602 end,
603 ))
604 }
605
606 fn raw_settings_line(&mut self) -> Result<String> {
607 let line = self.peek().map(|token| token.start.line);
608 let mut parts = Vec::new();
609 while let Some(token) = self.peek() {
610 if line.is_some_and(|line| token.start.line != line)
611 || matches!(token.kind, TokenKind::RBrace)
612 {
613 break;
614 }
615 self.pos += 1;
616 parts.push(raw_token_text(&token.kind));
617 }
618 Ok(parts.join(" "))
619 }
620
621 fn settings_number(&mut self, percent: bool) -> Result<f64> {
622 let value = match self.next() {
623 Some(Token {
624 kind: TokenKind::Number { value, .. },
625 ..
626 }) => value,
627 Some(token) => return Err(self.malformed("expected a settings number", &token)),
628 None => return Err(self.malformed("expected a settings number", self.eof())),
629 };
630 if percent {
631 self.expect(
632 TokenKind::Op("%".to_string()),
633 "expected '%' after settings percentage",
634 )?;
635 }
636 Ok(value)
637 }
638
639 fn settings_number_percent(&mut self) -> Result<f64> {
640 let value = self.settings_number(false)?;
641 if matches!(
642 self.peek(),
643 Some(Token {
644 kind: TokenKind::Op(op),
645 ..
646 }) if op == "%"
647 ) {
648 self.pos += 1;
649 }
650 Ok(value)
651 }
652
653 fn settings_bool(&mut self) -> Result<bool> {
654 let token = self
655 .next()
656 .ok_or_else(|| self.malformed("expected a settings boolean", self.eof()))?;
657 let TokenKind::Word(value) = token.kind else {
658 return Err(self.malformed("expected a settings boolean", &token));
659 };
660 if self.settings_name_matches("tokens", "On", &value)
661 || self.settings_name_matches("tokens", "Yes", &value)
662 {
663 Ok(true)
664 } else if self.settings_name_matches("tokens", "Off", &value)
665 || self.settings_name_matches("tokens", "No", &value)
666 {
667 Ok(false)
668 } else {
669 Err(self.unknown("setting boolean", &value))
670 }
671 }
672
673 fn resolve_enum_settings_name(&mut self, domain: &str) -> Result<String> {
674 let (display, _, _) = self.phrase_on_line()?;
675 table::ENUM_MEMBERS
676 .iter()
677 .find(|member| {
678 member.domain == domain
679 && self.settings_name_matches("enums", member.name, &display)
680 })
681 .map(|member| member.member.to_string())
682 .or_else(|| {
683 table::GENERATED_ENUM_MEMBERS
684 .iter()
685 .find(|member| {
686 member.domain == domain
687 && self.settings_name_matches("enums", member.name, &display)
688 })
689 .map(|member| member.member.to_string())
690 })
691 .ok_or_else(|| self.unknown("settings enum", &display))
692 }
693
694 fn resolve_settings_name(
695 &self,
696 names: &[table::NameMap],
697 section: &str,
698 display: &str,
699 ) -> Result<&'static str> {
700 names
701 .iter()
702 .find(|candidate| self.settings_name_matches(section, candidate.name, display))
703 .map(|candidate| candidate.key)
704 .ok_or_else(|| self.unknown("setting", display))
705 }
706
707 fn resolve_settings_name_extended(
708 &self,
709 names: &[table::NameMap],
710 generated: &[table::NameMap],
711 section: &str,
712 display: &str,
713 ) -> Result<&'static str> {
714 names
715 .iter()
716 .chain(generated.iter())
717 .find(|candidate| self.settings_name_matches(section, candidate.name, display))
718 .map(|candidate| candidate.key)
719 .ok_or_else(|| self.unknown("setting", display))
720 }
721
722 fn settings_name_matches_for_path(
723 &self,
724 candidate: &table::TableEntry,
725 display: &str,
726 hero: Option<&str>,
727 ) -> bool {
728 if let Some(PathPart::Part(key)) = candidate.path.last() {
729 if display == *key {
730 return true;
731 }
732 }
733 if let (Some(hero), Some(PathPart::Part(key))) = (hero, candidate.path.last()) {
734 if table::hero_setting_name(hero, key, self.locale.as_str()) == Some(display) {
735 return true;
736 }
737 if table::hero_setting_alias(hero, key, self.locale.as_str(), display) {
738 return true;
739 }
740 }
741 if let (Some(hero), Some(slot)) = (hero, table::ability_slot_for_path(candidate.path)) {
742 if candidate.workshop_name.contains("%1$s")
743 || matches!(
744 candidate.path.last(),
745 Some(PathPart::Part("enableAbility1" | "enableAbility2"))
746 )
747 {
748 return crate::gameplay_data::builtin()
749 .ok()
750 .and_then(|catalog| {
751 catalog
752 .query()
753 .ability_name(hero, slot, None, self.locale.as_str())
754 .ok()
755 .map(|name| name == display)
756 })
757 .unwrap_or(false);
758 }
759 }
760 self.settings_name_matches("labels", candidate.workshop_name, display)
761 }
762
763 fn settings_name_matches(&self, section: &str, english: &str, display: &str) -> bool {
764 let localized = table::localized_name(self.locale.as_str(), section, english);
765 localized
766 .is_some_and(|localized| localized == display)
767 || display == english
772 }
773
774 fn settings_span(&self, start: Position) -> Span {
775 Span::new(self.file(), start, self.previous_span().1)
776 }
777
778 fn variables_section(&mut self) -> Result<()> {
779 self.expect_keyword("variables")?;
780 self.expect(TokenKind::LBrace, "expected '{' after 'variables'")?;
781 let mut saw_section = false;
782 loop {
783 match self.peek() {
784 Some(Token {
785 kind: TokenKind::RBrace,
786 ..
787 }) => {
788 self.pos += 1;
789 break;
790 }
791 Some(Token {
792 kind: TokenKind::Word(word),
793 ..
794 }) if matches!(canonical_keyword(&word), "Global" | "global") => {
795 self.pos += 1;
796 self.expect(TokenKind::Colon, "expected ':' after 'global'")?;
797 while let Some(Token {
798 kind: TokenKind::Number { .. },
799 ..
800 }) = self.peek()
801 {
802 let variable = self.variable_line()?;
803 let id = self.target.global_variables.push(variable);
804 self.globals.insert(
805 self.target.global_variables.get(id).unwrap().name.clone(),
806 id,
807 );
808 }
809 saw_section = true;
810 }
811 Some(Token {
812 kind: TokenKind::Word(word),
813 ..
814 }) if canonical_keyword(&word) == "player" => {
815 self.pos += 1;
816 self.expect(TokenKind::Colon, "expected ':' after 'player'")?;
817 while let Some(Token {
818 kind: TokenKind::Number { .. },
819 ..
820 }) = self.peek()
821 {
822 let variable = self.variable_line()?;
823 let id = self.target.player_variables.push(variable);
824 self.players.insert(
825 self.target.player_variables.get(id).unwrap().name.clone(),
826 id,
827 );
828 }
829 saw_section = true;
830 }
831 Some(token) => {
832 return Err(self.malformed("expected 'global', 'player', or '}'", &token));
833 }
834 None => {
835 return Err(self.malformed("unexpected end of input in variables", self.eof()));
836 }
837 }
838 }
839 if !saw_section {
840 return Err(self.malformed("variables section is empty", self.previous()));
841 }
842 Ok(())
843 }
844
845 fn variable_line(&mut self) -> Result<wir::WorkshopVariable> {
846 let (index, span) = match self.next() {
847 Some(Token {
848 kind: TokenKind::Number { value, .. },
849 start,
850 end,
851 }) => (
852 value as u32,
853 Span::new(synthetic_span(start).file, start, end),
854 ),
855 Some(token) => return Err(self.malformed("expected a variable index", &token)),
856 None => return Err(self.malformed("expected a variable index", self.eof())),
857 };
858 self.expect(TokenKind::Colon, "expected ':' after variable index")?;
859 let (name, name_start, name_end) = self.phrase_on_line()?;
860 let name_span = Span::new(self.file(), name_start, name_end);
861 Ok(wir::WorkshopVariable {
862 name,
863 index,
864 span: Some(if span.file.index() == 0 {
865 name_span
866 } else {
867 span
868 }),
869 name_span: None,
872 })
873 }
874
875 fn subroutines_section(&mut self) -> Result<()> {
876 self.expect_keyword("subroutines")?;
877 self.expect(TokenKind::LBrace, "expected '{' after 'subroutines'")?;
878 while let Some(Token {
879 kind: TokenKind::Number { .. },
880 ..
881 }) = self.peek()
882 {
883 let index = match self.next() {
884 Some(Token {
885 kind: TokenKind::Number { value, .. },
886 ..
887 }) => value as u32,
888 _ => unreachable!(),
889 };
890 self.expect(TokenKind::Colon, "expected ':' after subroutine index")?;
891 let (name, start, end) = self.phrase_on_line()?;
892 let id = self.target.subroutines.push(wir::WorkshopSubroutine {
893 name,
894 index,
895 span: Some(Span::new(self.file(), start, end)),
896 name_span: None,
897 });
898 self.subroutines
899 .insert(self.target.subroutines.get(id).unwrap().name.clone(), id);
900 }
901 self.expect(TokenKind::RBrace, "expected '}' after subroutines")?;
902 Ok(())
903 }
904
905 fn rule(&mut self, disabled: bool) -> Result<()> {
906 self.expect_keyword("rule")?;
907 self.expect(TokenKind::LParen, "expected '(' after 'rule'")?;
908 let name = self.expect_string("expected a rule name string")?;
909 self.expect(TokenKind::RParen, "expected ')' after rule name")?;
910 let (rule_start, rule_end) = self.previous_span();
911 self.expect(TokenKind::LBrace, "expected '{' after rule header")?;
912
913 let mut rule = wir::Rule {
914 name,
915 span: Some(Span::new(self.file(), rule_start, rule_end)),
916 name_span: None,
917 disabled,
918 event: Event::Global,
919 conditions: Vec::new(),
920 actions: Vec::new(),
921 };
922 let mut seen_sections = Vec::new();
923 loop {
924 match self.peek() {
925 Some(Token {
926 kind: TokenKind::RBrace,
927 ..
928 }) => {
929 self.pos += 1;
930 break;
931 }
932 Some(Token {
933 kind: TokenKind::Word(word),
934 ..
935 }) => match self.canonical_keyword(&word).as_str() {
936 "event" => {
937 if seen_sections.contains(&"event") {
938 return Err(
939 self.malformed("duplicate 'event' section", &self.peek().unwrap())
940 );
941 }
942 seen_sections.push("event");
943 rule.event = self.event_section()?;
944 }
945 "conditions" => {
946 if seen_sections.contains(&"conditions") {
947 return Err(self.malformed(
948 "duplicate 'conditions' section",
949 &self.peek().unwrap(),
950 ));
951 }
952 seen_sections.push("conditions");
953 rule.conditions = self.conditions_section()?;
954 }
955 "actions" => {
956 if seen_sections.contains(&"actions") {
957 return Err(self
958 .malformed("duplicate 'actions' section", &self.peek().unwrap()));
959 }
960 seen_sections.push("actions");
961 rule.actions = self.actions_section()?;
962 }
963 _ => return Err(self.unknown("rule section", &word)),
964 },
965 Some(token) => {
966 return Err(self.malformed("expected a rule section or '}'", &token));
967 }
968 None => return Err(self.malformed("unexpected end of input in rule", self.eof())),
969 }
970 }
971 self.target.rules.push(rule);
972 Ok(())
973 }
974
975 fn event_section(&mut self) -> Result<Event> {
976 self.expect_keyword("event")?;
977 self.expect(TokenKind::LBrace, "expected '{' after 'event'")?;
978 let mut lines: Vec<String> = Vec::new();
979 loop {
980 match self.peek() {
981 Some(Token {
982 kind: TokenKind::RBrace,
983 ..
984 }) => {
985 self.pos += 1;
986 break;
987 }
988 Some(Token {
989 kind: TokenKind::Semi,
990 ..
991 }) => {
992 self.pos += 1;
993 lines.push(String::new());
994 }
995 Some(_) => {
996 let text = self.line_text()?;
997 lines.push(text);
998 }
999 None => return Err(self.malformed("unexpected end of input in event", self.eof())),
1000 }
1001 }
1002 let Some(name_line) = lines.first().cloned() else {
1003 return Err(self.malformed("event section is empty", self.previous()));
1004 };
1005 let name_line = name_line.trim();
1006 let entry = self
1007 .catalog
1008 .resolve(Kind::Event, &self.locale, name_line)
1009 .ok_or_else(|| WorkshopError::Unknown {
1010 kind: "event",
1011 spelling: name_line.to_string(),
1012 locale: self.locale.clone(),
1013 span: None,
1014 })?;
1015 match entry.id.as_str() {
1016 "global" => {
1017 if lines[1..].iter().any(|line| !line.trim().is_empty()) {
1018 return Err(self.unsupported_event_parameters("global"));
1019 }
1020 Ok(Event::Global)
1021 }
1022 "eachPlayer" => {
1023 if lines[1..].iter().all(|line| line.trim().is_empty()) {
1024 return Ok(Event::EachPlayer);
1025 }
1026 let (team, target) = self.event_filters(&lines, "eachPlayer", true)?;
1027 Ok(Event::EachPlayerWithFilters { team, target })
1028 }
1029 "playerDealtDamage" => self.player_event(&lines, PlayerEventKind::DealtDamage),
1030 "playerDealtFinalBlow" => self.player_event(&lines, PlayerEventKind::DealtFinalBlow),
1031 "playerDealtHealing" => self.player_event(&lines, PlayerEventKind::DealtHealing),
1032 "playerDealtKnockback" => self.player_event(&lines, PlayerEventKind::DealtKnockback),
1033 "playerDied" => self.player_event(&lines, PlayerEventKind::Died),
1034 "playerEarnedElimination" => {
1035 self.player_event(&lines, PlayerEventKind::EarnedElimination)
1036 }
1037 "playerJoined" => self.player_event(&lines, PlayerEventKind::Joined),
1038 "playerLeft" => self.player_event(&lines, PlayerEventKind::Left),
1039 "playerReceivedHealing" => self.player_event(&lines, PlayerEventKind::ReceivedHealing),
1040 "playerReceivedKnockback" => {
1041 self.player_event(&lines, PlayerEventKind::ReceivedKnockback)
1042 }
1043 "playerTookDamage" => self.player_event(&lines, PlayerEventKind::TookDamage),
1044 "subroutine" => {
1045 if lines
1046 .get(2..)
1047 .unwrap_or(&[])
1048 .iter()
1049 .any(|line| !line.trim().is_empty())
1050 {
1051 return Err(self.unsupported_event_parameters("subroutine"));
1052 }
1053 let Some(sub_name) = lines.get(1).map(|s| s.trim()) else {
1054 return Err(self.malformed(
1055 "subroutine event requires a subroutine name",
1056 self.previous(),
1057 ));
1058 };
1059 let id = self.subroutine_by_name(sub_name)?;
1060 Ok(Event::Subroutine(id))
1061 }
1062 other => Err(WorkshopError::Unsupported {
1063 message: format!("unsupported event '{other}'"),
1064 span: None,
1065 }),
1066 }
1067 }
1068
1069 fn player_event(&self, lines: &[String], kind: PlayerEventKind) -> Result<Event> {
1070 let (team, target) = self.event_filters(lines, kind.catalog_id(), false)?;
1071 Ok(Event::Player { kind, team, target })
1072 }
1073
1074 fn event_filters(
1075 &self,
1076 lines: &[String],
1077 event_id: &str,
1078 allow_empty: bool,
1079 ) -> Result<(EventTeam, EventTarget)> {
1080 let parameters: Vec<&str> = lines[1..]
1081 .iter()
1082 .map(String::as_str)
1083 .map(str::trim)
1084 .filter(|line| !line.is_empty())
1085 .collect();
1086 if parameters.is_empty() {
1087 if allow_empty {
1088 return Ok((EventTeam::All, EventTarget::All));
1089 }
1090 return Err(WorkshopError::Malformed {
1091 message: format!("event '{event_id}' requires team and player parameters"),
1092 span: None,
1093 });
1094 }
1095 if parameters.len() != 2 {
1096 if event_id == "eachPlayer" {
1097 return Err(WorkshopError::Unsupported {
1098 message: format!("event '{event_id}' requires both team and player parameters"),
1099 span: None,
1100 });
1101 }
1102 return Err(WorkshopError::Malformed {
1103 message: format!("event '{event_id}' requires team and player parameters"),
1104 span: None,
1105 });
1106 }
1107 let team_member = self
1108 .resolve_enum_member_mixed("EventTeam", parameters[0])
1109 .map(|(_, member)| member);
1110 let team = match team_member.as_deref() {
1111 Some("ALL") => EventTeam::All,
1112 Some("TEAM_1") => EventTeam::Team1,
1113 Some("TEAM_2") => EventTeam::Team2,
1114 _ => return Err(self.unknown("event team", parameters[0])),
1115 };
1116 let target = if let Some((_, member)) =
1117 self.resolve_enum_member_mixed("EventPlayer", parameters[1])
1118 {
1119 if member == "ALL" {
1120 EventTarget::All
1121 } else if let Some(slot) = member.strip_prefix("SLOT_") {
1122 let slot = slot
1123 .parse::<u8>()
1124 .map_err(|_| self.unknown("event player", parameters[1]))?;
1125 EventTarget::Slot(slot)
1126 } else {
1127 return Err(self.unknown("event player", parameters[1]));
1128 }
1129 } else if let Some((_, hero)) = self
1130 .catalog
1131 .bare_member_matches(&self.locale, parameters[1])
1132 .into_iter()
1133 .chain(
1134 self.catalog
1135 .bare_member_matches(&self.locale, ¶meters[1].replace(':', ": ")),
1136 )
1137 .find(|(domain, _)| domain == "Hero")
1138 {
1139 EventTarget::Hero(hero)
1140 } else {
1141 return Err(self.unknown("event player", parameters[1]));
1142 };
1143 Ok((team, target))
1144 }
1145
1146 fn unsupported_event_parameters(&self, event_id: &str) -> WorkshopError {
1147 WorkshopError::Unsupported {
1148 message: format!("event '{event_id}' does not accept parameters"),
1149 span: None,
1150 }
1151 }
1152
1153 fn conditions_section(&mut self) -> Result<Vec<wir::ValueId>> {
1154 self.expect_keyword("conditions")?;
1155 self.expect(TokenKind::LBrace, "expected '{' after 'conditions'")?;
1156 let mut conditions = Vec::new();
1157 loop {
1158 match self.peek() {
1159 Some(Token {
1160 kind: TokenKind::RBrace,
1161 ..
1162 }) => {
1163 self.pos += 1;
1164 break;
1165 }
1166 Some(Token {
1167 kind: TokenKind::Semi,
1168 ..
1169 }) => {
1170 self.pos += 1;
1171 }
1172 Some(_) => {
1173 if matches!(
1174 self.peek(),
1175 Some(Token {
1176 kind: TokenKind::String(_),
1177 ..
1178 })
1179 ) {
1180 self.pos += 1;
1181 continue;
1182 }
1183 if let Some(Token {
1184 kind: TokenKind::Word(word),
1185 ..
1186 }) = self.peek()
1187 {
1188 if self.settings_name_matches("tokens", "disabled", &word) {
1189 self.pos += 1;
1190 let _disabled_condition = self.value()?;
1191 self.expect(TokenKind::Semi, "expected ';' after condition")?;
1192 continue;
1193 }
1194 }
1195 let condition = self.value()?;
1196 self.expect(TokenKind::Semi, "expected ';' after condition")?;
1197 conditions.push(condition);
1198 }
1199 None => {
1200 return Err(self.malformed("unexpected end of input in conditions", self.eof()));
1201 }
1202 }
1203 }
1204 Ok(conditions)
1205 }
1206
1207 fn actions_section(&mut self) -> Result<Vec<wir::ActionId>> {
1208 self.expect_keyword("actions")?;
1209 self.expect(TokenKind::LBrace, "expected '{' after 'actions'")?;
1210 let mut all_actions = Vec::new();
1211 loop {
1212 let (actions, stop) = self.actions_until_end()?;
1213 all_actions.extend(actions);
1214 if stop == Stop::SectionClosed {
1215 return Ok(all_actions);
1216 }
1217 all_actions.push(self.opaque_action()?);
1222 }
1223 }
1224
1225 fn actions_until_end(&mut self) -> Result<(Vec<wir::ActionId>, Stop)> {
1229 let mut actions = Vec::new();
1230 loop {
1231 match self.peek() {
1232 Some(Token {
1233 kind: TokenKind::RBrace,
1234 ..
1235 }) => {
1236 self.pos += 1;
1237 return Ok((actions, Stop::SectionClosed));
1238 }
1239 Some(Token {
1240 kind: TokenKind::Word(_),
1241 ..
1242 }) => {
1243 if let Some(action) = self.assignment_action()? {
1244 actions.push(action);
1245 continue;
1246 }
1247 let saved = self.pos;
1248 let (phrase, start, end) = self.phrase()?;
1249 if let Some(rest) = self.disabled_action_rest(&phrase) {
1250 if let Some(structural) = self.resolve_entry(Kind::Structural, rest) {
1251 match structural.id.as_str() {
1252 "while" => {
1253 actions.push(self.while_group()?);
1254 continue;
1255 }
1256 "if" => {
1257 actions.push(self.if_group()?);
1258 continue;
1259 }
1260 _ => {}
1261 }
1262 }
1263 if self.resolve_entry(Kind::Action, rest).is_some() {
1264 let canonical = rest;
1265 actions.push(self.action_call_from_phrase(
1266 canonical.to_string(),
1267 start,
1268 end,
1269 )?);
1270 continue;
1271 }
1272 }
1273 if canonical_keyword(&phrase) == "disabled"
1274 && matches!(
1275 self.peek(),
1276 Some(Token {
1277 kind: TokenKind::Word(word),
1278 ..
1279 }) if canonical_keyword(&word) == "While"
1280 )
1281 {
1282 self.pos += 1;
1283 actions.push(self.while_group()?);
1284 continue;
1285 }
1286 match self.canonical_keyword(&phrase).as_str() {
1287 "end" => {
1288 self.pos = saved;
1289 return Ok((actions, Stop::End));
1290 }
1291 "elseIf" => {
1292 self.pos = saved;
1293 return Ok((actions, Stop::ElseIf));
1294 }
1295 "else" => {
1296 self.pos = saved;
1297 return Ok((actions, Stop::Else));
1298 }
1299 "if" => actions.push(self.if_group()?),
1300 "forGlobalVariable" => actions.push(self.for_group()?),
1301 "forPlayerVariable" => actions.push(self.for_player_group()?),
1302 "while" => actions.push(self.while_group()?),
1303 "Loop" => actions.push(self.action_call_from_phrase(phrase, start, end)?),
1304 "Loop If Condition Is True" => {
1305 actions.push(self.action_call_from_phrase(phrase, start, end)?)
1306 }
1307 "Global" | "Event Player" => {
1308 self.pos = saved;
1309 actions.push(self.opaque_action()?);
1310 }
1311 _ => {
1312 if self.line_has_assignment() {
1313 self.pos = saved;
1314 actions.push(self.opaque_action()?);
1315 } else {
1316 actions.push(self.action_call_from_phrase(phrase, start, end)?);
1317 }
1318 }
1319 }
1320 }
1321 Some(Token {
1322 kind: TokenKind::String(_),
1323 ..
1324 }) => {
1325 self.pos += 1;
1329 }
1330 Some(token) => {
1331 let saved = self.pos;
1332 if let Some(action) = self.member_assignment_action(saved, token.start)? {
1333 actions.push(action);
1334 } else {
1335 return Err(self.malformed("expected an action", &token));
1336 }
1337 }
1338 None => {
1339 return Err(self.malformed("unexpected end of input in actions", self.eof()));
1340 }
1341 }
1342 }
1343 }
1344
1345 fn disabled_action_rest<'a>(&self, phrase: &'a str) -> Option<&'a str> {
1349 if let Some(rest) = phrase.strip_prefix("disabled ") {
1350 return Some(rest);
1351 }
1352 let localized = table::localized_name(self.locale.as_str(), "tokens", "disabled")?;
1353 phrase.strip_prefix(localized)?.strip_prefix(' ')
1354 }
1355
1356 fn assignment_action(&mut self) -> Result<Option<wir::ActionId>> {
1357 let saved = self.pos;
1358 let Some(Token {
1359 kind: TokenKind::Word(first),
1360 start,
1361 ..
1362 }) = self.peek()
1363 else {
1364 return Ok(None);
1365 };
1366
1367 if matches!(canonical_keyword(&first), "Global" | "global") {
1368 self.pos += 1;
1369 self.expect(TokenKind::Dot, "expected '.' after 'Global'")?;
1370 let (name, _, target_end) = self.phrase()?;
1371 if matches!(
1372 self.peek().map(|token| token.kind),
1373 Some(TokenKind::LBracket)
1374 ) {
1375 self.pos += 1;
1376 let index = self.value()?;
1377 self.expect(
1378 TokenKind::RBracket,
1379 "expected ']' after global variable index",
1380 )?;
1381 let operator = self.assignment_operator()?.ok_or_else(|| {
1382 self.malformed(
1383 "expected assignment after global variable index",
1384 self.peek().as_ref().unwrap_or(self.eof()),
1385 )
1386 })?;
1387 let variable = self.global_by_name(&name)?;
1388 let target = self.target.values.push(ValueNode::new(
1389 Value::GlobalVariable(variable),
1390 Some(Span::new(self.file(), start, target_end)),
1391 ));
1392 let value = self.value()?;
1393 self.expect(TokenKind::Semi, "expected ';' after indexed assignment")?;
1394 return Ok(Some(self.indexed_assignment_action(
1395 true, target, index, operator, value, start,
1396 )));
1397 }
1398 let Some(operator) = self.assignment_operator()? else {
1399 return self.member_assignment_action(saved, start);
1400 };
1401 let variable = self.global_by_name(&name)?;
1402 let value = self.value()?;
1403 self.expect(TokenKind::Semi, "expected ';' after assignment")?;
1404 let span = Some(Span::new(self.file(), start, self.previous_span().1));
1405 let target_span = Some(Span::new(self.file(), start, target_end));
1406 return Ok(Some(self.target.actions.push(match operator {
1407 AssignmentOperator::Set => Action::SetGlobalVariable {
1408 variable,
1409 value,
1410 span,
1411 target_span,
1412 },
1413 AssignmentOperator::Modify(op) => Action::ModifyGlobalVariable {
1414 variable,
1415 op,
1416 value,
1417 span,
1418 target_span,
1419 },
1420 })));
1421 }
1422
1423 let first_canonical = canonical_keyword(&first);
1424 let is_event_player = first_canonical == "Event Player"
1425 || (matches!(first_canonical, "Event" | "event")
1426 && matches!(
1427 self.peek_at(1).map(|token| token.kind),
1428 Some(TokenKind::Word(word))
1429 if matches!(canonical_keyword(&word), "Player" | "player")
1430 ));
1431 if !is_event_player {
1432 if !matches!(first_canonical, "Event" | "event") {
1439 return self.member_assignment_action(saved, start);
1440 }
1441 return Ok(None);
1442 }
1443
1444 if first_canonical == "Event Player" {
1445 self.pos += 1;
1446 } else {
1447 self.pos += 2;
1448 }
1449 let event_player = self.target.values.push(ValueNode::new(
1450 Value::EventPlayer,
1451 Some(Span::new(self.file(), start, self.previous_span().1)),
1452 ));
1453 self.expect(TokenKind::Dot, "expected '.' after 'Event Player'")?;
1454 let (name, target_start, target_end) = self.phrase()?;
1455 let variable = self.player_by_name(&name)?;
1456 if matches!(
1457 self.peek().map(|token| token.kind),
1458 Some(TokenKind::LBracket)
1459 ) {
1460 self.pos += 1;
1461 let index = self.value()?;
1462 self.expect(
1463 TokenKind::RBracket,
1464 "expected ']' after player variable index",
1465 )?;
1466 let operator = self.assignment_operator()?.ok_or_else(|| {
1467 self.malformed(
1468 "expected assignment after player variable index",
1469 self.peek().as_ref().unwrap_or(self.eof()),
1470 )
1471 })?;
1472 let value = self.value()?;
1473 self.expect(TokenKind::Semi, "expected ';' after indexed assignment")?;
1474 let variable_value = self.target.values.push(ValueNode::new(
1475 Value::PlayerVariable {
1476 player: event_player,
1477 variable,
1478 },
1479 Some(Span::new(self.file(), target_start, target_end)),
1480 ));
1481 return Ok(Some(self.indexed_assignment_action(
1482 false,
1483 variable_value,
1484 index,
1485 operator,
1486 value,
1487 start,
1488 )));
1489 }
1490 let Some(operator) = self.assignment_operator()? else {
1491 return self.member_assignment_action(saved, start);
1492 };
1493 let value = self.value()?;
1494 self.expect(TokenKind::Semi, "expected ';' after assignment")?;
1495 let span = Some(Span::new(self.file(), start, self.previous_span().1));
1496 let target_span = Some(Span::new(self.file(), target_start, target_end));
1497 Ok(Some(self.target.actions.push(match operator {
1498 AssignmentOperator::Set => Action::SetPlayerVariable {
1499 player: event_player,
1500 variable,
1501 value,
1502 span,
1503 target_span,
1504 },
1505 AssignmentOperator::Modify(op) => Action::ModifyPlayerVariable {
1506 player: event_player,
1507 variable,
1508 op,
1509 value,
1510 span,
1511 target_span,
1512 },
1513 })))
1514 }
1515
1516 fn member_assignment_action(
1517 &mut self,
1518 saved: usize,
1519 start: Position,
1520 ) -> Result<Option<wir::ActionId>> {
1521 self.pos = saved;
1522 if !self.line_has_assignment() {
1523 return Ok(None);
1524 }
1525 let target = self.value()?;
1526 let Some(operator) = self.assignment_operator()? else {
1527 self.pos = saved;
1528 return Ok(None);
1529 };
1530 let value = self.value()?;
1531 self.expect(TokenKind::Semi, "expected ';' after member assignment")?;
1532 let op = match operator {
1533 AssignmentOperator::Set => None,
1534 AssignmentOperator::Modify(op) => Some(op),
1535 };
1536 Ok(Some(self.target.actions.push(Action::AssignMember {
1537 target,
1538 op,
1539 value,
1540 span: Some(Span::new(self.file(), start, self.previous_span().1)),
1541 })))
1542 }
1543
1544 fn indexed_assignment_action(
1545 &mut self,
1546 global: bool,
1547 variable: wir::ValueId,
1548 index: wir::ValueId,
1549 operator: AssignmentOperator,
1550 value: wir::ValueId,
1551 start: Position,
1552 ) -> wir::ActionId {
1553 let (name, args) = match operator {
1554 AssignmentOperator::Set => (
1555 if global {
1556 "setGlobalVariableAtIndex"
1557 } else {
1558 "setPlayerVariableAtIndex"
1559 },
1560 vec![variable, index, value],
1561 ),
1562 AssignmentOperator::Modify(op) => (
1563 if global {
1564 "modifyGlobalVariableAtIndex"
1565 } else {
1566 "modifyPlayerVariableAtIndex"
1567 },
1568 vec![
1569 variable,
1570 index,
1571 self.target.values.push(ValueNode::new(
1572 Value::Call {
1573 name: op.catalog_id().to_string(),
1574 args: Vec::new(),
1575 },
1576 None,
1577 )),
1578 value,
1579 ],
1580 ),
1581 };
1582 self.target.actions.push(Action::Call {
1583 name: name.to_string(),
1584 args,
1585 span: Some(Span::new(self.file(), start, self.previous_span().1)),
1586 })
1587 }
1588
1589 fn assignment_operator(&mut self) -> Result<Option<AssignmentOperator>> {
1590 let Some(token) = self.peek() else {
1591 return Ok(None);
1592 };
1593 if let TokenKind::Word(word) = &token.kind {
1594 let op = match word.as_str() {
1595 "min" => ModifyOp::Min,
1596 "max" => ModifyOp::Max,
1597 _ => {
1598 if matches!(self.peek_at(1).map(|token| token.kind), Some(TokenKind::Op(equal)) if equal == "=")
1599 {
1600 return Err(WorkshopError::Unsupported {
1601 message: format!("unsupported assignment operator '{word}='"),
1602 span: Some(Span::new(
1603 self.file(),
1604 token.start,
1605 self.peek_at(1).unwrap().end,
1606 )),
1607 });
1608 }
1609 return Ok(None);
1610 }
1611 };
1612 if !matches!(self.peek_at(1).map(|token| token.kind), Some(TokenKind::Op(equal)) if equal == "=")
1613 {
1614 return Ok(None);
1615 }
1616 self.pos += 2;
1617 return Ok(Some(AssignmentOperator::Modify(op)));
1618 }
1619 let operator = match &token.kind {
1620 TokenKind::Op(operator) => operator.clone(),
1621 _ => return Ok(None),
1622 };
1623 if operator == "=" {
1624 self.pos += 1;
1625 return Ok(Some(AssignmentOperator::Set));
1626 }
1627 let op = match operator.as_str() {
1628 "+" => ModifyOp::Add,
1629 "-" => ModifyOp::Subtract,
1630 "*" => ModifyOp::Multiply,
1631 "/" => ModifyOp::Divide,
1632 "%" => ModifyOp::Modulo,
1633 _ => return Ok(None),
1634 };
1635 if !matches!(self.peek_at(1).map(|token| token.kind), Some(TokenKind::Op(equal)) if equal == "=")
1636 {
1637 return Ok(None);
1638 }
1639 self.pos += 2;
1640 Ok(Some(AssignmentOperator::Modify(op)))
1641 }
1642
1643 fn opaque_action(&mut self) -> Result<wir::ActionId> {
1644 let start = self
1645 .peek()
1646 .map(|token| token.start)
1647 .unwrap_or(self.eof().start);
1648 while let Some(token) = self.peek() {
1649 self.pos += 1;
1650 if matches!(token.kind, TokenKind::Semi) {
1651 break;
1652 }
1653 }
1654 Ok(self.target.actions.push(Action::Call {
1655 name: "rawWorkshopAction".to_string(),
1656 args: Vec::new(),
1657 span: Some(Span::new(self.file(), start, self.previous_span().1)),
1658 }))
1659 }
1660
1661 fn if_group(&mut self) -> Result<wir::ActionId> {
1662 let start = self.previous_span().0;
1663 self.expect(TokenKind::LParen, "expected '(' after 'If'")?;
1664 let condition = self.value()?;
1665 self.expect(TokenKind::RParen, "expected ')' after If condition")?;
1666 self.expect(TokenKind::Semi, "expected ';' after If condition")?;
1667
1668 let mut branches = Vec::new();
1669 let mut stop = {
1670 let (body, stop) = self.actions_until_end()?;
1671 branches.push(wir::IfBranch { condition, body });
1672 stop
1673 };
1674
1675 let mut else_body = None;
1676 loop {
1677 match stop {
1678 Stop::End => {
1679 self.consume_phrase("End")?;
1680 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1681 break;
1682 }
1683 Stop::ElseIf => {
1684 self.consume_phrase("Else If")?;
1685 self.expect(TokenKind::LParen, "expected '(' after 'Else If'")?;
1686 let condition = self.value()?;
1687 self.expect(TokenKind::RParen, "expected ')' after Else If condition")?;
1688 self.expect(TokenKind::Semi, "expected ';' after Else If condition")?;
1689 let (body, next) = self.actions_until_end()?;
1690 branches.push(wir::IfBranch { condition, body });
1691 stop = next;
1692 }
1693 Stop::Else => {
1694 self.consume_phrase("Else")?;
1695 self.expect(TokenKind::Semi, "expected ';' after 'Else'")?;
1696 let (body, next) = self.actions_until_end()?;
1697 else_body = Some(body);
1698 stop = next;
1699 }
1700 Stop::SectionClosed => {
1701 self.pos -= 1;
1706 break;
1707 }
1708 }
1709 }
1710 let end_span = self.previous_span();
1711 let action = Action::If {
1712 branches,
1713 else_body,
1714 span: Some(Span::new(self.file(), start, end_span.1)),
1715 };
1716 Ok(self.target.actions.push(action))
1717 }
1718
1719 fn for_group(&mut self) -> Result<wir::ActionId> {
1720 let start = self.previous_span().0;
1721 self.expect(
1722 TokenKind::LParen,
1723 "expected '(' after 'For Global Variable'",
1724 )?;
1725 let (name, _, _) = self.phrase()?;
1726 let variable = self.global_by_name(&name)?;
1727 self.expect(TokenKind::Comma, "expected ',' after loop variable")?;
1728 let start_value = self.value()?;
1729 self.expect(TokenKind::Comma, "expected ',' after start")?;
1730 let stop = self.value()?;
1731 self.expect(TokenKind::Comma, "expected ',' after stop")?;
1732 let step = self.value()?;
1733 self.expect(TokenKind::RParen, "expected ')' after For bounds")?;
1734 self.expect(TokenKind::Semi, "expected ';' after For Global Variable")?;
1735 let (body, loop_stop) = self.actions_until_end()?;
1736 if loop_stop != Stop::End {
1737 return Err(self.malformed(
1738 "'For Global Variable' requires a matching 'End'",
1739 self.previous(),
1740 ));
1741 }
1742 self.consume_phrase("End")?;
1743 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1744 let end_span = self.previous_span();
1745 let action = Action::ForGlobalVariable {
1746 variable,
1747 start: start_value,
1748 stop,
1749 step,
1750 body,
1751 span: Some(Span::new(self.file(), start, end_span.1)),
1752 target_span: None,
1753 };
1754 Ok(self.target.actions.push(action))
1755 }
1756
1757 fn while_group(&mut self) -> Result<wir::ActionId> {
1758 let start = self.previous_span().0;
1759 self.expect(TokenKind::LParen, "expected '(' after 'While'")?;
1760 let condition = self.value()?;
1761 self.expect(TokenKind::RParen, "expected ')' after While condition")?;
1762 self.expect(TokenKind::Semi, "expected ';' after While condition")?;
1763 let (body, stop) = self.actions_until_end()?;
1764 if stop != Stop::End {
1765 return Err(self.malformed("'While' requires a matching 'End'", self.previous()));
1766 }
1767 self.consume_phrase("End")?;
1768 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1769 let end_span = self.previous_span();
1770 let action = Action::While {
1771 condition,
1772 body,
1773 span: Some(Span::new(self.file(), start, end_span.1)),
1774 };
1775 Ok(self.target.actions.push(action))
1776 }
1777
1778 fn for_player_group(&mut self) -> Result<wir::ActionId> {
1783 let start = self.previous_span().0;
1784 self.expect(
1785 TokenKind::LParen,
1786 "expected '(' after 'For Player Variable'",
1787 )?;
1788 let player = self.value()?;
1789 self.expect(TokenKind::Comma, "expected ',' after loop player")?;
1790 let (name, _, _) = self.phrase()?;
1791 let variable = self.player_by_name(&name)?;
1792 self.expect(TokenKind::Comma, "expected ',' after loop variable")?;
1793 let start_value = self.value()?;
1794 self.expect(TokenKind::Comma, "expected ',' after start")?;
1795 let stop = self.value()?;
1796 self.expect(TokenKind::Comma, "expected ',' after stop")?;
1797 let step = self.value()?;
1798 self.expect(TokenKind::RParen, "expected ')' after For bounds")?;
1799 self.expect(TokenKind::Semi, "expected ';' after For Player Variable")?;
1800 let (body, loop_stop) = self.actions_until_end()?;
1801 if loop_stop != Stop::End {
1802 return Err(self.malformed(
1803 "'For Player Variable' requires a matching 'End'",
1804 self.previous(),
1805 ));
1806 }
1807 self.consume_phrase("End")?;
1808 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1809 let end_span = self.previous_span();
1810 let action = Action::ForPlayerVariable {
1811 player,
1812 variable,
1813 start: start_value,
1814 stop,
1815 step,
1816 body,
1817 span: Some(Span::new(self.file(), start, end_span.1)),
1818 };
1819 Ok(self.target.actions.push(action))
1820 }
1821
1822 fn action_call_from_phrase(
1823 &mut self,
1824 phrase: String,
1825 start: Position,
1826 end: Position,
1827 ) -> Result<wir::ActionId> {
1828 match self
1829 .catalog
1830 .resolve(Kind::Structural, &self.locale, &phrase)
1831 {
1832 Some(entry) => match entry.id.as_str() {
1833 "setGlobalVariable" => {
1834 self.expect(
1835 TokenKind::LParen,
1836 "expected '(' after 'Set Global Variable'",
1837 )?;
1838 let (name, _, _) = self.phrase()?;
1839 let variable = self.global_by_name(&name)?;
1840 self.expect(TokenKind::Comma, "expected ',' after variable")?;
1841 let value = self.value()?;
1842 self.expect(TokenKind::RParen, "expected ')'")?;
1843 self.expect(TokenKind::Semi, "expected ';'")?;
1844 Ok(self.target.actions.push(Action::SetGlobalVariable {
1845 variable,
1846 value,
1847 span: Some(Span::new(self.file(), start, end)),
1848 target_span: None,
1849 }))
1850 }
1851 "modifyGlobalVariable" => {
1852 self.expect(TokenKind::LParen, "expected '('")?;
1853 let (name, _, _) = self.phrase()?;
1854 let variable = self.global_by_name(&name)?;
1855 self.expect(TokenKind::Comma, "expected ',' after variable")?;
1856 let op = self.modify_op()?;
1857 self.expect(TokenKind::Comma, "expected ',' after modify operator")?;
1858 let value = self.value()?;
1859 self.expect(TokenKind::RParen, "expected ')'")?;
1860 self.expect(TokenKind::Semi, "expected ';'")?;
1861 Ok(self.target.actions.push(Action::ModifyGlobalVariable {
1862 variable,
1863 op,
1864 value,
1865 span: Some(Span::new(self.file(), start, end)),
1866 target_span: None,
1867 }))
1868 }
1869 "setPlayerVariable" => {
1870 self.expect(TokenKind::LParen, "expected '('")?;
1871 let player = self.value()?;
1872 self.expect(TokenKind::Comma, "expected ',' after player")?;
1873 let (name, _, _) = self.phrase()?;
1874 let variable = self.player_by_name(&name)?;
1875 self.expect(TokenKind::Comma, "expected ',' after variable")?;
1876 let value = self.value()?;
1877 self.expect(TokenKind::RParen, "expected ')'")?;
1878 self.expect(TokenKind::Semi, "expected ';'")?;
1879 Ok(self.target.actions.push(Action::SetPlayerVariable {
1880 player,
1881 variable,
1882 value,
1883 span: Some(Span::new(self.file(), start, end)),
1884 target_span: None,
1885 }))
1886 }
1887 "modifyPlayerVariable" => {
1888 self.expect(TokenKind::LParen, "expected '('")?;
1889 let player = self.value()?;
1890 self.expect(TokenKind::Comma, "expected ',' after player")?;
1891 let (name, _, _) = self.phrase()?;
1892 let variable = self.player_by_name(&name)?;
1893 self.expect(TokenKind::Comma, "expected ',' after variable")?;
1894 let op = self.modify_op()?;
1895 self.expect(TokenKind::Comma, "expected ',' after modify operator")?;
1896 let value = self.value()?;
1897 self.expect(TokenKind::RParen, "expected ')'")?;
1898 self.expect(TokenKind::Semi, "expected ';'")?;
1899 Ok(self.target.actions.push(Action::ModifyPlayerVariable {
1900 player,
1901 variable,
1902 op,
1903 value,
1904 span: Some(Span::new(self.file(), start, end)),
1905 target_span: None,
1906 }))
1907 }
1908 "forGlobalVariable" => self.for_group(),
1909 "forPlayerVariable" => self.for_player_group(),
1910 "callSubroutine" => {
1911 self.expect(TokenKind::LParen, "expected '('")?;
1912 let (name, _, _) = self.phrase()?;
1913 let subroutine = self.subroutine_by_name(&name)?;
1914 self.expect(TokenKind::RParen, "expected ')'")?;
1915 self.expect(TokenKind::Semi, "expected ';'")?;
1916 Ok(self.target.actions.push(Action::CallSubroutine {
1917 subroutine,
1918 span: Some(Span::new(self.file(), start, end)),
1919 callee_span: None,
1920 }))
1921 }
1922 other => Err(WorkshopError::Unsupported {
1923 message: format!(
1924 "structural action '{other}' is not supported in action position"
1925 ),
1926 span: Some(Span::new(self.file(), start, end)),
1927 }),
1928 },
1929 None => {
1930 let Some(action) = self
1932 .resolve_entry(Kind::Action, &phrase)
1933 .or_else(|| self.resolve_entry(Kind::Action, &format!("{phrase} ")))
1934 .or_else(|| {
1935 let alias = match phrase.as_str() {
1936 "Set Player Allowed Heroes" => "Set Allowed Heroes",
1937 "设置技能充能" => "设置终极技能充能",
1938 _ => return None,
1939 };
1940 self.resolve_entry(Kind::Action, alias)
1941 })
1942 else {
1943 return Err(WorkshopError::Unknown {
1944 kind: "action",
1945 spelling: phrase,
1946 locale: self.locale.clone(),
1947 span: Some(Span::new(self.file(), start, end)),
1948 });
1949 };
1950 match action.id.as_str() {
1958 "chasePlayerVariableAtRate" | "chasePlayerVariableOverTime" => {
1959 self.expect(TokenKind::LParen, "expected '('")?;
1960 let player = self.value()?;
1961 self.expect(TokenKind::Comma, "expected ',' after player")?;
1962 let (name, _, _) = self.phrase()?;
1963 let variable = self.player_by_name(&name)?;
1964 let mut args = Vec::with_capacity(4);
1965 args.push(self.target.values.push(wir::ValueNode::new(
1966 wir::Value::PlayerVariable { player, variable },
1967 None,
1968 )));
1969 let mut arg_index = 2usize;
1974 loop {
1975 match self.peek() {
1976 Some(Token {
1977 kind: TokenKind::RParen,
1978 ..
1979 }) => break,
1980 Some(Token {
1981 kind: TokenKind::Comma,
1982 ..
1983 }) => {
1984 self.pos += 1;
1985 }
1986 _ => {}
1987 }
1988 let saved = self.expected_domain;
1989 self.expected_domain =
1990 self.context.expected_domain(action.id.as_str(), arg_index);
1991 let arg = self.value()?;
1992 self.expected_domain = saved;
1993 args.push(arg);
1994 arg_index += 1;
1995 }
1996 self.expect(TokenKind::RParen, "expected ')'")?;
1997 self.expect(TokenKind::Semi, "expected ';' after action")?;
1998 let canonical = if action.id == "chasePlayerVariableAtRate" {
1999 "chaseAtRate"
2000 } else {
2001 "chaseOverTime"
2002 };
2003 return Ok(self.target.actions.push(Action::Call {
2004 name: canonical.to_string(),
2005 args,
2006 span: Some(Span::new(self.file(), start, end)),
2007 }));
2008 }
2009 "startRule" => {
2010 self.expect(TokenKind::LParen, "expected '('")?;
2011 let (name, _, _) = self.phrase()?;
2012 let subroutine = self.subroutine_by_name(&name)?;
2013 self.expect(TokenKind::Comma, "expected ',' after subroutine")?;
2014 let saved = self.expected_domain;
2015 self.expected_domain = self.context.expected_domain(action.id.as_str(), 1);
2016 let behavior = self.value()?;
2017 self.expected_domain = saved;
2018 self.expect(TokenKind::RParen, "expected ')'")?;
2019 self.expect(TokenKind::Semi, "expected ';' after action")?;
2020 let subroutine_value = self
2021 .target
2022 .values
2023 .push(ValueNode::new(Value::Subroutine(subroutine), None));
2024 return Ok(self.target.actions.push(Action::Call {
2025 name: action.id.clone(),
2026 args: vec![subroutine_value, behavior],
2027 span: Some(Span::new(self.file(), start, end)),
2028 }));
2029 }
2030 "stopChasingPlayerVariable" => {
2031 self.expect(TokenKind::LParen, "expected '('")?;
2032 let player = self.value()?;
2033 self.expect(TokenKind::Comma, "expected ',' after player")?;
2034 let (name, _, _) = self.phrase()?;
2035 let variable = self.player_by_name(&name)?;
2036 self.expect(TokenKind::RParen, "expected ')'")?;
2037 self.expect(TokenKind::Semi, "expected ';' after action")?;
2038 let player_variable = self.target.values.push(ValueNode::new(
2039 Value::PlayerVariable { player, variable },
2040 None,
2041 ));
2042 return Ok(self.target.actions.push(Action::Call {
2043 name: action.id.clone(),
2044 args: vec![player_variable],
2045 span: Some(Span::new(self.file(), start, end)),
2046 }));
2047 }
2048 _ => {}
2049 }
2050 let args = if let Some(Token {
2051 kind: TokenKind::LParen,
2052 ..
2053 }) = self.peek()
2054 {
2055 self.pos += 1;
2056 let args = self.value_args(action.id.as_str())?;
2057 self.expect(TokenKind::RParen, "expected ')'")?;
2058 args
2059 } else {
2060 Vec::new()
2061 };
2062 self.expect(TokenKind::Semi, "expected ';' after action")?;
2063 Ok(self.target.actions.push(Action::Call {
2064 name: action.id.clone(),
2065 args,
2066 span: Some(Span::new(self.file(), start, end)),
2067 }))
2068 }
2069 }
2070 }
2071
2072 fn modify_op(&mut self) -> Result<ModifyOp> {
2073 let (phrase, start, end) = self.phrase()?;
2074 if phrase == "根据值从数组中移除" {
2075 return Ok(ModifyOp::RemoveFromArray);
2076 }
2077 if phrase == "根据索引从数组中移除" {
2078 return Ok(ModifyOp::RemoveFromArrayByIndex);
2079 }
2080 let entry = self
2081 .catalog
2082 .resolve(Kind::Operator, &self.locale, &phrase)
2083 .ok_or_else(|| WorkshopError::Unknown {
2084 kind: "modify operator",
2085 spelling: phrase.clone(),
2086 locale: self.locale.clone(),
2087 span: Some(Span::new(self.file(), start, end)),
2088 })?;
2089 let op = match entry.id.as_str() {
2090 "add" => ModifyOp::Add,
2091 "subtract" => ModifyOp::Subtract,
2092 "multiply" => ModifyOp::Multiply,
2093 "divide" => ModifyOp::Divide,
2094 "modulo" => ModifyOp::Modulo,
2095 "min" => ModifyOp::Min,
2096 "max" => ModifyOp::Max,
2097 "raiseToPower" => ModifyOp::RaiseToPower,
2098 "appendToArray" => ModifyOp::AppendToArray,
2099 "removeFromArray" | "removeFromArrayByValue" => ModifyOp::RemoveFromArray,
2100 "removeFromArrayByIndex" => ModifyOp::RemoveFromArrayByIndex,
2101 other => {
2102 return Err(WorkshopError::Unsupported {
2103 message: format!("unsupported modify operator '{other}'"),
2104 span: Some(Span::new(self.file(), start, end)),
2105 });
2106 }
2107 };
2108 Ok(op)
2109 }
2110
2111 fn value(&mut self) -> Result<wir::ValueId> {
2112 let mut value = self.primary()?;
2113 loop {
2114 if let Some(Token {
2115 kind: TokenKind::LBracket,
2116 start,
2117 ..
2118 }) = self.peek()
2119 {
2120 self.pos += 1;
2121 let index = self.value()?;
2122 let end = self.peek().map(|token| token.end).unwrap_or(start);
2123 self.expect(TokenKind::RBracket, "expected ']' after array index")?;
2124 value = self.target.values.push(ValueNode::new(
2125 Value::Call {
2126 name: "valueInArray".to_string(),
2127 args: vec![value, index],
2128 },
2129 Some(Span::new(self.file(), start, end)),
2130 ));
2131 continue;
2132 }
2133 if matches!(
2134 self.peek(),
2135 Some(Token {
2136 kind: TokenKind::Dot,
2137 ..
2138 })
2139 ) {
2140 self.pos += 1;
2141 let (name, _, _) = self.phrase()?;
2142 let member = self
2143 .target
2144 .values
2145 .push(ValueNode::new(Value::String(name), None));
2146 let mut args = vec![value, member];
2147 if matches!(
2148 self.peek(),
2149 Some(Token {
2150 kind: TokenKind::LBracket,
2151 ..
2152 })
2153 ) {
2154 self.pos += 1;
2155 args.push(self.value()?);
2156 self.expect(TokenKind::RBracket, "expected ']' after member index")?;
2157 }
2158 value = self.target.values.push(ValueNode::new(
2159 Value::Call {
2160 name: "memberAccess".to_string(),
2161 args,
2162 },
2163 None,
2164 ));
2165 continue;
2166 }
2167 if let Some(Token {
2168 kind: TokenKind::Op(op),
2169 start,
2170 end,
2171 }) = self.peek()
2172 {
2173 if op == "?" {
2174 self.pos += 1;
2175 let when_true = self.value()?;
2176 self.expect(TokenKind::Colon, "expected ':' in conditional value")?;
2177 let when_false = self.value()?;
2178 value = self.target.values.push(ValueNode::new(
2179 Value::Call {
2180 name: "ifThenElse".to_string(),
2181 args: vec![value, when_true, when_false],
2182 },
2183 Some(Span::new(self.file(), start, end)),
2184 ));
2185 continue;
2186 }
2187 let compound_assignment = matches!(
2188 self.peek_at(1).map(|token| token.kind),
2189 Some(TokenKind::Op(equal)) if equal == "="
2190 );
2191 if !compound_assignment
2192 && (is_comparison(&op)
2193 || matches!(op.as_str(), "and" | "or" | "+" | "-" | "*" | "/" | "%"))
2194 {
2195 self.pos += 1;
2196 let right = self.primary()?;
2197 let name = match op.as_str() {
2198 "+" => "add",
2199 "-" => "subtract",
2200 "*" => "multiply",
2201 "/" => "divide",
2202 "%" => "modulo",
2203 _ => op.as_str(),
2204 };
2205 value = self.target.values.push(ValueNode::new(
2206 Value::Call {
2207 name: name.to_string(),
2208 args: vec![value, right],
2209 },
2210 Some(Span::new(self.file(), start, end)),
2211 ));
2212 continue;
2213 }
2214 }
2215 break;
2216 }
2217 Ok(value)
2218 }
2219
2220 fn primary(&mut self) -> Result<wir::ValueId> {
2221 match self.peek() {
2222 Some(Token {
2223 kind: TokenKind::Op(op),
2224 start,
2225 ..
2226 }) if op == "not" => {
2227 self.pos += 1;
2228 let value = self.primary()?;
2229 Ok(self.target.values.push(ValueNode::new(
2230 Value::Call {
2231 name: "not".to_string(),
2232 args: vec![value],
2233 },
2234 Some(Span::new(self.file(), start, self.previous_span().1)),
2235 )))
2236 }
2237 Some(Token {
2238 kind: TokenKind::Number { value, text },
2239 start,
2240 end,
2241 }) => {
2242 let span = Some(Span::new(self.file(), start, end));
2243 self.pos += 1;
2244 Ok(self
2245 .target
2246 .values
2247 .push(ValueNode::new(Value::Number { value, text }, span)))
2248 }
2249 Some(Token {
2250 kind: TokenKind::Op(op),
2251 start,
2252 ..
2253 }) if op == "-" => {
2254 if let Some(Token {
2255 kind: TokenKind::Number { value, text },
2256 end: number_end,
2257 ..
2258 }) = self.peek_at(1)
2259 {
2260 let span = Some(Span::new(self.file(), start, number_end));
2261 self.pos += 2;
2262 Ok(self.target.values.push(ValueNode::new(
2263 Value::Number {
2264 value: -value,
2265 text: format!("-{text}"),
2266 },
2267 span,
2268 )))
2269 } else {
2270 Err(self.malformed("expected a number after '-'", &self.peek().unwrap()))
2271 }
2272 }
2273 Some(Token {
2274 kind: TokenKind::String(content),
2275 start,
2276 end,
2277 }) => {
2278 let span = Some(Span::new(self.file(), start, end));
2279 self.pos += 1;
2280 Ok(self
2281 .target
2282 .values
2283 .push(ValueNode::new(Value::String(content), span)))
2284 }
2285 Some(Token {
2286 kind: TokenKind::Word(word),
2287 ..
2288 }) if matches!(canonical_keyword(&word), "Global" | "global") => {
2289 let (start, end) = self.span_here();
2290 self.pos += 1;
2291 if let Some(Token {
2295 kind: TokenKind::Word(next),
2296 ..
2297 }) = self.peek()
2298 {
2299 if canonical_keyword(&next) == "Variable"
2300 || canonical_keyword(&next) == "variable"
2301 {
2302 self.pos += 1;
2303 self.expect(TokenKind::LParen, "expected '(' after 'Global Variable'")?;
2304 let (name, _, _) = self.phrase()?;
2305 let variable = self.global_by_name(&name)?;
2306 self.expect(TokenKind::RParen, "expected ')' after Global Variable")?;
2307 let span = Some(Span::new(self.file(), start, end));
2308 return Ok(self
2309 .target
2310 .values
2311 .push(ValueNode::new(Value::GlobalVariable(variable), span)));
2312 }
2313 }
2314 self.expect(TokenKind::Dot, "expected '.' after 'Global'")?;
2315 let (name, _, _) = self.phrase()?;
2316 let variable = self.global_by_name(&name)?;
2317 let span = Some(Span::new(self.file(), start, end));
2318 Ok(self
2319 .target
2320 .values
2321 .push(ValueNode::new(Value::GlobalVariable(variable), span)))
2322 }
2323 Some(Token {
2324 kind: TokenKind::Word(word),
2325 ..
2326 }) if matches!(canonical_keyword(&word), "Player" | "player")
2327 && matches!(
2328 self.peek_at(1),
2329 Some(Token {
2330 kind: TokenKind::Word(next),
2331 ..
2332 }) if matches!(canonical_keyword(&next), "Variable" | "variable")
2333 ) =>
2334 {
2335 let (start, end) = self.span_here();
2339 self.pos += 1;
2340 let saved = self.expected_domain;
2341 self.expected_domain = None;
2342 let result = (|| {
2343 self.consume_phrase("Variable")?;
2344 self.expect(TokenKind::LParen, "expected '(' after 'Player Variable'")?;
2345 let player = self.value()?;
2346 self.expect(TokenKind::Comma, "expected ',' after player")?;
2347 let (name, _, _) = self.phrase()?;
2348 let variable = self.player_by_name(&name)?;
2349 self.expect(TokenKind::RParen, "expected ')' after Player Variable")?;
2350 Ok(self.target.values.push(ValueNode::new(
2351 Value::PlayerVariable { player, variable },
2352 Some(Span::new(self.file(), start, end)),
2353 )))
2354 })();
2355 self.expected_domain = saved;
2356 result
2357 }
2358 Some(Token {
2359 kind: TokenKind::Word(word),
2360 ..
2361 }) if matches!(canonical_keyword(&word), "Event" | "event" | "Event Player") => {
2362 let (start, _) = self.span_here();
2363 self.pos += 1;
2364 if canonical_keyword(&word) == "Event Player" {
2365 let player = self.target.values.push(ValueNode::new(
2366 Value::EventPlayer,
2367 Some(Span::new(self.file(), start, self.previous_span().1)),
2368 ));
2369 if matches!(
2370 self.peek(),
2371 Some(Token {
2372 kind: TokenKind::Dot,
2373 ..
2374 })
2375 ) {
2376 self.pos += 1;
2377 let (name, name_start, name_end) = self.phrase()?;
2378 let variable = self.player_by_name(&name)?;
2379 return Ok(self.target.values.push(ValueNode::new(
2380 Value::PlayerVariable { player, variable },
2381 Some(Span::new(self.file(), name_start, name_end)),
2382 )));
2383 }
2384 return Ok(player);
2385 }
2386 if canonical_keyword(&word) != "Event Player"
2387 && matches!(self.peek(), Some(Token { kind: TokenKind::Word(next), .. }) if matches!(canonical_keyword(&next), "Player" | "player"))
2388 {
2389 self.pos += 1;
2390 let player = self.target.values.push(ValueNode::new(
2391 Value::EventPlayer,
2392 Some(Span::new(self.file(), start, self.previous_span().1)),
2393 ));
2394 if matches!(
2395 self.peek(),
2396 Some(Token {
2397 kind: TokenKind::Dot,
2398 ..
2399 })
2400 ) {
2401 self.pos += 1;
2402 let (name, name_start, name_end) = self.phrase()?;
2403 let variable = self.player_by_name(&name)?;
2404 return Ok(self.target.values.push(ValueNode::new(
2405 Value::PlayerVariable { player, variable },
2406 Some(Span::new(self.file(), name_start, name_end)),
2407 )));
2408 }
2409 return Ok(player);
2410 }
2411 self.pos -= 1;
2412 let (phrase, start, end) = self.phrase()?;
2413 if matches!(
2414 self.peek(),
2415 Some(Token {
2416 kind: TokenKind::LParen,
2417 ..
2418 })
2419 ) {
2420 self.call_or_enum(&phrase, start, end)
2421 } else {
2422 self.bare_member(&phrase, start, end)
2423 }
2424 }
2425 Some(Token {
2426 kind: TokenKind::LParen,
2427 ..
2428 }) => {
2429 self.pos += 1;
2432 let inner = self.value()?;
2433 if matches!(
2434 self.peek(),
2435 Some(Token {
2436 kind: TokenKind::Dot,
2437 ..
2438 })
2439 ) {
2440 self.pos += 1;
2441 let (name, _, _) = self.phrase()?;
2442 let member = self
2443 .target
2444 .values
2445 .push(ValueNode::new(Value::String(name), None));
2446 let mut args = vec![inner, member];
2447 if matches!(
2448 self.peek(),
2449 Some(Token {
2450 kind: TokenKind::LBracket,
2451 ..
2452 })
2453 ) {
2454 self.pos += 1;
2455 args.push(self.value()?);
2456 self.expect(TokenKind::RBracket, "expected ']' after member index")?;
2457 }
2458 let mut accessed = self.target.values.push(ValueNode::new(
2459 Value::Call {
2460 name: "memberAccess".to_string(),
2461 args,
2462 },
2463 None,
2464 ));
2465 if matches!(self.peek(), Some(Token { kind: TokenKind::Op(op), .. }) if op == "?")
2466 {
2467 self.pos += 1;
2468 let when_true = self.value()?;
2469 self.expect(TokenKind::Colon, "expected ':' in conditional value")?;
2470 let when_false = self.value()?;
2471 accessed = self.target.values.push(ValueNode::new(
2472 Value::Call {
2473 name: "ifThenElse".to_string(),
2474 args: vec![accessed, when_true, when_false],
2475 },
2476 None,
2477 ));
2478 }
2479 self.expect(TokenKind::RParen, "expected ')' after member access")?;
2480 return Ok(accessed);
2481 }
2482 self.expect(TokenKind::RParen, "expected ')' after parenthesized value")?;
2483 if let Some(Token {
2484 kind: TokenKind::Dot,
2485 ..
2486 }) = self.peek()
2487 {
2488 self.pos += 1;
2489 let (name, _, _) = self.phrase()?;
2490 if matches!(
2491 self.target.values.get(inner),
2492 Some(ValueNode {
2493 value: Value::EventPlayer,
2494 ..
2495 })
2496 ) || self.players.contains_key(&name)
2497 {
2498 let variable = self
2499 .players
2500 .get(&name)
2501 .copied()
2502 .unwrap_or(self.player_by_name(&name)?);
2503 Ok(self.target.values.push(ValueNode::new(
2504 Value::PlayerVariable {
2505 player: inner,
2506 variable,
2507 },
2508 None,
2509 )))
2510 } else {
2511 let member = self
2512 .target
2513 .values
2514 .push(ValueNode::new(Value::String(name), None));
2515 let mut args = vec![inner, member];
2516 if matches!(
2517 self.peek(),
2518 Some(Token {
2519 kind: TokenKind::LBracket,
2520 ..
2521 })
2522 ) {
2523 self.pos += 1;
2524 args.push(self.value()?);
2525 self.expect(TokenKind::RBracket, "expected ']' after member index")?;
2526 }
2527 Ok(self.target.values.push(ValueNode::new(
2528 Value::Call {
2529 name: "memberAccess".to_string(),
2530 args,
2531 },
2532 None,
2533 )))
2534 }
2535 } else {
2536 Ok(inner)
2537 }
2538 }
2539 _ => {
2540 if let Some((phrase, start, end)) = self.catalog_phrase_with_dot() {
2541 return self.bare_member_resolved(&phrase, start, end);
2542 }
2543 let (phrase, start, end) = self.phrase()?;
2544 match canonical_keyword(&phrase) {
2545 "True" | "真" => Ok(self.push_bool(true, start, end)),
2546 "False" | "假" => Ok(self.push_bool(false, start, end)),
2547 "Event Player" => Ok(self.target.values.push(ValueNode::new(
2548 Value::EventPlayer,
2549 Some(Span::new(self.file(), start, end)),
2550 ))),
2551 "Null" => Ok(self.target.values.push(ValueNode::new(
2552 Value::Null,
2553 Some(Span::new(self.file(), start, end)),
2554 ))),
2555 _ => {
2556 if let Some(Token {
2557 kind: TokenKind::LParen,
2558 ..
2559 }) = self.peek()
2560 {
2561 self.call_or_enum(&phrase, start, end)
2562 } else {
2563 self.bare_member(&phrase, start, end)
2564 }
2565 }
2566 }
2567 }
2568 }
2569 }
2570
2571 fn catalog_phrase_with_dot(&mut self) -> Option<(String, Position, Position)> {
2576 let first = self.peek()?;
2577 if !matches!(first.kind, TokenKind::Word(_)) {
2578 return None;
2579 }
2580 let start = first.start;
2581 let mut end = first.end;
2582 let mut parts = Vec::new();
2583 let mut has_dot = false;
2584 let mut index = self.pos;
2585 while let Some(token) = self.tokens.get(index) {
2586 match &token.kind {
2587 TokenKind::Word(word) => parts.push(word.clone()),
2588 TokenKind::Number { text, .. } => parts.push(text.clone()),
2589 TokenKind::Dot => {
2590 has_dot = true;
2591 parts.push(".".to_string());
2592 }
2593 _ => break,
2594 }
2595 end = token.end;
2596 index += 1;
2597 }
2598 if !has_dot {
2599 return None;
2600 }
2601 let phrase = parts.join(" ").replace(" .", ".").replace(". ", ".");
2602 let known_value = self.resolve_entry(Kind::Value, &phrase).is_some();
2603 let known_member = !self
2604 .catalog
2605 .bare_member_matches(&self.locale, &phrase)
2606 .is_empty();
2607 if !known_value && !known_member {
2608 return None;
2609 }
2610 self.pos = index;
2611 Some((phrase, start, end))
2612 }
2613
2614 fn call_or_enum(
2615 &mut self,
2616 phrase: &str,
2617 start: Position,
2618 end: Position,
2619 ) -> Result<wir::ValueId> {
2620 let prefer_enum = self.catalog.enum_domain("Hero").is_some()
2624 && (canonical_keyword(phrase) == "Hero"
2625 || self.resolve_enum_domain_mixed(phrase) == Some("Hero"));
2626 if !prefer_enum {
2627 if let Some(entry) = self.resolve_entry(Kind::Value, phrase) {
2628 self.expect(TokenKind::LParen, "expected '(' after value name")?;
2629 if entry.id == "compare" {
2630 let saved = self.expected_domain;
2634 self.expected_domain = None;
2635 let left = self.value();
2636 self.expected_domain = saved;
2637 let left = left?;
2638 self.expect(TokenKind::Comma, "expected ',' after Compare operand")?;
2639 let (op, op_start, op_end) = match self.next() {
2640 Some(Token {
2641 kind: TokenKind::Op(op),
2642 start,
2643 end,
2644 }) => (op, start, end),
2645 Some(token) => {
2646 return Err(
2647 self.malformed("expected a comparison operator in Compare", &token)
2648 );
2649 }
2650 None => {
2651 return Err(self.malformed(
2652 "expected a comparison operator in Compare",
2653 self.eof(),
2654 ));
2655 }
2656 };
2657 self.expect(TokenKind::Comma, "expected ',' after Compare operator")?;
2658 let saved = self.expected_domain;
2659 self.expected_domain = None;
2660 let right = self.value();
2661 self.expected_domain = saved;
2662 let right = right?;
2663 self.expect(TokenKind::RParen, "expected ')'")?;
2664 let span = Some(Span::new(self.file(), op_start, op_end));
2665 return Ok(self.target.values.push(ValueNode::new(
2666 Value::Call {
2667 name: op,
2668 args: vec![left, right],
2669 },
2670 span,
2671 )));
2672 }
2673 let args = self.value_args(entry.id.as_str())?;
2674 self.expect(TokenKind::RParen, "expected ')'")?;
2675 return Ok(self.target.values.push(ValueNode::new(
2676 Value::Call {
2677 name: entry.id.clone(),
2678 args,
2679 },
2680 Some(Span::new(self.file(), start, end)),
2681 )));
2682 }
2683 }
2684 if let Some(domain_name) = self
2685 .resolve_enum_domain_mixed(phrase)
2686 .or_else(|| self.resolve_enum_domain_mixed(canonical_keyword(phrase)))
2687 {
2688 let domain = self
2689 .catalog
2690 .enum_domain(domain_name)
2691 .expect("resolved enum domain must exist");
2692 self.expect(TokenKind::LParen, "expected '('")?;
2694 let (member_phrase, _, _) = self.enum_member_phrase()?;
2695 let member = self
2696 .resolve_enum_member_mixed(&domain.domain, &member_phrase)
2697 .unwrap_or_else(|| (domain.domain.clone(), member_phrase.clone()));
2698 self.expect(TokenKind::RParen, "expected ')' after enum member")?;
2699 return Ok(self.target.values.push(ValueNode::new(
2700 Value::Enum {
2701 value_type: member.0,
2702 value: member.1,
2703 },
2704 Some(Span::new(self.file(), start, end)),
2705 )));
2706 }
2707 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LParen)) {
2708 self.pos += 1;
2709 self.call_stack.push(phrase.to_string());
2710 let args = self.opaque_value_args()?;
2711 self.call_stack.pop();
2712 self.expect(TokenKind::RParen, "expected ')' after value call")?;
2713 return Ok(self.target.values.push(ValueNode::new(
2714 Value::Call {
2715 name: phrase.to_string(),
2716 args,
2717 },
2718 Some(Span::new(self.file(), start, end)),
2719 )));
2720 }
2721 Ok(self.target.values.push(ValueNode::new(
2722 Value::Call {
2723 name: phrase.to_string(),
2724 args: Vec::new(),
2725 },
2726 Some(Span::new(self.file(), start, end)),
2727 )))
2728 }
2729
2730 fn bare_member(
2731 &mut self,
2732 phrase: &str,
2733 start: Position,
2734 end: Position,
2735 ) -> Result<wir::ValueId> {
2736 if !matches!(phrase, "None" | "无" | "True" | "真" | "False" | "假")
2741 && matches!(self.peek().map(|token| token.kind), Some(TokenKind::Colon))
2742 {
2743 let saved = self.pos;
2744 self.pos += 1;
2745 let (suffix, _, suffix_end) = self.phrase()?;
2746 let owned_phrase = format!("{phrase}: {suffix}");
2747 let recognized = self
2748 .expected_domain
2749 .and_then(|domain| self.resolve_enum_member_mixed(domain, &owned_phrase))
2750 .or_else(|| self.resolve_enum_member_mixed("Hero", &owned_phrase));
2751 if recognized.is_some() {
2752 return self.bare_member_resolved(&owned_phrase, start, suffix_end);
2753 }
2754 self.pos = saved;
2755 }
2756 self.bare_member_resolved(phrase, start, end)
2757 }
2758
2759 fn bare_member_resolved(
2760 &mut self,
2761 phrase: &str,
2762 start: Position,
2763 end: Position,
2764 ) -> Result<wir::ValueId> {
2765 match (matches!(phrase, "None" | "无"), self.expected_domain) {
2766 (true, Some(expected))
2767 if matches!(
2768 expected,
2769 "ChaseTimeReeval"
2770 | "ChaseRateReeval"
2771 | "Invis"
2772 | "ThrottleReeval"
2773 | "EffectReeval"
2774 ) =>
2775 {
2776 return Ok(self.target.values.push(ValueNode::new(
2777 Value::Enum {
2778 value_type: expected.to_string(),
2779 value: "NONE".to_string(),
2780 },
2781 Some(Span::new(self.file(), start, end)),
2782 )));
2783 }
2784 _ => {}
2785 }
2786 if let Some(variable) = self.globals.get(phrase).copied() {
2787 return Ok(self.target.values.push(ValueNode::new(
2788 Value::GlobalVariable(variable),
2789 Some(Span::new(self.file(), start, end)),
2790 )));
2791 }
2792 if let Some(expected) = self.expected_domain {
2793 if let Some((value_type, value)) = self.resolve_enum_member_mixed(expected, phrase) {
2794 return Ok(self.target.values.push(ValueNode::new(
2795 Value::Enum { value_type, value },
2796 Some(Span::new(self.file(), start, end)),
2797 )));
2798 }
2799 }
2800 if let Some((value_type, value)) = self.resolve_enum_member_mixed("Team", phrase) {
2801 return Ok(self.target.values.push(ValueNode::new(
2802 Value::Enum { value_type, value },
2803 Some(Span::new(self.file(), start, end)),
2804 )));
2805 }
2806 if phrase == "Visible To and String" {
2807 return Ok(self.target.values.push(ValueNode::new(
2808 Value::Enum {
2809 value_type: "HudReeval".to_string(),
2810 value: "VISIBILITY_AND_STRING".to_string(),
2811 },
2812 Some(Span::new(self.file(), start, end)),
2813 )));
2814 }
2815 if self
2816 .call_stack
2817 .last()
2818 .is_some_and(|call| call == "createHudText")
2819 {
2820 if let Some((value_type, value)) = self.resolve_enum_member_mixed("HudPosition", phrase)
2821 {
2822 return Ok(self.target.values.push(ValueNode::new(
2823 Value::Enum { value_type, value },
2824 Some(Span::new(self.file(), start, end)),
2825 )));
2826 }
2827 }
2828 if (self.expected_domain.is_none()
2829 && matches!(
2830 phrase,
2831 "Up" | "上" | "Down" | "下" | "Left" | "左" | "Right" | "右"
2832 )
2833 && (self
2834 .call_stack
2835 .last()
2836 .is_some_and(|call| matches!(call.as_str(), "multiply" | "add"))
2837 || self.call_stack.is_empty()
2838 || self
2839 .call_stack
2840 .iter()
2841 .any(|call| call == "startAcceleration")
2842 || self.call_stack.iter().any(|call| {
2843 call == "raycastHitPosition"
2844 || call == "Direction Towards"
2845 || call == "directionTowards"
2846 })))
2847 || (matches!(self.expected_domain, Some("Position")) && matches!(phrase, "Up" | "上"))
2848 {
2849 let value = match phrase {
2850 "Left" | "左" => "LEFT",
2851 "Right" | "右" => "RIGHT",
2852 "Down" | "下" => "DOWN",
2853 _ => "UP",
2854 };
2855 return Ok(self.target.values.push(ValueNode::new(
2856 Value::Enum {
2857 value_type: "Vector".to_string(),
2858 value: value.to_string(),
2859 },
2860 Some(Span::new(self.file(), start, end)),
2861 )));
2862 }
2863 let matches: Vec<(String, String)> = self
2868 .catalog
2869 .bare_member_matches(&self.locale, phrase)
2870 .into_iter()
2871 .filter(|(domain, _)| domain != "EventTeam" && domain != "EventPlayer")
2872 .collect();
2873 if matches.len() == 1 {
2874 return Ok(self.target.values.push(ValueNode::new(
2875 Value::Enum {
2876 value_type: matches[0].0.clone(),
2877 value: matches[0].1.clone(),
2878 },
2879 Some(Span::new(self.file(), start, end)),
2880 )));
2881 }
2882 if matches.len() > 1 {
2883 if let Some(expected) = self.expected_domain {
2888 let pinned: Vec<&(String, String)> = matches
2889 .iter()
2890 .filter(|(domain, _)| domain == expected)
2891 .collect();
2892 if pinned.len() == 1 {
2893 return Ok(self.target.values.push(ValueNode::new(
2894 Value::Enum {
2895 value_type: pinned[0].0.clone(),
2896 value: pinned[0].1.clone(),
2897 },
2898 Some(Span::new(self.file(), start, end)),
2899 )));
2900 }
2901 }
2902 return Err(WorkshopError::Unsupported {
2903 message: format!("ambiguous enum member '{phrase}' (multiple domains match)"),
2904 span: Some(Span::new(self.file(), start, end)),
2905 });
2906 }
2907 if let Some(entry) = self.resolve_entry(Kind::Value, phrase) {
2909 if entry.id == "null" {
2910 return Ok(self.target.values.push(ValueNode::new(
2911 Value::Null,
2912 Some(Span::new(self.file(), start, end)),
2913 )));
2914 }
2915 return Ok(self.target.values.push(ValueNode::new(
2916 Value::Call {
2917 name: entry.id.clone(),
2918 args: Vec::new(),
2919 },
2920 Some(Span::new(self.file(), start, end)),
2921 )));
2922 }
2923 Ok(self.target.values.push(ValueNode::new(
2924 Value::Call {
2925 name: phrase.to_string(),
2926 args: Vec::new(),
2927 },
2928 Some(Span::new(self.file(), start, end)),
2929 )))
2930 }
2931
2932 fn value_args(&mut self, call_id: &str) -> Result<Vec<wir::ValueId>> {
2933 let mut args = Vec::new();
2934 if let Some(Token {
2935 kind: TokenKind::RParen,
2936 ..
2937 }) = self.peek()
2938 {
2939 return Ok(args);
2940 }
2941 self.call_stack.push(call_id.to_string());
2942 let mut arg_index = 0usize;
2943 let mut raw_modify_operator = false;
2944 loop {
2945 if call_id == "string" && arg_index == 0 {
2949 args.push(self.localized_string_argument()?);
2950 } else if matches!(
2951 call_id,
2952 "setGlobalVariableAtIndex" | "modifyGlobalVariableAtIndex"
2953 ) && arg_index == 0
2954 {
2955 let (name, start, end) = self.phrase()?;
2956 let variable = self.global_by_name(&name)?;
2957 args.push(self.target.values.push(ValueNode::new(
2958 Value::GlobalVariable(variable),
2959 Some(Span::new(self.file(), start, end)),
2960 )));
2961 } else if matches!(
2962 call_id,
2963 "setPlayerVariableAtIndex" | "modifyPlayerVariableAtIndex"
2964 ) && arg_index == 1
2965 {
2966 let saved = self.pos;
2967 let (name, start, end) = self.phrase()?;
2968 if let Some(variable) = self.players.get(&name).copied() {
2969 let player = args[0];
2970 args[0] = self.target.values.push(ValueNode::new(
2971 Value::PlayerVariable { player, variable },
2972 Some(Span::new(self.file(), start, end)),
2973 ));
2974 } else {
2975 self.pos = saved;
2976 let saved_domain = self.expected_domain;
2977 self.expected_domain = self.context.expected_domain(call_id, arg_index);
2978 let arg = self.value();
2979 self.expected_domain = saved_domain;
2980 args.push(arg?);
2981 }
2982 } else if matches!(
2983 call_id,
2984 "modifyGlobalVariableAtIndex" | "modifyPlayerVariableAtIndex"
2985 ) && {
2986 let operator_position = arg_index == 2 || arg_index == 3 && raw_modify_operator;
2987 let saved = self.pos;
2988 let is_operator = operator_position && self.modify_op().is_ok();
2989 self.pos = saved;
2990 is_operator
2991 } {
2992 let operator = self.modify_op()?;
2993 let name = operator.catalog_id();
2994 args.push(self.target.values.push(ValueNode::new(
2995 Value::Call {
2996 name: name.to_string(),
2997 args: Vec::new(),
2998 },
2999 None,
3000 )));
3001 } else {
3002 if matches!(
3003 call_id,
3004 "modifyGlobalVariableAtIndex" | "modifyPlayerVariableAtIndex"
3005 ) && arg_index == 2
3006 {
3007 let saved = self.pos;
3008 raw_modify_operator = self.modify_op().is_err();
3009 self.pos = saved;
3010 }
3011 let saved = self.expected_domain;
3015 self.expected_domain =
3016 self.context
3017 .expected_domain(call_id, arg_index)
3018 .or_else(|| {
3019 (matches!(call_id, "array" | "randomValueInArray"))
3020 .then_some(saved)
3021 .flatten()
3022 });
3023 let arg = self.value();
3024 self.expected_domain = saved;
3025 args.push(arg?);
3026 }
3027 arg_index += 1;
3028 match self.peek() {
3029 Some(Token {
3030 kind: TokenKind::Comma,
3031 ..
3032 }) => {
3033 self.pos += 1;
3034 }
3035 Some(Token {
3036 kind: TokenKind::Colon,
3037 ..
3038 }) => {
3039 self.pos += 1;
3040 if !matches!(
3041 self.peek(),
3042 Some(Token {
3043 kind: TokenKind::RParen,
3044 ..
3045 })
3046 ) {
3047 let _ = self.phrase()?;
3048 }
3049 match self.peek() {
3050 Some(Token {
3051 kind: TokenKind::Comma,
3052 ..
3053 }) => self.pos += 1,
3054 Some(Token {
3055 kind: TokenKind::RParen,
3056 ..
3057 }) => break,
3058 Some(token) => return Err(self.malformed("expected ',' or ')'", &token)),
3059 None => {
3060 return Err(self.malformed("unexpected end of value call", self.eof()));
3061 }
3062 }
3063 }
3064 _ => break,
3065 }
3066 }
3067 self.call_stack.pop();
3068 Ok(args)
3069 }
3070
3071 fn localized_string_argument(&mut self) -> Result<wir::ValueId> {
3072 let Some(token) = self.peek() else {
3073 return Err(self.malformed("expected a localized string", self.eof()));
3074 };
3075 if matches!(token.kind, TokenKind::String(_)) {
3076 let span = Some(Span::new(self.file(), token.start, token.end));
3077 let content = self.expect_string("expected a localized string")?;
3078 return self.localized_string_literal(content, span);
3079 }
3080
3081 let saved = self.pos;
3086 let (phrase, _, _) = self.phrase()?;
3087 let expression_like = self.resolve_entry(Kind::Value, &phrase).is_some()
3088 || matches!(
3089 self.canonical_keyword(&phrase).as_str(),
3090 "True" | "False" | "真" | "假"
3091 )
3092 || self
3093 .peek()
3094 .is_some_and(|token| matches!(&token.kind, TokenKind::LParen | TokenKind::Dot));
3095 self.pos = saved;
3096 if expression_like || !matches!(token.kind, TokenKind::Word(_)) {
3097 return self.value();
3098 }
3099
3100 let (text, start, end) = self.phrase()?;
3101 self.localized_string_literal(text, Some(Span::new(self.file(), start, end)))
3102 }
3103
3104 fn localized_string_literal(
3105 &mut self,
3106 text: String,
3107 span: Option<Span>,
3108 ) -> Result<wir::ValueId> {
3109 let Some(entry) = self.catalog.resolve_localized_string(&self.locale, &text) else {
3110 return Err(WorkshopError::Unknown {
3111 kind: "localized string",
3112 spelling: text,
3113 locale: self.locale.clone(),
3114 span,
3115 });
3116 };
3117 Ok(self.target.values.push(ValueNode::new(
3118 Value::LocalizedString(entry.id.clone()),
3119 span,
3120 )))
3121 }
3122
3123 fn opaque_value_args(&mut self) -> Result<Vec<wir::ValueId>> {
3124 let mut args = Vec::new();
3125 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::RParen)) {
3126 return Ok(args);
3127 }
3128 loop {
3129 args.push(self.value()?);
3130 match self.peek() {
3131 Some(Token {
3132 kind: TokenKind::Colon,
3133 ..
3134 }) => {
3135 self.pos += 1;
3136 if !matches!(
3137 self.peek(),
3138 Some(Token {
3139 kind: TokenKind::RParen,
3140 ..
3141 })
3142 ) {
3143 let _ = self.phrase()?;
3144 }
3145 match self.peek() {
3146 Some(Token {
3147 kind: TokenKind::Comma,
3148 ..
3149 }) => self.pos += 1,
3150 Some(Token {
3151 kind: TokenKind::RParen,
3152 ..
3153 }) => break,
3154 Some(token) => return Err(self.malformed("expected ',' or ')'", &token)),
3155 None => {
3156 return Err(self.malformed("unexpected end of value call", self.eof()));
3157 }
3158 }
3159 }
3160 Some(Token {
3161 kind: TokenKind::Comma,
3162 ..
3163 }) => self.pos += 1,
3164 Some(Token {
3165 kind: TokenKind::RParen,
3166 ..
3167 }) => break,
3168 Some(token) => return Err(self.malformed("expected ',' or ')'", &token)),
3169 None => return Err(self.malformed("unexpected end of value call", self.eof())),
3170 }
3171 }
3172 Ok(args)
3173 }
3174
3175 fn line_has_assignment(&self) -> bool {
3176 let tokens: Vec<_> = self.tokens[self.pos..]
3177 .iter()
3178 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
3179 .collect();
3180 tokens.iter().any(|token| {
3181 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
3182 }) || tokens.windows(2).any(|window| {
3183 matches!(&window[0].kind, TokenKind::Word(_))
3184 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
3185 })
3186 }
3187
3188 fn push_bool(&mut self, value: bool, start: Position, end: Position) -> wir::ValueId {
3189 self.target.values.push(ValueNode::new(
3190 Value::Bool(value),
3191 Some(Span::new(self.file(), start, end)),
3192 ))
3193 }
3194
3195 fn global_by_name(&mut self, name: &str) -> Result<wir::GlobalVarId> {
3196 if let Some(id) = self.globals.get(name).copied() {
3197 return Ok(id);
3198 }
3199 let index = self.next_variable_index(false);
3200 let id = self.target.global_variables.push(wir::WorkshopVariable {
3201 name: name.to_string(),
3202 index,
3203 span: None,
3204 name_span: None,
3205 });
3206 self.globals.insert(name.to_string(), id);
3207 Ok(id)
3208 }
3209
3210 fn player_by_name(&mut self, name: &str) -> Result<wir::PlayerVarId> {
3211 if let Some(id) = self.players.get(name).copied() {
3212 return Ok(id);
3213 }
3214 let index = self.next_variable_index(true);
3215 let id = self.target.player_variables.push(wir::WorkshopVariable {
3216 name: name.to_string(),
3217 index,
3218 span: None,
3219 name_span: None,
3220 });
3221 self.players.insert(name.to_string(), id);
3222 Ok(id)
3223 }
3224
3225 fn next_variable_index(&self, player: bool) -> u32 {
3226 let variables = if player {
3227 &self.target.player_variables
3228 } else {
3229 &self.target.global_variables
3230 };
3231 variables
3232 .iter()
3233 .map(|variable| variable.index)
3234 .max()
3235 .map_or(0, |index| index.saturating_add(1))
3236 }
3237
3238 fn subroutine_by_name(&self, name: &str) -> Result<wir::SubroutineId> {
3239 self.subroutines
3240 .get(name)
3241 .copied()
3242 .ok_or_else(|| WorkshopError::Unknown {
3243 kind: "subroutine",
3244 spelling: name.to_string(),
3245 locale: self.locale.clone(),
3246 span: None,
3247 })
3248 }
3249
3250 fn phrase(&mut self) -> Result<(String, Position, Position)> {
3253 let mut words = Vec::new();
3254 let (start, mut end) = match self.peek() {
3255 Some(Token {
3256 kind: TokenKind::Word(word),
3257 start,
3258 end,
3259 }) => {
3260 words.push(word.clone());
3261 (start, end)
3262 }
3263 Some(Token {
3264 kind: TokenKind::Number { text, .. },
3265 start,
3266 end,
3267 }) => {
3268 words.push(text.clone());
3269 (start, end)
3270 }
3271 Some(token) => return Err(self.malformed("expected an identifier", &token)),
3272 None => return Err(self.malformed("expected an identifier", self.eof())),
3273 };
3274 self.pos += 1;
3275 while let Some(token) = self.peek() {
3276 match token {
3277 Token {
3278 kind: TokenKind::Word(word),
3279 end: word_end,
3280 ..
3281 } => {
3282 if matches!(
3283 self.peek_at(1).map(|token| token.kind),
3284 Some(TokenKind::Op(equal)) if equal == "="
3285 ) {
3286 break;
3287 }
3288 words.push(word.clone());
3289 end = word_end;
3290 self.pos += 1;
3291 }
3292 Token {
3296 kind: TokenKind::Number { text, .. },
3297 end: number_end,
3298 ..
3299 } => {
3300 words.push(text.clone());
3301 end = number_end;
3302 self.pos += 1;
3303 }
3304 _ => break,
3305 }
3306 }
3307 Ok((words.join(" "), start, end))
3308 }
3309
3310 fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
3313 let mut words = Vec::new();
3314 let (start, mut end, line) = match self.peek() {
3315 Some(Token {
3316 kind: TokenKind::Word(word),
3317 start,
3318 end,
3319 }) => {
3320 words.push(word.clone());
3321 (start, end, start.line)
3322 }
3323 Some(Token {
3324 kind: TokenKind::Number { text, .. },
3325 start,
3326 end,
3327 }) => {
3328 words.push(text.clone());
3329 (start, end, start.line)
3330 }
3331 Some(token) => return Err(self.malformed("expected an identifier", &token)),
3332 None => return Err(self.malformed("expected an identifier", self.eof())),
3333 };
3334 self.pos += 1;
3335 while let Some(token) = self.peek() {
3336 let (word, word_start, word_end) = match token {
3337 Token {
3338 kind: TokenKind::Word(word),
3339 start,
3340 end,
3341 } => (word, start, end),
3342 Token {
3343 kind: TokenKind::Number { text, .. },
3344 start,
3345 end,
3346 } => (text, start, end),
3347 Token {
3348 kind: TokenKind::Dot,
3349 start,
3350 end,
3351 } => (".".to_string(), start, end),
3352 Token {
3353 kind: TokenKind::Op(op),
3354 start,
3355 end,
3356 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
3357 _ => break,
3358 };
3359 if word_start.line != line {
3360 break;
3361 }
3362 words.push(word);
3363 end = word_end;
3364 self.pos += 1;
3365 }
3366 Ok((
3367 words
3368 .join(" ")
3369 .replace(" .", ".")
3370 .replace(". ", ".")
3371 .replace(" : ", ":")
3372 .replace(" %", "%"),
3373 start,
3374 end,
3375 ))
3376 }
3377
3378 fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
3379 let (mut phrase, start, mut end) = self.phrase_on_line()?;
3380 if matches!(
3381 self.peek(),
3382 Some(Token {
3383 kind: TokenKind::Colon,
3384 ..
3385 })
3386 ) {
3387 let colon_pos = self.pos;
3388 self.next();
3389 let (rest, _, rest_end) = self.phrase_on_line()?;
3390 if matches!(
3391 self.peek(),
3392 Some(Token {
3393 kind: TokenKind::LBrace,
3394 ..
3395 })
3396 ) {
3397 phrase.push(':');
3398 phrase.push(' ');
3399 phrase.push_str(&rest);
3400 end = rest_end;
3401 } else {
3402 self.pos = colon_pos;
3403 }
3404 }
3405 Ok((phrase, start, end))
3406 }
3407
3408 fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
3409 let first = self
3410 .peek()
3411 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
3412 let start = first.start;
3413 let line = first.start.line;
3414 let mut end = first.end;
3415 let mut parts = Vec::new();
3416 while let Some(token) = self.peek() {
3417 if token.start.line != line
3418 || matches!(token.kind, TokenKind::RParen | TokenKind::Comma)
3419 {
3420 break;
3421 }
3422 self.pos += 1;
3423 end = token.end;
3424 parts.push(raw_token_text(&token.kind));
3425 }
3426 if parts.is_empty() {
3427 return Err(self.malformed("expected an enum member", &first));
3428 }
3429 Ok((
3430 parts
3431 .join(" ")
3432 .replace(" : ", ":")
3433 .replace(" .", ".")
3434 .replace(". ", "."),
3435 start,
3436 end,
3437 ))
3438 }
3439
3440 fn line_text(&mut self) -> Result<String> {
3443 let mut parts = Vec::new();
3444 loop {
3445 match self.peek() {
3446 Some(Token {
3447 kind: TokenKind::Semi,
3448 ..
3449 }) => {
3450 self.pos += 1;
3451 break;
3452 }
3453 Some(Token {
3454 kind: TokenKind::Word(word),
3455 ..
3456 }) => {
3457 parts.push(word.clone());
3458 self.pos += 1;
3459 }
3460 Some(Token {
3461 kind: TokenKind::Op(op),
3462 ..
3463 }) if op == "-" => {
3464 parts.push("-".to_string());
3465 self.pos += 1;
3466 }
3467 Some(Token {
3468 kind: TokenKind::Number { value, .. },
3469 ..
3470 }) => {
3471 parts.push(value.to_string());
3472 self.pos += 1;
3473 }
3474 Some(Token {
3475 kind: TokenKind::Dot,
3476 ..
3477 }) => {
3478 parts.push(".".to_string());
3479 self.pos += 1;
3480 }
3481 Some(Token {
3482 kind: TokenKind::Colon,
3483 ..
3484 }) => {
3485 parts.push(":".to_string());
3486 self.pos += 1;
3487 }
3488 Some(token) => return Err(self.malformed("expected a text line", &token)),
3489 None => return Err(self.malformed("unexpected end of input in line", self.eof())),
3490 }
3491 }
3492 Ok(parts
3493 .join(" ")
3494 .replace(" .", ".")
3495 .replace(". ", ".")
3496 .replace(" : ", ":"))
3497 }
3498
3499 fn consume_phrase(&mut self, expected: &str) -> Result<()> {
3501 let (phrase, _, _) = self.phrase()?;
3502 if phrase != expected {
3503 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
3504 }
3505 Ok(())
3506 }
3507
3508 fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
3509 match self.next() {
3510 Some(Token {
3511 kind: TokenKind::Word(word),
3512 start,
3513 ..
3514 }) if self.canonical_keyword(&word) == expected => Ok(start),
3515 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
3516 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
3517 }
3518 }
3519
3520 fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
3521 match self.next() {
3522 Some(token) if token.kind == kind => Ok(()),
3523 Some(token) => Err(self.malformed(message, &token)),
3524 None => Err(self.malformed(message, self.eof())),
3525 }
3526 }
3527
3528 fn expect_string(&mut self, message: &str) -> Result<String> {
3529 match self.next() {
3530 Some(Token {
3531 kind: TokenKind::String(content),
3532 ..
3533 }) => Ok(content),
3534 Some(token) => Err(self.malformed(message, &token)),
3535 None => Err(self.malformed(message, self.eof())),
3536 }
3537 }
3538
3539 fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
3540 WorkshopError::Malformed {
3541 message: message.to_string(),
3542 span: Some(Span::new(self.file(), token.start, token.end)),
3543 }
3544 }
3545
3546 fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
3547 WorkshopError::Unknown {
3548 kind,
3549 spelling: spelling.to_string(),
3550 locale: self.locale.clone(),
3551 span: None,
3552 }
3553 }
3554
3555 fn peek(&self) -> Option<Token> {
3556 self.tokens.get(self.pos).cloned()
3557 }
3558
3559 fn peek_at(&self, offset: usize) -> Option<Token> {
3560 self.tokens.get(self.pos + offset).cloned()
3561 }
3562
3563 fn next(&mut self) -> Option<Token> {
3564 let token = self.tokens.get(self.pos).cloned();
3565 if token.is_some() {
3566 self.pos += 1;
3567 }
3568 token
3569 }
3570
3571 fn previous(&self) -> &Token {
3572 self.tokens
3573 .get(self.pos.saturating_sub(1))
3574 .unwrap_or_else(|| self.tokens.last().unwrap())
3575 }
3576
3577 fn previous_span(&self) -> (Position, Position) {
3578 let token = self.previous();
3579 (token.start, token.end)
3580 }
3581
3582 fn span_here(&self) -> (Position, Position) {
3583 let token = self
3584 .peek()
3585 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
3586 (token.start, token.end)
3587 }
3588
3589 fn eof(&self) -> &Token {
3590 self.tokens.last().unwrap()
3591 }
3592
3593 fn file(&self) -> crate::ids::Id<SourceFile> {
3594 crate::ids::Id::from_index(0)
3595 }
3596}
3597
3598fn is_comparison(op: &str) -> bool {
3599 matches!(op, "==" | "!=" | "<" | "<=" | ">" | ">=")
3600}
3601
3602fn canonical_keyword(keyword: &str) -> &str {
3603 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
3604 KEYWORDS
3605 .get_or_init(|| {
3606 serde_json::from_str(include_str!("structural_keywords.json"))
3607 .expect("structural keyword data is valid JSON")
3608 })
3609 .get(keyword)
3610 .map(String::as_str)
3611 .unwrap_or(keyword)
3612}
3613
3614fn raw_token_text(kind: &TokenKind) -> String {
3615 match kind {
3616 TokenKind::Word(value) => value.clone(),
3617 TokenKind::Number { text, .. } => text.clone(),
3618 TokenKind::String(value) => format!("\"{}\"", value.replace('"', "\\\"")),
3619 TokenKind::Op(value) => value.clone(),
3620 TokenKind::LParen => "(".to_string(),
3621 TokenKind::RParen => ")".to_string(),
3622 TokenKind::Comma => ",".to_string(),
3623 TokenKind::Semi => ";".to_string(),
3624 TokenKind::LBrace => "{".to_string(),
3625 TokenKind::RBrace => "}".to_string(),
3626 TokenKind::Colon => ":".to_string(),
3627 TokenKind::Dot => ".".to_string(),
3628 TokenKind::LBracket => "[".to_string(),
3629 TokenKind::RBracket => "]".to_string(),
3630 TokenKind::Eof => String::new(),
3631 }
3632}