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