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