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::BoolEnum(domain) => Ok(SettingsNode::Bool {
696 name: name.to_string(),
697 value: self.settings_bool_enum(domain)?,
698 span,
699 }),
700 KeyKind::Enum(domain) => Ok(SettingsNode::String {
701 name: name.to_string(),
702 value: self.resolve_enum_settings_name(domain)?,
703 span,
704 }),
705 KeyKind::ListMap | KeyKind::ListHero => {
706 Err(self.malformed("settings list requires a brace block", self.previous()))
707 }
708 }
709 }
710
711 fn settings_opaque_group(&mut self, name: &str, start: Position) -> Result<SettingsNode> {
712 Ok(SettingsNode::Group {
713 name: name.to_string(),
714 children: self.settings_opaque_members()?,
715 span: Some(self.settings_span(start)),
716 })
717 }
718
719 fn settings_opaque_members(&mut self) -> Result<Vec<SettingsNode>> {
720 let mut children = Vec::new();
721 while !matches!(self.peek().map(|token| token.kind), Some(TokenKind::RBrace)) {
722 let (display, start, _) = self.opaque_name_on_line()?;
723 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LBrace)) {
724 self.pos += 1;
725 children.push(self.settings_opaque_group(&display, start)?);
726 } else {
727 children.push(self.settings_raw_member(display, start)?);
728 }
729 }
730 self.expect(TokenKind::RBrace, "expected '}' after settings group")?;
731 Ok(children)
732 }
733
734 fn settings_raw_member(&mut self, name: String, start: Position) -> Result<SettingsNode> {
735 let mut value = String::new();
736 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::Colon)) {
737 self.pos += 1;
738 value = self.raw_settings_line()?;
739 }
740 let end = self.previous_span().1;
741 Ok(SettingsNode::Raw {
742 name,
743 value,
744 span: Some(Span::new(self.file(), start, end)),
745 })
746 }
747
748 fn opaque_name_on_line(&mut self) -> Result<(String, Position, Position)> {
749 let first = self
750 .peek()
751 .ok_or_else(|| self.malformed("expected an identifier", self.eof()))?;
752 let start = first.start;
753 let line = first.start.line;
754 let mut end = first.end;
755 let mut parts = Vec::new();
756 while let Some(token) = self.peek() {
757 if token.start.line != line
758 || matches!(
759 token.kind,
760 TokenKind::Colon | TokenKind::LBrace | TokenKind::RBrace
761 )
762 {
763 break;
764 }
765 self.pos += 1;
766 end = token.end;
767 parts.push(raw_token_text(&token.kind));
768 }
769 if parts.is_empty() {
770 return Err(self.malformed("expected an identifier", &first));
771 }
772 Ok((
773 parts
774 .join(" ")
775 .replace(" : ", ":")
776 .replace(" .", ".")
777 .replace(". ", "."),
778 start,
779 end,
780 ))
781 }
782
783 fn raw_settings_line(&mut self) -> Result<String> {
784 let line = self.peek().map(|token| token.start.line);
785 let mut parts = Vec::new();
786 while let Some(token) = self.peek() {
787 if line.is_some_and(|line| token.start.line != line)
788 || matches!(token.kind, TokenKind::RBrace)
789 {
790 break;
791 }
792 self.pos += 1;
793 parts.push(raw_token_text(&token.kind));
794 }
795 Ok(parts.join(" "))
796 }
797
798 fn settings_number(&mut self, percent: bool) -> Result<f64> {
799 let value = match self.next() {
800 Some(Token {
801 kind: TokenKind::Number { value, .. },
802 ..
803 }) => value,
804 Some(token) => return Err(self.malformed("expected a settings number", &token)),
805 None => return Err(self.malformed("expected a settings number", self.eof())),
806 };
807 if percent {
808 self.expect(
809 TokenKind::Op("%".to_string()),
810 "expected '%' after settings percentage",
811 )?;
812 }
813 Ok(value)
814 }
815
816 fn settings_number_percent(&mut self) -> Result<f64> {
817 let value = self.settings_number(false)?;
818 if matches!(
819 self.peek(),
820 Some(Token {
821 kind: TokenKind::Op(op),
822 ..
823 }) if op == "%"
824 ) {
825 self.pos += 1;
826 }
827 Ok(value)
828 }
829
830 fn settings_bool(&mut self) -> Result<bool> {
831 let token = self
832 .next()
833 .ok_or_else(|| self.malformed("expected a settings boolean", self.eof()))?;
834 let TokenKind::Word(value) = token.kind else {
835 return Err(self.malformed("expected a settings boolean", &token));
836 };
837 if self.settings_name_matches("tokens", "On", &value)
838 || self.settings_name_matches("tokens", "Yes", &value)
839 {
840 Ok(true)
841 } else if self.settings_name_matches("tokens", "Off", &value)
842 || self.settings_name_matches("tokens", "No", &value)
843 {
844 Ok(false)
845 } else {
846 Err(self.unknown("setting boolean", &value))
847 }
848 }
849
850 fn settings_bool_enum(&mut self, domain: &str) -> Result<bool> {
851 let member = self.resolve_enum_settings_name(domain)?;
852 if member == "enabled" {
853 Ok(true)
854 } else {
855 Err(self.unknown("setting boolean", &member))
856 }
857 }
858
859 fn resolve_enum_settings_name(&mut self, domain: &str) -> Result<String> {
860 let (display, _, _) = self.phrase_on_line()?;
861 table::ENUM_MEMBERS
862 .iter()
863 .find(|member| {
864 member.domain == domain
865 && self.settings_name_matches("enums", member.name, &display)
866 })
867 .map(|member| member.member.to_string())
868 .or_else(|| {
869 table::GENERATED_ENUM_MEMBERS
870 .iter()
871 .find(|member| {
872 member.domain == domain
873 && self.settings_name_matches("enums", member.name, &display)
874 })
875 .map(|member| member.member.to_string())
876 })
877 .ok_or_else(|| self.unknown("settings enum", &display))
878 }
879
880 fn resolve_settings_name(
881 &self,
882 names: &[table::NameMap],
883 section: &str,
884 display: &str,
885 ) -> Result<&'static str> {
886 names
887 .iter()
888 .find(|candidate| self.settings_name_matches(section, candidate.name, display))
889 .map(|candidate| candidate.key)
890 .ok_or_else(|| self.unknown("setting", display))
891 }
892
893 fn resolve_settings_name_extended(
894 &self,
895 names: &[table::NameMap],
896 generated: &[table::NameMap],
897 section: &str,
898 display: &str,
899 ) -> Result<&'static str> {
900 names
901 .iter()
902 .chain(generated.iter())
903 .find(|candidate| self.settings_name_matches(section, candidate.name, display))
904 .map(|candidate| candidate.key)
905 .ok_or_else(|| self.unknown("setting", display))
906 }
907
908 fn settings_name_matches_for_path(
909 &self,
910 candidate: &table::TableEntry,
911 display: &str,
912 hero: Option<&str>,
913 ) -> bool {
914 if let Some(PathPart::Part(key)) = candidate.path.last() {
915 if display == *key {
916 return true;
917 }
918 }
919 if let (Some(hero), Some(PathPart::Part(key))) = (hero, candidate.path.last()) {
920 if table::hero_setting_name(hero, key, self.locale.as_str()) == Some(display) {
921 return true;
922 }
923 if table::hero_setting_alias(hero, key, self.locale.as_str(), display) {
924 return true;
925 }
926 }
927 if let (Some(hero), Some(slot)) = (hero, table::ability_slot_for_path(candidate.path)) {
928 if candidate.workshop_name.contains("%1$s")
929 || matches!(
930 candidate.path.last(),
931 Some(PathPart::Part("enableAbility1" | "enableAbility2"))
932 )
933 {
934 return crate::gameplay_data::builtin()
935 .ok()
936 .and_then(|catalog| {
937 catalog
938 .query()
939 .ability_name(hero, slot, None, self.locale.as_str())
940 .ok()
941 .map(|name| name == display)
942 })
943 .unwrap_or(false);
944 }
945 }
946 self.settings_name_matches("labels", candidate.workshop_name, display)
947 }
948
949 fn settings_name_matches(&self, section: &str, english: &str, display: &str) -> bool {
950 let localized = table::localized_name(self.locale.as_str(), section, english);
951 localized
952 .is_some_and(|localized| localized == display)
953 || display == english
958 }
959
960 fn settings_span(&self, start: Position) -> Span {
961 Span::new(self.file(), start, self.previous_span().1)
962 }
963
964 fn variables_section(&mut self) -> Result<()> {
965 self.expect_keyword("variables")?;
966 self.expect(TokenKind::LBrace, "expected '{' after 'variables'")?;
967 let mut saw_section = false;
968 loop {
969 match self.peek() {
970 Some(Token {
971 kind: TokenKind::RBrace,
972 ..
973 }) => {
974 self.pos += 1;
975 break;
976 }
977 Some(Token {
978 kind: TokenKind::Word(word),
979 ..
980 }) if matches!(canonical_keyword(&word), "Global" | "global") => {
981 self.pos += 1;
982 self.expect(TokenKind::Colon, "expected ':' after 'global'")?;
983 while let Some(Token {
984 kind: TokenKind::Number { .. },
985 ..
986 }) = self.peek()
987 {
988 let variable = self.variable_line()?;
989 let id = self.target.global_variables.push(variable);
990 self.globals.insert(
991 self.target.global_variables.get(id).unwrap().name.clone(),
992 id,
993 );
994 }
995 saw_section = true;
996 }
997 Some(Token {
998 kind: TokenKind::Word(word),
999 ..
1000 }) if canonical_keyword(&word) == "player" => {
1001 self.pos += 1;
1002 self.expect(TokenKind::Colon, "expected ':' after 'player'")?;
1003 while let Some(Token {
1004 kind: TokenKind::Number { .. },
1005 ..
1006 }) = self.peek()
1007 {
1008 let variable = self.variable_line()?;
1009 let id = self.target.player_variables.push(variable);
1010 self.players.insert(
1011 self.target.player_variables.get(id).unwrap().name.clone(),
1012 id,
1013 );
1014 }
1015 saw_section = true;
1016 }
1017 Some(token) => {
1018 return Err(self.malformed("expected 'global', 'player', or '}'", &token));
1019 }
1020 None => {
1021 return Err(self.malformed("unexpected end of input in variables", self.eof()));
1022 }
1023 }
1024 }
1025 if !saw_section {
1026 return Err(self.malformed("variables section is empty", self.previous()));
1027 }
1028 Ok(())
1029 }
1030
1031 fn variable_line(&mut self) -> Result<wir::WorkshopVariable> {
1032 let (index, span) = match self.next() {
1033 Some(Token {
1034 kind: TokenKind::Number { value, .. },
1035 start,
1036 end,
1037 }) => (
1038 value as u32,
1039 Span::new(synthetic_span(start).file, start, end),
1040 ),
1041 Some(token) => return Err(self.malformed("expected a variable index", &token)),
1042 None => return Err(self.malformed("expected a variable index", self.eof())),
1043 };
1044 self.expect(TokenKind::Colon, "expected ':' after variable index")?;
1045 let (name, name_start, name_end) = self.phrase_on_line()?;
1046 let name_span = Span::new(self.file(), name_start, name_end);
1047 Ok(wir::WorkshopVariable {
1048 name,
1049 index,
1050 span: Some(if span.file.index() == 0 {
1051 name_span
1052 } else {
1053 span
1054 }),
1055 name_span: None,
1058 })
1059 }
1060
1061 fn subroutines_section(&mut self) -> Result<()> {
1062 self.expect_keyword("subroutines")?;
1063 self.expect(TokenKind::LBrace, "expected '{' after 'subroutines'")?;
1064 while let Some(Token {
1065 kind: TokenKind::Number { .. },
1066 ..
1067 }) = self.peek()
1068 {
1069 let index = match self.next() {
1070 Some(Token {
1071 kind: TokenKind::Number { value, .. },
1072 ..
1073 }) => value as u32,
1074 _ => unreachable!(),
1075 };
1076 self.expect(TokenKind::Colon, "expected ':' after subroutine index")?;
1077 let (name, start, end) = self.phrase_on_line()?;
1078 let id = self.target.subroutines.push(wir::WorkshopSubroutine {
1079 name,
1080 index,
1081 span: Some(Span::new(self.file(), start, end)),
1082 name_span: None,
1083 });
1084 self.subroutines
1085 .insert(self.target.subroutines.get(id).unwrap().name.clone(), id);
1086 }
1087 self.expect(TokenKind::RBrace, "expected '}' after subroutines")?;
1088 Ok(())
1089 }
1090
1091 fn rule(&mut self, disabled: bool) -> Result<()> {
1092 self.expect_keyword("rule")?;
1093 self.expect(TokenKind::LParen, "expected '(' after 'rule'")?;
1094 let name = self.expect_string("expected a rule name string")?;
1095 self.expect(TokenKind::RParen, "expected ')' after rule name")?;
1096 let (rule_start, rule_end) = self.previous_span();
1097 self.expect(TokenKind::LBrace, "expected '{' after rule header")?;
1098
1099 let mut rule = wir::Rule {
1100 name,
1101 span: Some(Span::new(self.file(), rule_start, rule_end)),
1102 name_span: None,
1103 disabled,
1104 event: Event::Global,
1105 conditions: Vec::new(),
1106 actions: Vec::new(),
1107 };
1108 let mut seen_sections = Vec::new();
1109 loop {
1110 match self.peek() {
1111 Some(Token {
1112 kind: TokenKind::RBrace,
1113 ..
1114 }) => {
1115 self.pos += 1;
1116 break;
1117 }
1118 Some(Token {
1119 kind: TokenKind::Word(word),
1120 ..
1121 }) => match self.canonical_keyword(&word).as_str() {
1122 "event" => {
1123 if seen_sections.contains(&"event") {
1124 return Err(
1125 self.malformed("duplicate 'event' section", &self.peek().unwrap())
1126 );
1127 }
1128 seen_sections.push("event");
1129 rule.event = self.event_section()?;
1130 }
1131 "conditions" => {
1132 if seen_sections.contains(&"conditions") {
1133 return Err(self.malformed(
1134 "duplicate 'conditions' section",
1135 &self.peek().unwrap(),
1136 ));
1137 }
1138 seen_sections.push("conditions");
1139 rule.conditions = self.conditions_section()?;
1140 }
1141 "actions" => {
1142 if seen_sections.contains(&"actions") {
1143 return Err(self
1144 .malformed("duplicate 'actions' section", &self.peek().unwrap()));
1145 }
1146 seen_sections.push("actions");
1147 rule.actions = self.actions_section()?;
1148 }
1149 _ => return Err(self.unknown("rule section", &word)),
1150 },
1151 Some(token) => {
1152 return Err(self.malformed("expected a rule section or '}'", &token));
1153 }
1154 None => return Err(self.malformed("unexpected end of input in rule", self.eof())),
1155 }
1156 }
1157 self.target.rules.push(rule);
1158 Ok(())
1159 }
1160
1161 fn event_section(&mut self) -> Result<Event> {
1162 self.expect_keyword("event")?;
1163 self.expect(TokenKind::LBrace, "expected '{' after 'event'")?;
1164 let mut lines: Vec<String> = Vec::new();
1165 loop {
1166 match self.peek() {
1167 Some(Token {
1168 kind: TokenKind::RBrace,
1169 ..
1170 }) => {
1171 self.pos += 1;
1172 break;
1173 }
1174 Some(Token {
1175 kind: TokenKind::Semi,
1176 ..
1177 }) => {
1178 self.pos += 1;
1179 lines.push(String::new());
1180 }
1181 Some(_) => {
1182 let text = self.line_text()?;
1183 lines.push(text);
1184 }
1185 None => return Err(self.malformed("unexpected end of input in event", self.eof())),
1186 }
1187 }
1188 let Some(name_line) = lines.first().cloned() else {
1189 return Err(self.malformed("event section is empty", self.previous()));
1190 };
1191 let name_line = name_line.trim();
1192 let entry = self
1193 .catalog
1194 .resolve(Kind::Event, &self.locale, name_line)
1195 .ok_or_else(|| WorkshopError::Unknown {
1196 kind: "event",
1197 spelling: name_line.to_string(),
1198 locale: self.locale.clone(),
1199 span: None,
1200 })?;
1201 match entry.id.as_str() {
1202 "global" => {
1203 if lines[1..].iter().any(|line| !line.trim().is_empty()) {
1204 return Err(self.unsupported_event_parameters("global"));
1205 }
1206 Ok(Event::Global)
1207 }
1208 "eachPlayer" => {
1209 if lines[1..].iter().all(|line| line.trim().is_empty()) {
1210 return Ok(Event::EachPlayer);
1211 }
1212 let (team, target) = self.event_filters(&lines, "eachPlayer", true)?;
1213 Ok(Event::EachPlayerWithFilters { team, target })
1214 }
1215 "playerDealtDamage" => self.player_event(&lines, PlayerEventKind::DealtDamage),
1216 "playerDealtFinalBlow" => self.player_event(&lines, PlayerEventKind::DealtFinalBlow),
1217 "playerDealtHealing" => self.player_event(&lines, PlayerEventKind::DealtHealing),
1218 "playerDealtKnockback" => self.player_event(&lines, PlayerEventKind::DealtKnockback),
1219 "playerDied" => self.player_event(&lines, PlayerEventKind::Died),
1220 "playerEarnedElimination" => {
1221 self.player_event(&lines, PlayerEventKind::EarnedElimination)
1222 }
1223 "playerJoined" => self.player_event(&lines, PlayerEventKind::Joined),
1224 "playerLeft" => self.player_event(&lines, PlayerEventKind::Left),
1225 "playerReceivedHealing" => self.player_event(&lines, PlayerEventKind::ReceivedHealing),
1226 "playerReceivedKnockback" => {
1227 self.player_event(&lines, PlayerEventKind::ReceivedKnockback)
1228 }
1229 "playerTookDamage" => self.player_event(&lines, PlayerEventKind::TookDamage),
1230 "subroutine" => {
1231 if lines
1232 .get(2..)
1233 .unwrap_or(&[])
1234 .iter()
1235 .any(|line| !line.trim().is_empty())
1236 {
1237 return Err(self.unsupported_event_parameters("subroutine"));
1238 }
1239 let Some(sub_name) = lines.get(1).map(|s| s.trim()) else {
1240 return Err(self.malformed(
1241 "subroutine event requires a subroutine name",
1242 self.previous(),
1243 ));
1244 };
1245 let id = self.subroutine_by_name(sub_name)?;
1246 Ok(Event::Subroutine(id))
1247 }
1248 other => Err(WorkshopError::Unsupported {
1249 message: format!("unsupported event '{other}'"),
1250 span: None,
1251 }),
1252 }
1253 }
1254
1255 fn player_event(&self, lines: &[String], kind: PlayerEventKind) -> Result<Event> {
1256 let (team, target) = self.event_filters(lines, kind.catalog_id(), false)?;
1257 Ok(Event::Player { kind, team, target })
1258 }
1259
1260 fn event_filters(
1261 &self,
1262 lines: &[String],
1263 event_id: &str,
1264 allow_empty: bool,
1265 ) -> Result<(EventTeam, EventTarget)> {
1266 let parameters: Vec<&str> = lines[1..]
1267 .iter()
1268 .map(String::as_str)
1269 .map(str::trim)
1270 .filter(|line| !line.is_empty())
1271 .collect();
1272 if parameters.is_empty() {
1273 if allow_empty {
1274 return Ok((EventTeam::All, EventTarget::All));
1275 }
1276 return Err(WorkshopError::Malformed {
1277 message: format!("event '{event_id}' requires team and player parameters"),
1278 span: None,
1279 });
1280 }
1281 if parameters.len() != 2 {
1282 if event_id == "eachPlayer" {
1283 return Err(WorkshopError::Unsupported {
1284 message: format!("event '{event_id}' requires both team and player parameters"),
1285 span: None,
1286 });
1287 }
1288 return Err(WorkshopError::Malformed {
1289 message: format!("event '{event_id}' requires team and player parameters"),
1290 span: None,
1291 });
1292 }
1293 let team_member = self
1294 .resolve_enum_member_mixed("EventTeam", parameters[0])
1295 .map(|(_, member)| member);
1296 let team = match team_member.as_deref() {
1297 Some("ALL") => EventTeam::All,
1298 Some("TEAM_1") => EventTeam::Team1,
1299 Some("TEAM_2") => EventTeam::Team2,
1300 _ => return Err(self.unknown("event team", parameters[0])),
1301 };
1302 let target = if let Some((_, member)) =
1303 self.resolve_enum_member_mixed("EventPlayer", parameters[1])
1304 {
1305 if member == "ALL" {
1306 EventTarget::All
1307 } else if let Some(slot) = member.strip_prefix("SLOT_") {
1308 let slot = slot
1309 .parse::<u8>()
1310 .map_err(|_| self.unknown("event player", parameters[1]))?;
1311 EventTarget::Slot(slot)
1312 } else {
1313 return Err(self.unknown("event player", parameters[1]));
1314 }
1315 } else if let Some((_, hero)) = self
1316 .catalog
1317 .bare_member_matches(&self.locale, parameters[1])
1318 .into_iter()
1319 .chain(
1320 self.catalog
1321 .bare_member_matches(&self.locale, ¶meters[1].replace(':', ": ")),
1322 )
1323 .find(|(domain, _)| domain == "Hero")
1324 {
1325 EventTarget::Hero(hero)
1326 } else {
1327 return Err(self.unknown("event player", parameters[1]));
1328 };
1329 Ok((team, target))
1330 }
1331
1332 fn unsupported_event_parameters(&self, event_id: &str) -> WorkshopError {
1333 WorkshopError::Unsupported {
1334 message: format!("event '{event_id}' does not accept parameters"),
1335 span: None,
1336 }
1337 }
1338
1339 fn conditions_section(&mut self) -> Result<Vec<wir::ValueId>> {
1340 self.expect_keyword("conditions")?;
1341 self.expect(TokenKind::LBrace, "expected '{' after 'conditions'")?;
1342 let mut conditions = Vec::new();
1343 loop {
1344 match self.peek() {
1345 Some(Token {
1346 kind: TokenKind::RBrace,
1347 ..
1348 }) => {
1349 self.pos += 1;
1350 break;
1351 }
1352 Some(Token {
1353 kind: TokenKind::Semi,
1354 ..
1355 }) => {
1356 self.pos += 1;
1357 }
1358 Some(_) => {
1359 if matches!(
1360 self.peek(),
1361 Some(Token {
1362 kind: TokenKind::String(_),
1363 ..
1364 })
1365 ) {
1366 self.pos += 1;
1367 continue;
1368 }
1369 if let Some(Token {
1370 kind: TokenKind::Word(word),
1371 ..
1372 }) = self.peek()
1373 {
1374 if self.settings_name_matches("tokens", "disabled", &word) {
1375 self.pos += 1;
1376 let _disabled_condition = self.value()?;
1377 self.expect(TokenKind::Semi, "expected ';' after condition")?;
1378 continue;
1379 }
1380 }
1381 let condition = self.value()?;
1382 self.expect(TokenKind::Semi, "expected ';' after condition")?;
1383 conditions.push(condition);
1384 }
1385 None => {
1386 return Err(self.malformed("unexpected end of input in conditions", self.eof()));
1387 }
1388 }
1389 }
1390 Ok(conditions)
1391 }
1392
1393 fn actions_section(&mut self) -> Result<Vec<wir::ActionId>> {
1394 self.expect_keyword("actions")?;
1395 self.expect(TokenKind::LBrace, "expected '{' after 'actions'")?;
1396 let mut all_actions = Vec::new();
1397 loop {
1398 let (actions, stop) = self.actions_until_end()?;
1399 all_actions.extend(actions);
1400 if stop == Stop::SectionClosed {
1401 return Ok(all_actions);
1402 }
1403 all_actions.push(self.opaque_action()?);
1408 }
1409 }
1410
1411 fn actions_until_end(&mut self) -> Result<(Vec<wir::ActionId>, Stop)> {
1415 let mut actions = Vec::new();
1416 loop {
1417 match self.peek() {
1418 Some(Token {
1419 kind: TokenKind::RBrace,
1420 ..
1421 }) => {
1422 self.pos += 1;
1423 return Ok((actions, Stop::SectionClosed));
1424 }
1425 Some(Token {
1426 kind: TokenKind::Word(_),
1427 ..
1428 }) => {
1429 if let Some(action) = self.assignment_action()? {
1430 actions.push(action);
1431 continue;
1432 }
1433 let saved = self.pos;
1434 let (phrase, start, end) = self.phrase()?;
1435 if let Some(rest) = self.disabled_action_rest(&phrase) {
1436 if let Some(structural) = self.resolve_entry(Kind::Structural, rest) {
1437 match structural.id.as_str() {
1438 "while" => {
1439 actions.push(self.while_group()?);
1440 continue;
1441 }
1442 "if" => {
1443 actions.push(self.if_group()?);
1444 continue;
1445 }
1446 _ => {}
1447 }
1448 }
1449 if self.resolve_entry(Kind::Action, rest).is_some() {
1450 let canonical = rest;
1451 actions.push(self.action_call_from_phrase(
1452 canonical.to_string(),
1453 start,
1454 end,
1455 )?);
1456 continue;
1457 }
1458 }
1459 if canonical_keyword(&phrase) == "disabled"
1460 && matches!(
1461 self.peek(),
1462 Some(Token {
1463 kind: TokenKind::Word(word),
1464 ..
1465 }) if canonical_keyword(&word) == "While"
1466 )
1467 {
1468 self.pos += 1;
1469 actions.push(self.while_group()?);
1470 continue;
1471 }
1472 match self.canonical_keyword(&phrase).as_str() {
1473 "end" => {
1474 self.pos = saved;
1475 return Ok((actions, Stop::End));
1476 }
1477 "elseIf" => {
1478 self.pos = saved;
1479 return Ok((actions, Stop::ElseIf));
1480 }
1481 "else" => {
1482 self.pos = saved;
1483 return Ok((actions, Stop::Else));
1484 }
1485 "if" => actions.push(self.if_group()?),
1486 "forGlobalVariable" => actions.push(self.for_group()?),
1487 "forPlayerVariable" => actions.push(self.for_player_group()?),
1488 "while" => actions.push(self.while_group()?),
1489 "Loop" => actions.push(self.action_call_from_phrase(phrase, start, end)?),
1490 "Loop If Condition Is True" => {
1491 actions.push(self.action_call_from_phrase(phrase, start, end)?)
1492 }
1493 "Global" | "Event Player" => {
1494 self.pos = saved;
1495 actions.push(self.opaque_action()?);
1496 }
1497 _ => {
1498 if self.line_has_assignment() {
1499 self.pos = saved;
1500 actions.push(self.opaque_action()?);
1501 } else {
1502 actions.push(self.action_call_from_phrase(phrase, start, end)?);
1503 }
1504 }
1505 }
1506 }
1507 Some(Token {
1508 kind: TokenKind::String(_),
1509 ..
1510 }) => {
1511 self.pos += 1;
1515 }
1516 Some(token) => {
1517 let saved = self.pos;
1518 if let Some(action) = self.member_assignment_action(saved, token.start)? {
1519 actions.push(action);
1520 } else {
1521 return Err(self.malformed("expected an action", &token));
1522 }
1523 }
1524 None => {
1525 return Err(self.malformed("unexpected end of input in actions", self.eof()));
1526 }
1527 }
1528 }
1529 }
1530
1531 fn disabled_action_rest<'a>(&self, phrase: &'a str) -> Option<&'a str> {
1535 if let Some(rest) = phrase.strip_prefix("disabled ") {
1536 return Some(rest);
1537 }
1538 let localized = table::localized_name(self.locale.as_str(), "tokens", "disabled")?;
1539 phrase.strip_prefix(localized)?.strip_prefix(' ')
1540 }
1541
1542 fn assignment_action(&mut self) -> Result<Option<wir::ActionId>> {
1543 let saved = self.pos;
1544 let Some(Token {
1545 kind: TokenKind::Word(first),
1546 start,
1547 ..
1548 }) = self.peek()
1549 else {
1550 return Ok(None);
1551 };
1552
1553 if matches!(canonical_keyword(&first), "Global" | "global") {
1554 self.pos += 1;
1555 self.expect(TokenKind::Dot, "expected '.' after 'Global'")?;
1556 let (name, _, target_end) = self.phrase()?;
1557 if matches!(
1558 self.peek().map(|token| token.kind),
1559 Some(TokenKind::LBracket)
1560 ) {
1561 self.pos += 1;
1562 let index = self.value()?;
1563 self.expect(
1564 TokenKind::RBracket,
1565 "expected ']' after global variable index",
1566 )?;
1567 let operator = self.assignment_operator()?.ok_or_else(|| {
1568 self.malformed(
1569 "expected assignment after global variable index",
1570 self.peek().as_ref().unwrap_or(self.eof()),
1571 )
1572 })?;
1573 let variable = self.global_by_name(&name)?;
1574 let target = self.target.values.push(ValueNode::new(
1575 Value::GlobalVariable(variable),
1576 Some(Span::new(self.file(), start, target_end)),
1577 ));
1578 let value = self.value()?;
1579 self.expect(TokenKind::Semi, "expected ';' after indexed assignment")?;
1580 return Ok(Some(self.indexed_assignment_action(
1581 true, target, index, operator, value, start,
1582 )));
1583 }
1584 let Some(operator) = self.assignment_operator()? else {
1585 return self.member_assignment_action(saved, start);
1586 };
1587 let variable = self.global_by_name(&name)?;
1588 let value = self.value()?;
1589 let value = if let AssignmentOperator::Modify(op) = &operator {
1590 self.normalize_modify_value(*op, value)
1591 } else {
1592 value
1593 };
1594 self.expect(TokenKind::Semi, "expected ';' after assignment")?;
1595 let span = Some(Span::new(self.file(), start, self.previous_span().1));
1596 let target_span = Some(Span::new(self.file(), start, target_end));
1597 return Ok(Some(self.target.actions.push(match operator {
1598 AssignmentOperator::Set => Action::SetGlobalVariable {
1599 variable,
1600 value,
1601 span,
1602 target_span,
1603 },
1604 AssignmentOperator::Modify(op) => Action::ModifyGlobalVariable {
1605 variable,
1606 op,
1607 value,
1608 span,
1609 target_span,
1610 },
1611 })));
1612 }
1613
1614 let first_canonical = canonical_keyword(&first);
1615 let is_event_player = first_canonical == "Event Player"
1616 || (matches!(first_canonical, "Event" | "event")
1617 && matches!(
1618 self.peek_at(1).map(|token| token.kind),
1619 Some(TokenKind::Word(word))
1620 if matches!(canonical_keyword(&word), "Player" | "player")
1621 ));
1622 if !is_event_player {
1623 if !matches!(first_canonical, "Event" | "event") {
1630 return self.member_assignment_action(saved, start);
1631 }
1632 return Ok(None);
1633 }
1634
1635 if first_canonical == "Event Player" {
1636 self.pos += 1;
1637 } else {
1638 self.pos += 2;
1639 }
1640 let event_player = self.target.values.push(ValueNode::new(
1641 Value::EventPlayer,
1642 Some(Span::new(self.file(), start, self.previous_span().1)),
1643 ));
1644 self.expect(TokenKind::Dot, "expected '.' after 'Event Player'")?;
1645 let (name, target_start, target_end) = self.phrase()?;
1646 let variable = self.player_by_name(&name)?;
1647 if matches!(
1648 self.peek().map(|token| token.kind),
1649 Some(TokenKind::LBracket)
1650 ) {
1651 self.pos += 1;
1652 let index = self.value()?;
1653 self.expect(
1654 TokenKind::RBracket,
1655 "expected ']' after player variable index",
1656 )?;
1657 let operator = self.assignment_operator()?.ok_or_else(|| {
1658 self.malformed(
1659 "expected assignment after player variable index",
1660 self.peek().as_ref().unwrap_or(self.eof()),
1661 )
1662 })?;
1663 let value = self.value()?;
1664 self.expect(TokenKind::Semi, "expected ';' after indexed assignment")?;
1665 let variable_value = self.target.values.push(ValueNode::new(
1666 Value::PlayerVariable {
1667 player: event_player,
1668 variable,
1669 },
1670 Some(Span::new(self.file(), target_start, target_end)),
1671 ));
1672 return Ok(Some(self.indexed_assignment_action(
1673 false,
1674 variable_value,
1675 index,
1676 operator,
1677 value,
1678 start,
1679 )));
1680 }
1681 let Some(operator) = self.assignment_operator()? else {
1682 return self.member_assignment_action(saved, start);
1683 };
1684 let value = self.value()?;
1685 let value = if let AssignmentOperator::Modify(op) = &operator {
1686 self.normalize_modify_value(*op, value)
1687 } else {
1688 value
1689 };
1690 self.expect(TokenKind::Semi, "expected ';' after assignment")?;
1691 let span = Some(Span::new(self.file(), start, self.previous_span().1));
1692 let target_span = Some(Span::new(self.file(), target_start, target_end));
1693 Ok(Some(self.target.actions.push(match operator {
1694 AssignmentOperator::Set => Action::SetPlayerVariable {
1695 player: event_player,
1696 variable,
1697 value,
1698 span,
1699 target_span,
1700 },
1701 AssignmentOperator::Modify(op) => Action::ModifyPlayerVariable {
1702 player: event_player,
1703 variable,
1704 op,
1705 value,
1706 span,
1707 target_span,
1708 },
1709 })))
1710 }
1711
1712 fn member_assignment_action(
1713 &mut self,
1714 saved: usize,
1715 start: Position,
1716 ) -> Result<Option<wir::ActionId>> {
1717 self.pos = saved;
1718 if !self.line_has_assignment() {
1719 return Ok(None);
1720 }
1721 let target = self.value()?;
1722 let Some(operator) = self.assignment_operator()? else {
1723 self.pos = saved;
1724 return Ok(None);
1725 };
1726 let value = self.value()?;
1727 self.expect(TokenKind::Semi, "expected ';' after member assignment")?;
1728 let op = match operator {
1729 AssignmentOperator::Set => None,
1730 AssignmentOperator::Modify(op) => Some(op),
1731 };
1732 Ok(Some(self.target.actions.push(Action::AssignMember {
1733 target,
1734 op,
1735 value,
1736 span: Some(Span::new(self.file(), start, self.previous_span().1)),
1737 })))
1738 }
1739
1740 fn indexed_assignment_action(
1741 &mut self,
1742 global: bool,
1743 variable: wir::ValueId,
1744 index: wir::ValueId,
1745 operator: AssignmentOperator,
1746 value: wir::ValueId,
1747 start: Position,
1748 ) -> wir::ActionId {
1749 let name = match &operator {
1750 AssignmentOperator::Set => {
1751 if global {
1752 "setGlobalVariableAtIndex"
1753 } else {
1754 "setPlayerVariableAtIndex"
1755 }
1756 }
1757 AssignmentOperator::Modify(_) => {
1758 if global {
1759 "modifyGlobalVariableAtIndex"
1760 } else {
1761 "modifyPlayerVariableAtIndex"
1762 }
1763 }
1764 };
1765 let index = self.normalize_contextual_argument(name, 1, index);
1766 let (value, modify_op) = match operator {
1767 AssignmentOperator::Set => (value, None),
1768 AssignmentOperator::Modify(op) => (self.normalize_modify_value(op, value), Some(op)),
1769 };
1770 let args = match modify_op {
1771 None => vec![variable, index, value],
1772 Some(op) => vec![
1773 variable,
1774 index,
1775 self.target.values.push(ValueNode::new(
1776 Value::Call {
1777 name: op.catalog_id().to_string(),
1778 args: Vec::new(),
1779 },
1780 None,
1781 )),
1782 value,
1783 ],
1784 };
1785 self.target.actions.push(Action::Call {
1786 name: name.to_string(),
1787 args,
1788 span: Some(Span::new(self.file(), start, self.previous_span().1)),
1789 })
1790 }
1791
1792 fn assignment_operator(&mut self) -> Result<Option<AssignmentOperator>> {
1793 let Some(token) = self.peek() else {
1794 return Ok(None);
1795 };
1796 if let TokenKind::Word(word) = &token.kind {
1797 let op = match word.as_str() {
1798 "min" => ModifyOp::Min,
1799 "max" => ModifyOp::Max,
1800 _ => {
1801 if matches!(self.peek_at(1).map(|token| token.kind), Some(TokenKind::Op(equal)) if equal == "=")
1802 {
1803 return Err(WorkshopError::Unsupported {
1804 message: format!("unsupported assignment operator '{word}='"),
1805 span: Some(Span::new(
1806 self.file(),
1807 token.start,
1808 self.peek_at(1).unwrap().end,
1809 )),
1810 });
1811 }
1812 return Ok(None);
1813 }
1814 };
1815 if !matches!(self.peek_at(1).map(|token| token.kind), Some(TokenKind::Op(equal)) if equal == "=")
1816 {
1817 return Ok(None);
1818 }
1819 self.pos += 2;
1820 return Ok(Some(AssignmentOperator::Modify(op)));
1821 }
1822 let operator = match &token.kind {
1823 TokenKind::Op(operator) => operator.clone(),
1824 _ => return Ok(None),
1825 };
1826 if operator == "=" {
1827 self.pos += 1;
1828 return Ok(Some(AssignmentOperator::Set));
1829 }
1830 let op = match operator.as_str() {
1831 "+" => ModifyOp::Add,
1832 "-" => ModifyOp::Subtract,
1833 "*" => ModifyOp::Multiply,
1834 "/" => ModifyOp::Divide,
1835 "%" => ModifyOp::Modulo,
1836 _ => return Ok(None),
1837 };
1838 if !matches!(self.peek_at(1).map(|token| token.kind), Some(TokenKind::Op(equal)) if equal == "=")
1839 {
1840 return Ok(None);
1841 }
1842 self.pos += 2;
1843 Ok(Some(AssignmentOperator::Modify(op)))
1844 }
1845
1846 fn opaque_action(&mut self) -> Result<wir::ActionId> {
1847 let start = self
1848 .peek()
1849 .map(|token| token.start)
1850 .unwrap_or(self.eof().start);
1851 while let Some(token) = self.peek() {
1852 self.pos += 1;
1853 if matches!(token.kind, TokenKind::Semi) {
1854 break;
1855 }
1856 }
1857 Ok(self.target.actions.push(Action::Call {
1858 name: "rawWorkshopAction".to_string(),
1859 args: Vec::new(),
1860 span: Some(Span::new(self.file(), start, self.previous_span().1)),
1861 }))
1862 }
1863
1864 fn if_group(&mut self) -> Result<wir::ActionId> {
1865 let start = self.previous_span().0;
1866 self.expect(TokenKind::LParen, "expected '(' after 'If'")?;
1867 let condition = self.value()?;
1868 self.expect(TokenKind::RParen, "expected ')' after If condition")?;
1869 self.expect(TokenKind::Semi, "expected ';' after If condition")?;
1870
1871 let mut branches = Vec::new();
1872 let mut stop = {
1873 let (body, stop) = self.actions_until_end()?;
1874 branches.push(wir::IfBranch { condition, body });
1875 stop
1876 };
1877
1878 let mut else_body = None;
1879 loop {
1880 match stop {
1881 Stop::End => {
1882 self.consume_phrase("End")?;
1883 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1884 break;
1885 }
1886 Stop::ElseIf => {
1887 self.consume_phrase("Else If")?;
1888 self.expect(TokenKind::LParen, "expected '(' after 'Else If'")?;
1889 let condition = self.value()?;
1890 self.expect(TokenKind::RParen, "expected ')' after Else If condition")?;
1891 self.expect(TokenKind::Semi, "expected ';' after Else If condition")?;
1892 let (body, next) = self.actions_until_end()?;
1893 branches.push(wir::IfBranch { condition, body });
1894 stop = next;
1895 }
1896 Stop::Else => {
1897 self.consume_phrase("Else")?;
1898 self.expect(TokenKind::Semi, "expected ';' after 'Else'")?;
1899 let (body, next) = self.actions_until_end()?;
1900 else_body = Some(body);
1901 stop = next;
1902 }
1903 Stop::SectionClosed => {
1904 self.pos -= 1;
1909 break;
1910 }
1911 }
1912 }
1913 let end_span = self.previous_span();
1914 let action = Action::If {
1915 branches,
1916 else_body,
1917 span: Some(Span::new(self.file(), start, end_span.1)),
1918 };
1919 Ok(self.target.actions.push(action))
1920 }
1921
1922 fn for_group(&mut self) -> Result<wir::ActionId> {
1923 let start = self.previous_span().0;
1924 self.expect(
1925 TokenKind::LParen,
1926 "expected '(' after 'For Global Variable'",
1927 )?;
1928 let (name, _, _) = self.phrase()?;
1929 let variable = self.global_by_name(&name)?;
1930 self.expect(TokenKind::Comma, "expected ',' after loop variable")?;
1931 let start_value = self.value()?;
1932 self.expect(TokenKind::Comma, "expected ',' after start")?;
1933 let stop = self.value()?;
1934 self.expect(TokenKind::Comma, "expected ',' after stop")?;
1935 let step = self.value()?;
1936 self.expect(TokenKind::RParen, "expected ')' after For bounds")?;
1937 self.expect(TokenKind::Semi, "expected ';' after For Global Variable")?;
1938 let (body, loop_stop) = self.actions_until_end()?;
1939 if loop_stop != Stop::End {
1940 return Err(self.malformed(
1941 "'For Global Variable' requires a matching 'End'",
1942 self.previous(),
1943 ));
1944 }
1945 self.consume_phrase("End")?;
1946 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1947 let end_span = self.previous_span();
1948 let action = Action::ForGlobalVariable {
1949 variable,
1950 start: start_value,
1951 stop,
1952 step,
1953 body,
1954 span: Some(Span::new(self.file(), start, end_span.1)),
1955 target_span: None,
1956 };
1957 Ok(self.target.actions.push(action))
1958 }
1959
1960 fn while_group(&mut self) -> Result<wir::ActionId> {
1961 let start = self.previous_span().0;
1962 self.expect(TokenKind::LParen, "expected '(' after 'While'")?;
1963 let condition = self.value()?;
1964 self.expect(TokenKind::RParen, "expected ')' after While condition")?;
1965 self.expect(TokenKind::Semi, "expected ';' after While condition")?;
1966 let (body, stop) = self.actions_until_end()?;
1967 if stop != Stop::End {
1968 return Err(self.malformed("'While' requires a matching 'End'", self.previous()));
1969 }
1970 self.consume_phrase("End")?;
1971 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
1972 let end_span = self.previous_span();
1973 let action = Action::While {
1974 condition,
1975 body,
1976 span: Some(Span::new(self.file(), start, end_span.1)),
1977 };
1978 Ok(self.target.actions.push(action))
1979 }
1980
1981 fn for_player_group(&mut self) -> Result<wir::ActionId> {
1986 let start = self.previous_span().0;
1987 self.expect(
1988 TokenKind::LParen,
1989 "expected '(' after 'For Player Variable'",
1990 )?;
1991 let player = self.value()?;
1992 self.expect(TokenKind::Comma, "expected ',' after loop player")?;
1993 let (name, _, _) = self.phrase()?;
1994 let variable = self.player_by_name(&name)?;
1995 self.expect(TokenKind::Comma, "expected ',' after loop variable")?;
1996 let start_value = self.value()?;
1997 self.expect(TokenKind::Comma, "expected ',' after start")?;
1998 let stop = self.value()?;
1999 self.expect(TokenKind::Comma, "expected ',' after stop")?;
2000 let step = self.value()?;
2001 self.expect(TokenKind::RParen, "expected ')' after For bounds")?;
2002 self.expect(TokenKind::Semi, "expected ';' after For Player Variable")?;
2003 let (body, loop_stop) = self.actions_until_end()?;
2004 if loop_stop != Stop::End {
2005 return Err(self.malformed(
2006 "'For Player Variable' requires a matching 'End'",
2007 self.previous(),
2008 ));
2009 }
2010 self.consume_phrase("End")?;
2011 self.expect(TokenKind::Semi, "expected ';' after 'End'")?;
2012 let end_span = self.previous_span();
2013 let action = Action::ForPlayerVariable {
2014 player,
2015 variable,
2016 start: start_value,
2017 stop,
2018 step,
2019 body,
2020 span: Some(Span::new(self.file(), start, end_span.1)),
2021 };
2022 Ok(self.target.actions.push(action))
2023 }
2024
2025 fn action_call_from_phrase(
2026 &mut self,
2027 phrase: String,
2028 start: Position,
2029 end: Position,
2030 ) -> Result<wir::ActionId> {
2031 match self
2032 .catalog
2033 .resolve(Kind::Structural, &self.locale, &phrase)
2034 {
2035 Some(entry) => match entry.id.as_str() {
2036 "setGlobalVariable" => {
2037 self.expect(
2038 TokenKind::LParen,
2039 "expected '(' after 'Set Global Variable'",
2040 )?;
2041 let (name, _, _) = self.phrase()?;
2042 let variable = self.global_by_name(&name)?;
2043 self.expect(TokenKind::Comma, "expected ',' after variable")?;
2044 let value = self.value()?;
2045 self.expect(TokenKind::RParen, "expected ')'")?;
2046 self.expect(TokenKind::Semi, "expected ';'")?;
2047 Ok(self.target.actions.push(Action::SetGlobalVariable {
2048 variable,
2049 value,
2050 span: Some(Span::new(self.file(), start, end)),
2051 target_span: None,
2052 }))
2053 }
2054 "modifyGlobalVariable" => {
2055 self.expect(TokenKind::LParen, "expected '('")?;
2056 let (name, _, _) = self.phrase()?;
2057 let variable = self.global_by_name(&name)?;
2058 self.expect(TokenKind::Comma, "expected ',' after variable")?;
2059 let op = self.modify_op()?;
2060 self.expect(TokenKind::Comma, "expected ',' after modify operator")?;
2061 let value = self.value()?;
2062 let value = self.normalize_modify_value(op, value);
2063 self.expect(TokenKind::RParen, "expected ')'")?;
2064 self.expect(TokenKind::Semi, "expected ';'")?;
2065 Ok(self.target.actions.push(Action::ModifyGlobalVariable {
2066 variable,
2067 op,
2068 value,
2069 span: Some(Span::new(self.file(), start, end)),
2070 target_span: None,
2071 }))
2072 }
2073 "setPlayerVariable" => {
2074 self.expect(TokenKind::LParen, "expected '('")?;
2075 let player = self.value()?;
2076 self.expect(TokenKind::Comma, "expected ',' after player")?;
2077 let (name, _, _) = self.phrase()?;
2078 let variable = self.player_by_name(&name)?;
2079 self.expect(TokenKind::Comma, "expected ',' after variable")?;
2080 let value = self.value()?;
2081 self.expect(TokenKind::RParen, "expected ')'")?;
2082 self.expect(TokenKind::Semi, "expected ';'")?;
2083 Ok(self.target.actions.push(Action::SetPlayerVariable {
2084 player,
2085 variable,
2086 value,
2087 span: Some(Span::new(self.file(), start, end)),
2088 target_span: None,
2089 }))
2090 }
2091 "modifyPlayerVariable" => {
2092 self.expect(TokenKind::LParen, "expected '('")?;
2093 let player = self.value()?;
2094 self.expect(TokenKind::Comma, "expected ',' after player")?;
2095 let (name, _, _) = self.phrase()?;
2096 let variable = self.player_by_name(&name)?;
2097 self.expect(TokenKind::Comma, "expected ',' after variable")?;
2098 let op = self.modify_op()?;
2099 self.expect(TokenKind::Comma, "expected ',' after modify operator")?;
2100 let value = self.value()?;
2101 let value = self.normalize_modify_value(op, value);
2102 self.expect(TokenKind::RParen, "expected ')'")?;
2103 self.expect(TokenKind::Semi, "expected ';'")?;
2104 Ok(self.target.actions.push(Action::ModifyPlayerVariable {
2105 player,
2106 variable,
2107 op,
2108 value,
2109 span: Some(Span::new(self.file(), start, end)),
2110 target_span: None,
2111 }))
2112 }
2113 "forGlobalVariable" => self.for_group(),
2114 "forPlayerVariable" => self.for_player_group(),
2115 "callSubroutine" => {
2116 self.expect(TokenKind::LParen, "expected '('")?;
2117 let (name, _, _) = self.phrase()?;
2118 let subroutine = self.subroutine_by_name(&name)?;
2119 self.expect(TokenKind::RParen, "expected ')'")?;
2120 self.expect(TokenKind::Semi, "expected ';'")?;
2121 Ok(self.target.actions.push(Action::CallSubroutine {
2122 subroutine,
2123 span: Some(Span::new(self.file(), start, end)),
2124 callee_span: None,
2125 }))
2126 }
2127 other => Err(WorkshopError::Unsupported {
2128 message: format!(
2129 "structural action '{other}' is not supported in action position"
2130 ),
2131 span: Some(Span::new(self.file(), start, end)),
2132 }),
2133 },
2134 None => {
2135 let Some(action) = self
2137 .resolve_entry(Kind::Action, &phrase)
2138 .or_else(|| self.resolve_entry(Kind::Action, &format!("{phrase} ")))
2139 .or_else(|| {
2140 let alias = match phrase.as_str() {
2141 "Set Player Allowed Heroes" => "Set Allowed Heroes",
2142 "设置技能充能" => "设置终极技能充能",
2143 _ => return None,
2144 };
2145 self.resolve_entry(Kind::Action, alias)
2146 })
2147 else {
2148 return Err(WorkshopError::Unknown {
2149 kind: "action",
2150 spelling: phrase,
2151 locale: self.locale.clone(),
2152 span: Some(Span::new(self.file(), start, end)),
2153 });
2154 };
2155 match action.id.as_str() {
2163 "chasePlayerVariableAtRate" | "chasePlayerVariableOverTime" => {
2164 self.expect(TokenKind::LParen, "expected '('")?;
2165 let player = self.value()?;
2166 self.expect(TokenKind::Comma, "expected ',' after player")?;
2167 let (name, _, _) = self.phrase()?;
2168 let variable = self.player_by_name(&name)?;
2169 let mut args = Vec::with_capacity(4);
2170 args.push(self.target.values.push(wir::ValueNode::new(
2171 wir::Value::PlayerVariable { player, variable },
2172 None,
2173 )));
2174 let mut arg_index = 2usize;
2179 loop {
2180 match self.peek() {
2181 Some(Token {
2182 kind: TokenKind::RParen,
2183 ..
2184 }) => break,
2185 Some(Token {
2186 kind: TokenKind::Comma,
2187 ..
2188 }) => {
2189 self.pos += 1;
2190 }
2191 _ => {}
2192 }
2193 let saved = self.expected_domain;
2194 self.expected_domain =
2195 self.context.expected_domain(action.id.as_str(), arg_index);
2196 let arg = self.value()?;
2197 self.expected_domain = saved;
2198 args.push(arg);
2199 arg_index += 1;
2200 }
2201 self.expect(TokenKind::RParen, "expected ')'")?;
2202 self.expect(TokenKind::Semi, "expected ';' after action")?;
2203 let canonical = if action.id == "chasePlayerVariableAtRate" {
2204 "chaseAtRate"
2205 } else {
2206 "chaseOverTime"
2207 };
2208 return Ok(self.target.actions.push(Action::Call {
2209 name: canonical.to_string(),
2210 args,
2211 span: Some(Span::new(self.file(), start, end)),
2212 }));
2213 }
2214 "startRule" => {
2215 self.expect(TokenKind::LParen, "expected '('")?;
2216 let (name, _, _) = self.phrase()?;
2217 let subroutine = self.subroutine_by_name(&name)?;
2218 self.expect(TokenKind::Comma, "expected ',' after subroutine")?;
2219 let saved = self.expected_domain;
2220 self.expected_domain = self.context.expected_domain(action.id.as_str(), 1);
2221 let behavior = self.value()?;
2222 self.expected_domain = saved;
2223 self.expect(TokenKind::RParen, "expected ')'")?;
2224 self.expect(TokenKind::Semi, "expected ';' after action")?;
2225 let subroutine_value = self
2226 .target
2227 .values
2228 .push(ValueNode::new(Value::Subroutine(subroutine), None));
2229 return Ok(self.target.actions.push(Action::Call {
2230 name: action.id.clone(),
2231 args: vec![subroutine_value, behavior],
2232 span: Some(Span::new(self.file(), start, end)),
2233 }));
2234 }
2235 "stopChasingPlayerVariable" => {
2236 self.expect(TokenKind::LParen, "expected '('")?;
2237 let player = self.value()?;
2238 self.expect(TokenKind::Comma, "expected ',' after player")?;
2239 let (name, _, _) = self.phrase()?;
2240 let variable = self.player_by_name(&name)?;
2241 self.expect(TokenKind::RParen, "expected ')'")?;
2242 self.expect(TokenKind::Semi, "expected ';' after action")?;
2243 let player_variable = self.target.values.push(ValueNode::new(
2244 Value::PlayerVariable { player, variable },
2245 None,
2246 ));
2247 return Ok(self.target.actions.push(Action::Call {
2248 name: action.id.clone(),
2249 args: vec![player_variable],
2250 span: Some(Span::new(self.file(), start, end)),
2251 }));
2252 }
2253 _ => {}
2254 }
2255 let args = if let Some(Token {
2256 kind: TokenKind::LParen,
2257 ..
2258 }) = self.peek()
2259 {
2260 self.pos += 1;
2261 let args = self.value_args(action.id.as_str())?;
2262 self.expect(TokenKind::RParen, "expected ')'")?;
2263 args
2264 } else {
2265 Vec::new()
2266 };
2267 self.expect(TokenKind::Semi, "expected ';' after action")?;
2268 Ok(self.target.actions.push(Action::Call {
2269 name: action.id.clone(),
2270 args,
2271 span: Some(Span::new(self.file(), start, end)),
2272 }))
2273 }
2274 }
2275 }
2276
2277 fn modify_op(&mut self) -> Result<ModifyOp> {
2278 let (phrase, start, end) = self.phrase()?;
2279 if phrase == "根据值从数组中移除" {
2280 return Ok(ModifyOp::RemoveFromArray);
2281 }
2282 if phrase == "根据索引从数组中移除" {
2283 return Ok(ModifyOp::RemoveFromArrayByIndex);
2284 }
2285 let entry = self
2286 .catalog
2287 .resolve(Kind::Operator, &self.locale, &phrase)
2288 .ok_or_else(|| WorkshopError::Unknown {
2289 kind: "modify operator",
2290 spelling: phrase.clone(),
2291 locale: self.locale.clone(),
2292 span: Some(Span::new(self.file(), start, end)),
2293 })?;
2294 let op = match entry.id.as_str() {
2295 "add" => ModifyOp::Add,
2296 "subtract" => ModifyOp::Subtract,
2297 "multiply" => ModifyOp::Multiply,
2298 "divide" => ModifyOp::Divide,
2299 "modulo" => ModifyOp::Modulo,
2300 "min" => ModifyOp::Min,
2301 "max" => ModifyOp::Max,
2302 "raiseToPower" => ModifyOp::RaiseToPower,
2303 "appendToArray" => ModifyOp::AppendToArray,
2304 "removeFromArray" | "removeFromArrayByValue" => ModifyOp::RemoveFromArray,
2305 "removeFromArrayByIndex" => ModifyOp::RemoveFromArrayByIndex,
2306 other => {
2307 return Err(WorkshopError::Unsupported {
2308 message: format!("unsupported modify operator '{other}'"),
2309 span: Some(Span::new(self.file(), start, end)),
2310 });
2311 }
2312 };
2313 Ok(op)
2314 }
2315
2316 fn value(&mut self) -> Result<wir::ValueId> {
2317 let mut value = self.primary()?;
2318 loop {
2319 if let Some(Token {
2320 kind: TokenKind::LBracket,
2321 start,
2322 ..
2323 }) = self.peek()
2324 {
2325 self.pos += 1;
2326 let index = self.value()?;
2327 let index = self.normalize_contextual_argument("valueInArray", 1, index);
2328 let end = self.peek().map(|token| token.end).unwrap_or(start);
2329 self.expect(TokenKind::RBracket, "expected ']' after array index")?;
2330 value = self.target.values.push(ValueNode::new(
2331 Value::Call {
2332 name: "valueInArray".to_string(),
2333 args: vec![value, index],
2334 },
2335 Some(Span::new(self.file(), start, end)),
2336 ));
2337 continue;
2338 }
2339 if matches!(
2340 self.peek(),
2341 Some(Token {
2342 kind: TokenKind::Dot,
2343 ..
2344 })
2345 ) {
2346 self.pos += 1;
2347 let (name, _, _) = self.phrase()?;
2348 let member = self
2349 .target
2350 .values
2351 .push(ValueNode::new(Value::String(name), None));
2352 let mut args = vec![value, member];
2353 if matches!(
2354 self.peek(),
2355 Some(Token {
2356 kind: TokenKind::LBracket,
2357 ..
2358 })
2359 ) {
2360 self.pos += 1;
2361 args.push(self.value()?);
2362 self.expect(TokenKind::RBracket, "expected ']' after member index")?;
2363 }
2364 value = self.target.values.push(ValueNode::new(
2365 Value::Call {
2366 name: "memberAccess".to_string(),
2367 args,
2368 },
2369 None,
2370 ));
2371 continue;
2372 }
2373 if let Some(Token {
2374 kind: TokenKind::Op(op),
2375 start,
2376 end,
2377 }) = self.peek()
2378 {
2379 if op == "?" {
2380 self.pos += 1;
2381 let when_true = self.value()?;
2382 self.expect(TokenKind::Colon, "expected ':' in conditional value")?;
2383 let when_false = self.value()?;
2384 let when_true = self.normalize_contextual_argument("ifThenElse", 1, when_true);
2385 let when_false =
2386 self.normalize_contextual_argument("ifThenElse", 2, when_false);
2387 value = self.target.values.push(ValueNode::new(
2388 Value::Call {
2389 name: "ifThenElse".to_string(),
2390 args: vec![value, when_true, when_false],
2391 },
2392 Some(Span::new(self.file(), start, end)),
2393 ));
2394 continue;
2395 }
2396 let compound_assignment = matches!(
2397 self.peek_at(1).map(|token| token.kind),
2398 Some(TokenKind::Op(equal)) if equal == "="
2399 );
2400 if !compound_assignment
2401 && (is_comparison(&op)
2402 || matches!(op.as_str(), "and" | "or" | "+" | "-" | "*" | "/" | "%"))
2403 {
2404 self.pos += 1;
2405 let right = self.primary()?;
2406 let name = match op.as_str() {
2407 "+" => "add",
2408 "-" => "subtract",
2409 "*" => "multiply",
2410 "/" => "divide",
2411 "%" => "modulo",
2412 _ => op.as_str(),
2413 };
2414 let left = self.normalize_contextual_argument(name, 0, value);
2415 let right = self.normalize_contextual_argument(name, 1, right);
2416 value = self.target.values.push(ValueNode::new(
2417 Value::Call {
2418 name: name.to_string(),
2419 args: vec![left, right],
2420 },
2421 Some(Span::new(self.file(), start, end)),
2422 ));
2423 continue;
2424 }
2425 }
2426 break;
2427 }
2428 Ok(value)
2429 }
2430
2431 fn primary(&mut self) -> Result<wir::ValueId> {
2432 match self.peek() {
2433 Some(Token {
2434 kind: TokenKind::Op(op),
2435 start,
2436 ..
2437 }) if op == "not" => {
2438 self.pos += 1;
2439 let value = self.primary()?;
2440 Ok(self.target.values.push(ValueNode::new(
2441 Value::Call {
2442 name: "not".to_string(),
2443 args: vec![value],
2444 },
2445 Some(Span::new(self.file(), start, self.previous_span().1)),
2446 )))
2447 }
2448 Some(Token {
2449 kind: TokenKind::Number { value, text },
2450 start,
2451 end,
2452 }) => {
2453 let span = Some(Span::new(self.file(), start, end));
2454 self.pos += 1;
2455 Ok(self
2456 .target
2457 .values
2458 .push(ValueNode::new(Value::Number { value, text }, span)))
2459 }
2460 Some(Token {
2461 kind: TokenKind::Op(op),
2462 start,
2463 ..
2464 }) if op == "-" => {
2465 if let Some(Token {
2466 kind: TokenKind::Number { value, text },
2467 end: number_end,
2468 ..
2469 }) = self.peek_at(1)
2470 {
2471 let span = Some(Span::new(self.file(), start, number_end));
2472 self.pos += 2;
2473 Ok(self.target.values.push(ValueNode::new(
2474 Value::Number {
2475 value: -value,
2476 text: format!("-{text}"),
2477 },
2478 span,
2479 )))
2480 } else {
2481 Err(self.malformed("expected a number after '-'", &self.peek().unwrap()))
2482 }
2483 }
2484 Some(Token {
2485 kind: TokenKind::String(content),
2486 start,
2487 end,
2488 }) => {
2489 let span = Some(Span::new(self.file(), start, end));
2490 self.pos += 1;
2491 Ok(self
2492 .target
2493 .values
2494 .push(ValueNode::new(Value::String(content), span)))
2495 }
2496 Some(Token {
2497 kind: TokenKind::Word(word),
2498 ..
2499 }) if matches!(canonical_keyword(&word), "Global" | "global") => {
2500 let (start, end) = self.span_here();
2501 self.pos += 1;
2502 if let Some(Token {
2506 kind: TokenKind::Word(next),
2507 ..
2508 }) = self.peek()
2509 {
2510 if canonical_keyword(&next) == "Variable"
2511 || canonical_keyword(&next) == "variable"
2512 {
2513 self.pos += 1;
2514 self.expect(TokenKind::LParen, "expected '(' after 'Global Variable'")?;
2515 let (name, _, _) = self.phrase()?;
2516 let variable = self.global_by_name(&name)?;
2517 self.expect(TokenKind::RParen, "expected ')' after Global Variable")?;
2518 let span = Some(Span::new(self.file(), start, end));
2519 return Ok(self
2520 .target
2521 .values
2522 .push(ValueNode::new(Value::GlobalVariable(variable), span)));
2523 }
2524 }
2525 self.expect(TokenKind::Dot, "expected '.' after 'Global'")?;
2526 let (name, _, _) = self.phrase()?;
2527 let variable = self.global_by_name(&name)?;
2528 let span = Some(Span::new(self.file(), start, end));
2529 Ok(self
2530 .target
2531 .values
2532 .push(ValueNode::new(Value::GlobalVariable(variable), span)))
2533 }
2534 Some(Token {
2535 kind: TokenKind::Word(word),
2536 ..
2537 }) if matches!(canonical_keyword(&word), "Player" | "player")
2538 && matches!(
2539 self.peek_at(1),
2540 Some(Token {
2541 kind: TokenKind::Word(next),
2542 ..
2543 }) if matches!(canonical_keyword(&next), "Variable" | "variable")
2544 ) =>
2545 {
2546 let (start, end) = self.span_here();
2550 self.pos += 1;
2551 let saved = self.expected_domain;
2552 self.expected_domain = None;
2553 let result = (|| {
2554 self.consume_phrase("Variable")?;
2555 self.expect(TokenKind::LParen, "expected '(' after 'Player Variable'")?;
2556 let player = self.value()?;
2557 self.expect(TokenKind::Comma, "expected ',' after player")?;
2558 let (name, _, _) = self.phrase()?;
2559 let variable = self.player_by_name(&name)?;
2560 self.expect(TokenKind::RParen, "expected ')' after Player Variable")?;
2561 Ok(self.target.values.push(ValueNode::new(
2562 Value::PlayerVariable { player, variable },
2563 Some(Span::new(self.file(), start, end)),
2564 )))
2565 })();
2566 self.expected_domain = saved;
2567 result
2568 }
2569 Some(Token {
2570 kind: TokenKind::Word(word),
2571 ..
2572 }) if matches!(canonical_keyword(&word), "Event" | "event" | "Event Player") => {
2573 let (start, _) = self.span_here();
2574 self.pos += 1;
2575 if canonical_keyword(&word) == "Event Player" {
2576 let player = self.target.values.push(ValueNode::new(
2577 Value::EventPlayer,
2578 Some(Span::new(self.file(), start, self.previous_span().1)),
2579 ));
2580 if matches!(
2581 self.peek(),
2582 Some(Token {
2583 kind: TokenKind::Dot,
2584 ..
2585 })
2586 ) {
2587 self.pos += 1;
2588 let (name, name_start, name_end) = self.phrase()?;
2589 let variable = self.player_by_name(&name)?;
2590 return Ok(self.target.values.push(ValueNode::new(
2591 Value::PlayerVariable { player, variable },
2592 Some(Span::new(self.file(), name_start, name_end)),
2593 )));
2594 }
2595 return Ok(player);
2596 }
2597 if canonical_keyword(&word) != "Event Player"
2598 && matches!(self.peek(), Some(Token { kind: TokenKind::Word(next), .. }) if matches!(canonical_keyword(&next), "Player" | "player"))
2599 {
2600 self.pos += 1;
2601 let player = self.target.values.push(ValueNode::new(
2602 Value::EventPlayer,
2603 Some(Span::new(self.file(), start, self.previous_span().1)),
2604 ));
2605 if matches!(
2606 self.peek(),
2607 Some(Token {
2608 kind: TokenKind::Dot,
2609 ..
2610 })
2611 ) {
2612 self.pos += 1;
2613 let (name, name_start, name_end) = self.phrase()?;
2614 let variable = self.player_by_name(&name)?;
2615 return Ok(self.target.values.push(ValueNode::new(
2616 Value::PlayerVariable { player, variable },
2617 Some(Span::new(self.file(), name_start, name_end)),
2618 )));
2619 }
2620 return Ok(player);
2621 }
2622 self.pos -= 1;
2623 let (phrase, start, end) = self.phrase()?;
2624 if matches!(
2625 self.peek(),
2626 Some(Token {
2627 kind: TokenKind::LParen,
2628 ..
2629 })
2630 ) {
2631 self.call_or_enum(&phrase, start, end)
2632 } else {
2633 self.bare_member(&phrase, start, end)
2634 }
2635 }
2636 Some(Token {
2637 kind: TokenKind::LParen,
2638 ..
2639 }) => {
2640 self.pos += 1;
2643 let inner = self.value()?;
2644 if matches!(
2645 self.peek(),
2646 Some(Token {
2647 kind: TokenKind::Dot,
2648 ..
2649 })
2650 ) {
2651 self.pos += 1;
2652 let (name, _, _) = self.phrase()?;
2653 let member = self
2654 .target
2655 .values
2656 .push(ValueNode::new(Value::String(name), None));
2657 let mut args = vec![inner, member];
2658 if matches!(
2659 self.peek(),
2660 Some(Token {
2661 kind: TokenKind::LBracket,
2662 ..
2663 })
2664 ) {
2665 self.pos += 1;
2666 args.push(self.value()?);
2667 self.expect(TokenKind::RBracket, "expected ']' after member index")?;
2668 }
2669 let mut accessed = self.target.values.push(ValueNode::new(
2670 Value::Call {
2671 name: "memberAccess".to_string(),
2672 args,
2673 },
2674 None,
2675 ));
2676 if matches!(self.peek(), Some(Token { kind: TokenKind::Op(op), .. }) if op == "?")
2677 {
2678 self.pos += 1;
2679 let when_true = self.value()?;
2680 self.expect(TokenKind::Colon, "expected ':' in conditional value")?;
2681 let when_false = self.value()?;
2682 accessed = self.target.values.push(ValueNode::new(
2683 Value::Call {
2684 name: "ifThenElse".to_string(),
2685 args: vec![accessed, when_true, when_false],
2686 },
2687 None,
2688 ));
2689 }
2690 self.expect(TokenKind::RParen, "expected ')' after member access")?;
2691 return Ok(accessed);
2692 }
2693 self.expect(TokenKind::RParen, "expected ')' after parenthesized value")?;
2694 if let Some(Token {
2695 kind: TokenKind::Dot,
2696 ..
2697 }) = self.peek()
2698 {
2699 self.pos += 1;
2700 let (name, _, _) = self.phrase()?;
2701 if matches!(
2702 self.target.values.get(inner),
2703 Some(ValueNode {
2704 value: Value::EventPlayer,
2705 ..
2706 })
2707 ) || self.players.contains_key(&name)
2708 {
2709 let variable = self
2710 .players
2711 .get(&name)
2712 .copied()
2713 .unwrap_or(self.player_by_name(&name)?);
2714 Ok(self.target.values.push(ValueNode::new(
2715 Value::PlayerVariable {
2716 player: inner,
2717 variable,
2718 },
2719 None,
2720 )))
2721 } else {
2722 let member = self
2723 .target
2724 .values
2725 .push(ValueNode::new(Value::String(name), None));
2726 let mut args = vec![inner, member];
2727 if matches!(
2728 self.peek(),
2729 Some(Token {
2730 kind: TokenKind::LBracket,
2731 ..
2732 })
2733 ) {
2734 self.pos += 1;
2735 args.push(self.value()?);
2736 self.expect(TokenKind::RBracket, "expected ']' after member index")?;
2737 }
2738 Ok(self.target.values.push(ValueNode::new(
2739 Value::Call {
2740 name: "memberAccess".to_string(),
2741 args,
2742 },
2743 None,
2744 )))
2745 }
2746 } else {
2747 Ok(inner)
2748 }
2749 }
2750 _ => {
2751 if let Some((phrase, start, end)) = self.catalog_phrase_with_dot() {
2752 return self.bare_member_resolved(&phrase, start, end);
2753 }
2754 let (phrase, start, end) = self.phrase()?;
2755 match canonical_keyword(&phrase) {
2756 "True" | "真" => Ok(self.push_bool(true, start, end)),
2757 "False" | "假" => Ok(self.push_bool(false, start, end)),
2758 "Event Player" => Ok(self.target.values.push(ValueNode::new(
2759 Value::EventPlayer,
2760 Some(Span::new(self.file(), start, end)),
2761 ))),
2762 "Null" => Ok(self.target.values.push(ValueNode::new(
2763 Value::Null,
2764 Some(Span::new(self.file(), start, end)),
2765 ))),
2766 _ => {
2767 if let Some(Token {
2768 kind: TokenKind::LParen,
2769 ..
2770 }) = self.peek()
2771 {
2772 self.call_or_enum(&phrase, start, end)
2773 } else {
2774 self.bare_member(&phrase, start, end)
2775 }
2776 }
2777 }
2778 }
2779 }
2780 }
2781
2782 fn catalog_phrase_with_dot(&mut self) -> Option<(String, Position, Position)> {
2787 let first = self.peek()?;
2788 if !matches!(first.kind, TokenKind::Word(_)) {
2789 return None;
2790 }
2791 let start = first.start;
2792 let mut end = first.end;
2793 let mut parts = Vec::new();
2794 let mut has_dot = false;
2795 let mut index = self.pos;
2796 while let Some(token) = self.tokens.get(index) {
2797 match &token.kind {
2798 TokenKind::Word(word) => parts.push(word.clone()),
2799 TokenKind::Number { text, .. } => parts.push(text.clone()),
2800 TokenKind::Dot => {
2801 has_dot = true;
2802 parts.push(".".to_string());
2803 }
2804 _ => break,
2805 }
2806 end = token.end;
2807 index += 1;
2808 }
2809 if !has_dot {
2810 return None;
2811 }
2812 let phrase = parts.join(" ").replace(" .", ".").replace(". ", ".");
2813 let known_value = self.resolve_entry(Kind::Value, &phrase).is_some();
2814 let known_member = !self
2815 .catalog
2816 .bare_member_matches(&self.locale, &phrase)
2817 .is_empty();
2818 if !known_value && !known_member {
2819 return None;
2820 }
2821 self.pos = index;
2822 Some((phrase, start, end))
2823 }
2824
2825 fn call_or_enum(
2826 &mut self,
2827 phrase: &str,
2828 start: Position,
2829 end: Position,
2830 ) -> Result<wir::ValueId> {
2831 let prefer_enum = self.catalog.enum_domain("Hero").is_some()
2835 && (canonical_keyword(phrase) == "Hero"
2836 || self.resolve_enum_domain_mixed(phrase) == Some("Hero"));
2837 if !prefer_enum {
2838 if let Some(entry) = self.resolve_entry(Kind::Value, phrase) {
2839 self.expect(TokenKind::LParen, "expected '(' after value name")?;
2840 if entry.id == "compare" {
2841 let saved = self.expected_domain;
2845 self.expected_domain = None;
2846 let left = self.value();
2847 self.expected_domain = saved;
2848 let left = left?;
2849 self.expect(TokenKind::Comma, "expected ',' after Compare operand")?;
2850 let (op, op_start, op_end) = match self.next() {
2851 Some(Token {
2852 kind: TokenKind::Op(op),
2853 start,
2854 end,
2855 }) => (op, start, end),
2856 Some(token) => {
2857 return Err(
2858 self.malformed("expected a comparison operator in Compare", &token)
2859 );
2860 }
2861 None => {
2862 return Err(self.malformed(
2863 "expected a comparison operator in Compare",
2864 self.eof(),
2865 ));
2866 }
2867 };
2868 self.expect(TokenKind::Comma, "expected ',' after Compare operator")?;
2869 let saved = self.expected_domain;
2870 self.expected_domain = None;
2871 let right = self.value();
2872 self.expected_domain = saved;
2873 let right = right?;
2874 self.expect(TokenKind::RParen, "expected ')'")?;
2875 let span = Some(Span::new(self.file(), op_start, op_end));
2876 return Ok(self.target.values.push(ValueNode::new(
2877 Value::Call {
2878 name: op,
2879 args: vec![left, right],
2880 },
2881 span,
2882 )));
2883 }
2884 let args = self.value_args(entry.id.as_str())?;
2885 self.expect(TokenKind::RParen, "expected ')'")?;
2886 return Ok(self.target.values.push(ValueNode::new(
2887 Value::Call {
2888 name: entry.id.clone(),
2889 args,
2890 },
2891 Some(Span::new(self.file(), start, end)),
2892 )));
2893 }
2894 }
2895 if let Some(domain_name) = self
2896 .resolve_enum_domain_mixed(phrase)
2897 .or_else(|| self.resolve_enum_domain_mixed(canonical_keyword(phrase)))
2898 {
2899 let domain = self
2900 .catalog
2901 .enum_domain(domain_name)
2902 .expect("resolved enum domain must exist");
2903 self.expect(TokenKind::LParen, "expected '('")?;
2905 let (member_phrase, _, _) = self.enum_member_phrase()?;
2906 let member = self
2907 .resolve_enum_member_mixed(&domain.domain, &member_phrase)
2908 .unwrap_or_else(|| (domain.domain.clone(), member_phrase.clone()));
2909 self.expect(TokenKind::RParen, "expected ')' after enum member")?;
2910 return Ok(self.target.values.push(ValueNode::new(
2911 Value::Enum {
2912 value_type: member.0,
2913 value: member.1,
2914 },
2915 Some(Span::new(self.file(), start, end)),
2916 )));
2917 }
2918 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::LParen)) {
2919 self.pos += 1;
2920 self.call_stack.push(phrase.to_string());
2921 let args = self.opaque_value_args()?;
2922 self.call_stack.pop();
2923 self.expect(TokenKind::RParen, "expected ')' after value call")?;
2924 return Ok(self.target.values.push(ValueNode::new(
2925 Value::Call {
2926 name: phrase.to_string(),
2927 args,
2928 },
2929 Some(Span::new(self.file(), start, end)),
2930 )));
2931 }
2932 Ok(self.target.values.push(ValueNode::new(
2933 Value::Call {
2934 name: phrase.to_string(),
2935 args: Vec::new(),
2936 },
2937 Some(Span::new(self.file(), start, end)),
2938 )))
2939 }
2940
2941 fn bare_member(
2942 &mut self,
2943 phrase: &str,
2944 start: Position,
2945 end: Position,
2946 ) -> Result<wir::ValueId> {
2947 if !matches!(phrase, "None" | "无" | "True" | "真" | "False" | "假")
2952 && matches!(self.peek().map(|token| token.kind), Some(TokenKind::Colon))
2953 {
2954 let saved = self.pos;
2955 self.pos += 1;
2956 let (suffix, _, suffix_end) = self.phrase()?;
2957 let owned_phrase = format!("{phrase}: {suffix}");
2958 let recognized = self
2959 .expected_domain
2960 .and_then(|domain| self.resolve_enum_member_mixed(domain, &owned_phrase))
2961 .or_else(|| self.resolve_enum_member_mixed("Hero", &owned_phrase));
2962 if recognized.is_some() {
2963 return self.bare_member_resolved(&owned_phrase, start, suffix_end);
2964 }
2965 self.pos = saved;
2966 }
2967 self.bare_member_resolved(phrase, start, end)
2968 }
2969
2970 fn bare_member_resolved(
2971 &mut self,
2972 phrase: &str,
2973 start: Position,
2974 end: Position,
2975 ) -> Result<wir::ValueId> {
2976 match (matches!(phrase, "None" | "无"), self.expected_domain) {
2977 (true, Some(expected))
2978 if matches!(
2979 expected,
2980 "ChaseTimeReeval"
2981 | "ChaseRateReeval"
2982 | "Invis"
2983 | "ThrottleReeval"
2984 | "EffectReeval"
2985 ) =>
2986 {
2987 return Ok(self.target.values.push(ValueNode::new(
2988 Value::Enum {
2989 value_type: expected.to_string(),
2990 value: "NONE".to_string(),
2991 },
2992 Some(Span::new(self.file(), start, end)),
2993 )));
2994 }
2995 _ => {}
2996 }
2997 if let Some(variable) = self.globals.get(phrase).copied() {
2998 return Ok(self.target.values.push(ValueNode::new(
2999 Value::GlobalVariable(variable),
3000 Some(Span::new(self.file(), start, end)),
3001 )));
3002 }
3003 if let Some(expected) = self.expected_domain {
3004 if let Some((value_type, value)) = self.resolve_enum_member_mixed(expected, phrase) {
3005 return Ok(self.target.values.push(ValueNode::new(
3006 Value::Enum { value_type, value },
3007 Some(Span::new(self.file(), start, end)),
3008 )));
3009 }
3010 }
3011 if let Some((value_type, value)) = self.resolve_enum_member_mixed("Team", phrase) {
3012 return Ok(self.target.values.push(ValueNode::new(
3013 Value::Enum { value_type, value },
3014 Some(Span::new(self.file(), start, end)),
3015 )));
3016 }
3017 if phrase == "Visible To and String" {
3018 return Ok(self.target.values.push(ValueNode::new(
3019 Value::Enum {
3020 value_type: "HudReeval".to_string(),
3021 value: "VISIBILITY_AND_STRING".to_string(),
3022 },
3023 Some(Span::new(self.file(), start, end)),
3024 )));
3025 }
3026 if self
3027 .call_stack
3028 .last()
3029 .is_some_and(|call| call == "createHudText")
3030 {
3031 if let Some((value_type, value)) = self.resolve_enum_member_mixed("HudPosition", phrase)
3032 {
3033 return Ok(self.target.values.push(ValueNode::new(
3034 Value::Enum { value_type, value },
3035 Some(Span::new(self.file(), start, end)),
3036 )));
3037 }
3038 }
3039 if (self.expected_domain.is_none()
3040 && matches!(
3041 phrase,
3042 "Up" | "上" | "Down" | "下" | "Left" | "左" | "Right" | "右"
3043 )
3044 && (self
3045 .call_stack
3046 .last()
3047 .is_some_and(|call| matches!(call.as_str(), "multiply" | "add"))
3048 || self.call_stack.is_empty()
3049 || self
3050 .call_stack
3051 .iter()
3052 .any(|call| call == "startAcceleration")
3053 || self.call_stack.iter().any(|call| {
3054 call == "raycastHitPosition"
3055 || call == "Direction Towards"
3056 || call == "directionTowards"
3057 })))
3058 || (matches!(self.expected_domain, Some("Position")) && matches!(phrase, "Up" | "上"))
3059 {
3060 let value = match phrase {
3061 "Left" | "左" => "LEFT",
3062 "Right" | "右" => "RIGHT",
3063 "Down" | "下" => "DOWN",
3064 _ => "UP",
3065 };
3066 return Ok(self.target.values.push(ValueNode::new(
3067 Value::Enum {
3068 value_type: "Vector".to_string(),
3069 value: value.to_string(),
3070 },
3071 Some(Span::new(self.file(), start, end)),
3072 )));
3073 }
3074 let matches: Vec<(String, String)> = self
3079 .catalog
3080 .bare_member_matches(&self.locale, phrase)
3081 .into_iter()
3082 .filter(|(domain, _)| domain != "EventTeam" && domain != "EventPlayer")
3083 .collect();
3084 if matches.len() == 1 {
3085 return Ok(self.target.values.push(ValueNode::new(
3086 Value::Enum {
3087 value_type: matches[0].0.clone(),
3088 value: matches[0].1.clone(),
3089 },
3090 Some(Span::new(self.file(), start, end)),
3091 )));
3092 }
3093 if matches.len() > 1 {
3094 if let Some(expected) = self.expected_domain {
3099 let pinned: Vec<&(String, String)> = matches
3100 .iter()
3101 .filter(|(domain, _)| domain == expected)
3102 .collect();
3103 if pinned.len() == 1 {
3104 return Ok(self.target.values.push(ValueNode::new(
3105 Value::Enum {
3106 value_type: pinned[0].0.clone(),
3107 value: pinned[0].1.clone(),
3108 },
3109 Some(Span::new(self.file(), start, end)),
3110 )));
3111 }
3112 }
3113 return Err(WorkshopError::Unsupported {
3114 message: format!("ambiguous enum member '{phrase}' (multiple domains match)"),
3115 span: Some(Span::new(self.file(), start, end)),
3116 });
3117 }
3118 if let Some(entry) = self.resolve_entry(Kind::Value, phrase) {
3120 if entry.id == "null" {
3121 return Ok(self.target.values.push(ValueNode::new(
3122 Value::Null,
3123 Some(Span::new(self.file(), start, end)),
3124 )));
3125 }
3126 return Ok(self.target.values.push(ValueNode::new(
3127 Value::Call {
3128 name: entry.id.clone(),
3129 args: Vec::new(),
3130 },
3131 Some(Span::new(self.file(), start, end)),
3132 )));
3133 }
3134 Ok(self.target.values.push(ValueNode::new(
3135 Value::Call {
3136 name: phrase.to_string(),
3137 args: Vec::new(),
3138 },
3139 Some(Span::new(self.file(), start, end)),
3140 )))
3141 }
3142
3143 fn value_args(&mut self, call_id: &str) -> Result<Vec<wir::ValueId>> {
3144 let mut args = Vec::new();
3145 if let Some(Token {
3146 kind: TokenKind::RParen,
3147 ..
3148 }) = self.peek()
3149 {
3150 return Ok(args);
3151 }
3152 self.call_stack.push(call_id.to_string());
3153 let mut arg_index = 0usize;
3154 let mut raw_modify_operator = false;
3155 loop {
3156 let canonical_arg_index = if matches!(
3157 call_id,
3158 "setPlayerVariableAtIndex" | "modifyPlayerVariableAtIndex"
3159 ) && arg_index >= 2
3160 && args.len() == arg_index - 1
3161 {
3162 arg_index - 1
3163 } else {
3164 arg_index
3165 };
3166 if call_id == "string" && arg_index == 0 {
3170 args.push(self.localized_string_argument()?);
3171 } else if matches!(
3172 call_id,
3173 "setGlobalVariableAtIndex" | "modifyGlobalVariableAtIndex"
3174 ) && arg_index == 0
3175 {
3176 let (name, start, end) = self.phrase()?;
3177 let variable = self.global_by_name(&name)?;
3178 args.push(self.target.values.push(ValueNode::new(
3179 Value::GlobalVariable(variable),
3180 Some(Span::new(self.file(), start, end)),
3181 )));
3182 } else if matches!(
3183 call_id,
3184 "setPlayerVariableAtIndex" | "modifyPlayerVariableAtIndex"
3185 ) && arg_index == 1
3186 {
3187 let saved = self.pos;
3188 let (name, start, end) = self.phrase()?;
3189 if let Some(variable) = self.players.get(&name).copied() {
3190 let player = args[0];
3191 args[0] = self.target.values.push(ValueNode::new(
3192 Value::PlayerVariable { player, variable },
3193 Some(Span::new(self.file(), start, end)),
3194 ));
3195 } else {
3196 self.pos = saved;
3197 let saved_domain = self.expected_domain;
3198 self.expected_domain =
3199 self.context.expected_domain(call_id, canonical_arg_index);
3200 let arg = self.value();
3201 self.expected_domain = saved_domain;
3202 args.push(self.normalize_contextual_argument(
3203 call_id,
3204 canonical_arg_index,
3205 arg?,
3206 ));
3207 }
3208 } else if matches!(
3209 call_id,
3210 "modifyGlobalVariableAtIndex" | "modifyPlayerVariableAtIndex"
3211 ) && {
3212 let operator_position = arg_index == 2 || arg_index == 3 && raw_modify_operator;
3213 let saved = self.pos;
3214 let is_operator = operator_position && self.modify_op().is_ok();
3215 self.pos = saved;
3216 is_operator
3217 } {
3218 let operator = self.modify_op()?;
3219 let name = operator.catalog_id();
3220 args.push(self.target.values.push(ValueNode::new(
3221 Value::Call {
3222 name: name.to_string(),
3223 args: Vec::new(),
3224 },
3225 None,
3226 )));
3227 } else {
3228 if matches!(
3229 call_id,
3230 "modifyGlobalVariableAtIndex" | "modifyPlayerVariableAtIndex"
3231 ) && arg_index == 2
3232 {
3233 let saved = self.pos;
3234 raw_modify_operator = self.modify_op().is_err();
3235 self.pos = saved;
3236 }
3237 let saved = self.expected_domain;
3241 self.expected_domain = self
3242 .context
3243 .expected_domain(call_id, canonical_arg_index)
3244 .or_else(|| {
3245 (matches!(call_id, "array" | "randomValueInArray"))
3246 .then_some(saved)
3247 .flatten()
3248 });
3249 let arg = self.value();
3250 self.expected_domain = saved;
3251 let arg = self.normalize_contextual_argument(call_id, canonical_arg_index, arg?);
3252 args.push(self.normalize_modify_call_value(
3253 call_id,
3254 canonical_arg_index,
3255 &args,
3256 arg,
3257 ));
3258 }
3259 arg_index += 1;
3260 match self.peek() {
3261 Some(Token {
3262 kind: TokenKind::Comma,
3263 ..
3264 }) => {
3265 self.pos += 1;
3266 }
3267 Some(Token {
3268 kind: TokenKind::Colon,
3269 ..
3270 }) => {
3271 self.pos += 1;
3272 if !matches!(
3273 self.peek(),
3274 Some(Token {
3275 kind: TokenKind::RParen,
3276 ..
3277 })
3278 ) {
3279 let _ = self.phrase()?;
3280 }
3281 match self.peek() {
3282 Some(Token {
3283 kind: TokenKind::Comma,
3284 ..
3285 }) => self.pos += 1,
3286 Some(Token {
3287 kind: TokenKind::RParen,
3288 ..
3289 }) => break,
3290 Some(token) => return Err(self.malformed("expected ',' or ')'", &token)),
3291 None => {
3292 return Err(self.malformed("unexpected end of value call", self.eof()));
3293 }
3294 }
3295 }
3296 _ => break,
3297 }
3298 }
3299 self.call_stack.pop();
3300 Ok(args)
3301 }
3302
3303 fn localized_string_argument(&mut self) -> Result<wir::ValueId> {
3304 let Some(token) = self.peek() else {
3305 return Err(self.malformed("expected a localized string", self.eof()));
3306 };
3307 if matches!(token.kind, TokenKind::String(_)) {
3308 let span = Some(Span::new(self.file(), token.start, token.end));
3309 let content = self.expect_string("expected a localized string")?;
3310 return self.localized_string_literal(content, span);
3311 }
3312
3313 let saved = self.pos;
3318 let (phrase, _, _) = self.phrase()?;
3319 let expression_like = self.resolve_entry(Kind::Value, &phrase).is_some()
3320 || matches!(
3321 self.canonical_keyword(&phrase).as_str(),
3322 "True" | "False" | "真" | "假"
3323 )
3324 || self
3325 .peek()
3326 .is_some_and(|token| matches!(&token.kind, TokenKind::LParen | TokenKind::Dot));
3327 self.pos = saved;
3328 if expression_like || !matches!(token.kind, TokenKind::Word(_)) {
3329 return self.value();
3330 }
3331
3332 let (text, start, end) = self.phrase()?;
3333 self.localized_string_literal(text, Some(Span::new(self.file(), start, end)))
3334 }
3335
3336 fn localized_string_literal(
3337 &mut self,
3338 text: String,
3339 span: Option<Span>,
3340 ) -> Result<wir::ValueId> {
3341 let Some(entry) = self.catalog.resolve_localized_string(&self.locale, &text) else {
3342 return Err(WorkshopError::Unknown {
3343 kind: "localized string",
3344 spelling: text,
3345 locale: self.locale.clone(),
3346 span,
3347 });
3348 };
3349 Ok(self.target.values.push(ValueNode::new(
3350 Value::LocalizedString(entry.id.clone()),
3351 span,
3352 )))
3353 }
3354
3355 fn opaque_value_args(&mut self) -> Result<Vec<wir::ValueId>> {
3356 let mut args = Vec::new();
3357 if matches!(self.peek().map(|token| token.kind), Some(TokenKind::RParen)) {
3358 return Ok(args);
3359 }
3360 loop {
3361 args.push(self.value()?);
3362 match self.peek() {
3363 Some(Token {
3364 kind: TokenKind::Colon,
3365 ..
3366 }) => {
3367 self.pos += 1;
3368 if !matches!(
3369 self.peek(),
3370 Some(Token {
3371 kind: TokenKind::RParen,
3372 ..
3373 })
3374 ) {
3375 let _ = self.phrase()?;
3376 }
3377 match self.peek() {
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 => {
3388 return Err(self.malformed("unexpected end of value call", self.eof()));
3389 }
3390 }
3391 }
3392 Some(Token {
3393 kind: TokenKind::Comma,
3394 ..
3395 }) => self.pos += 1,
3396 Some(Token {
3397 kind: TokenKind::RParen,
3398 ..
3399 }) => break,
3400 Some(token) => return Err(self.malformed("expected ',' or ')'", &token)),
3401 None => return Err(self.malformed("unexpected end of value call", self.eof())),
3402 }
3403 }
3404 Ok(args)
3405 }
3406
3407 fn line_has_assignment(&self) -> bool {
3408 let tokens: Vec<_> = self.tokens[self.pos..]
3409 .iter()
3410 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
3411 .collect();
3412 tokens.iter().any(|token| {
3413 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
3414 }) || tokens.windows(2).any(|window| {
3415 matches!(&window[0].kind, TokenKind::Word(_))
3416 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
3417 })
3418 }
3419
3420 fn push_bool(&mut self, value: bool, start: Position, end: Position) -> wir::ValueId {
3421 self.target.values.push(ValueNode::new(
3422 Value::Bool(value),
3423 Some(Span::new(self.file(), start, end)),
3424 ))
3425 }
3426
3427 fn global_by_name(&mut self, name: &str) -> Result<wir::GlobalVarId> {
3428 if let Some(id) = self.globals.get(name).copied() {
3429 return Ok(id);
3430 }
3431 let index = self.next_variable_index(false);
3432 let id = self.target.global_variables.push(wir::WorkshopVariable {
3433 name: name.to_string(),
3434 index,
3435 span: None,
3436 name_span: None,
3437 });
3438 self.globals.insert(name.to_string(), id);
3439 Ok(id)
3440 }
3441
3442 fn player_by_name(&mut self, name: &str) -> Result<wir::PlayerVarId> {
3443 if let Some(id) = self.players.get(name).copied() {
3444 return Ok(id);
3445 }
3446 let index = self.next_variable_index(true);
3447 let id = self.target.player_variables.push(wir::WorkshopVariable {
3448 name: name.to_string(),
3449 index,
3450 span: None,
3451 name_span: None,
3452 });
3453 self.players.insert(name.to_string(), id);
3454 Ok(id)
3455 }
3456
3457 fn next_variable_index(&self, player: bool) -> u32 {
3458 let variables = if player {
3459 &self.target.player_variables
3460 } else {
3461 &self.target.global_variables
3462 };
3463 variables
3464 .iter()
3465 .map(|variable| variable.index)
3466 .max()
3467 .map_or(0, |index| index.saturating_add(1))
3468 }
3469
3470 fn subroutine_by_name(&self, name: &str) -> Result<wir::SubroutineId> {
3471 self.subroutines
3472 .get(name)
3473 .copied()
3474 .ok_or_else(|| WorkshopError::Unknown {
3475 kind: "subroutine",
3476 spelling: name.to_string(),
3477 locale: self.locale.clone(),
3478 span: None,
3479 })
3480 }
3481
3482 fn phrase(&mut self) -> Result<(String, Position, Position)> {
3485 let mut words = Vec::new();
3486 let (start, mut end) = match self.peek() {
3487 Some(Token {
3488 kind: TokenKind::Word(word),
3489 start,
3490 end,
3491 }) => {
3492 words.push(word.clone());
3493 (start, end)
3494 }
3495 Some(Token {
3496 kind: TokenKind::Number { text, .. },
3497 start,
3498 end,
3499 }) => {
3500 words.push(text.clone());
3501 (start, end)
3502 }
3503 Some(token) => return Err(self.malformed("expected an identifier", &token)),
3504 None => return Err(self.malformed("expected an identifier", self.eof())),
3505 };
3506 self.pos += 1;
3507 while let Some(token) = self.peek() {
3508 match token {
3509 Token {
3510 kind: TokenKind::Word(word),
3511 end: word_end,
3512 ..
3513 } => {
3514 if matches!(
3515 self.peek_at(1).map(|token| token.kind),
3516 Some(TokenKind::Op(equal)) if equal == "="
3517 ) {
3518 break;
3519 }
3520 words.push(word.clone());
3521 end = word_end;
3522 self.pos += 1;
3523 }
3524 Token {
3528 kind: TokenKind::Number { text, .. },
3529 end: number_end,
3530 ..
3531 } => {
3532 words.push(text.clone());
3533 end = number_end;
3534 self.pos += 1;
3535 }
3536 _ => break,
3537 }
3538 }
3539 Ok((words.join(" "), start, end))
3540 }
3541
3542 fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
3545 let mut words = Vec::new();
3546 let (start, mut end, line) = match self.peek() {
3547 Some(Token {
3548 kind: TokenKind::Word(word),
3549 start,
3550 end,
3551 }) => {
3552 words.push(word.clone());
3553 (start, end, start.line)
3554 }
3555 Some(Token {
3556 kind: TokenKind::Number { text, .. },
3557 start,
3558 end,
3559 }) => {
3560 words.push(text.clone());
3561 (start, end, start.line)
3562 }
3563 Some(token) => return Err(self.malformed("expected an identifier", &token)),
3564 None => return Err(self.malformed("expected an identifier", self.eof())),
3565 };
3566 self.pos += 1;
3567 while let Some(token) = self.peek() {
3568 let (word, word_start, word_end) = match token {
3569 Token {
3570 kind: TokenKind::Word(word),
3571 start,
3572 end,
3573 } => (word, start, end),
3574 Token {
3575 kind: TokenKind::Number { text, .. },
3576 start,
3577 end,
3578 } => (text, start, end),
3579 Token {
3580 kind: TokenKind::Dot,
3581 start,
3582 end,
3583 } => (".".to_string(), start, end),
3584 Token {
3585 kind: TokenKind::Op(op),
3586 start,
3587 end,
3588 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
3589 _ => break,
3590 };
3591 if word_start.line != line {
3592 break;
3593 }
3594 words.push(word);
3595 end = word_end;
3596 self.pos += 1;
3597 }
3598 Ok((
3599 words
3600 .join(" ")
3601 .replace(" .", ".")
3602 .replace(". ", ".")
3603 .replace(" : ", ":")
3604 .replace(" %", "%"),
3605 start,
3606 end,
3607 ))
3608 }
3609
3610 fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
3611 let (mut phrase, start, mut end) = self.phrase_on_line()?;
3612 if matches!(
3613 self.peek(),
3614 Some(Token {
3615 kind: TokenKind::Colon,
3616 ..
3617 })
3618 ) {
3619 let colon_pos = self.pos;
3620 self.next();
3621 let (rest, _, rest_end) = self.phrase_on_line()?;
3622 if matches!(
3623 self.peek(),
3624 Some(Token {
3625 kind: TokenKind::LBrace,
3626 ..
3627 })
3628 ) {
3629 phrase.push(':');
3630 phrase.push(' ');
3631 phrase.push_str(&rest);
3632 end = rest_end;
3633 } else {
3634 self.pos = colon_pos;
3635 }
3636 }
3637 Ok((phrase, start, end))
3638 }
3639
3640 fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
3641 let first = self
3642 .peek()
3643 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
3644 let start = first.start;
3645 let line = first.start.line;
3646 let mut end = first.end;
3647 let mut parts = Vec::new();
3648 while let Some(token) = self.peek() {
3649 if token.start.line != line
3650 || matches!(token.kind, TokenKind::RParen | TokenKind::Comma)
3651 {
3652 break;
3653 }
3654 self.pos += 1;
3655 end = token.end;
3656 parts.push(raw_token_text(&token.kind));
3657 }
3658 if parts.is_empty() {
3659 return Err(self.malformed("expected an enum member", &first));
3660 }
3661 Ok((
3662 parts
3663 .join(" ")
3664 .replace(" : ", ":")
3665 .replace(" .", ".")
3666 .replace(". ", "."),
3667 start,
3668 end,
3669 ))
3670 }
3671
3672 fn line_text(&mut self) -> Result<String> {
3675 let mut parts = Vec::new();
3676 loop {
3677 match self.peek() {
3678 Some(Token {
3679 kind: TokenKind::Semi,
3680 ..
3681 }) => {
3682 self.pos += 1;
3683 break;
3684 }
3685 Some(Token {
3686 kind: TokenKind::Word(word),
3687 ..
3688 }) => {
3689 parts.push(word.clone());
3690 self.pos += 1;
3691 }
3692 Some(Token {
3693 kind: TokenKind::Op(op),
3694 ..
3695 }) if op == "-" => {
3696 parts.push("-".to_string());
3697 self.pos += 1;
3698 }
3699 Some(Token {
3700 kind: TokenKind::Number { value, .. },
3701 ..
3702 }) => {
3703 parts.push(value.to_string());
3704 self.pos += 1;
3705 }
3706 Some(Token {
3707 kind: TokenKind::Dot,
3708 ..
3709 }) => {
3710 parts.push(".".to_string());
3711 self.pos += 1;
3712 }
3713 Some(Token {
3714 kind: TokenKind::Colon,
3715 ..
3716 }) => {
3717 parts.push(":".to_string());
3718 self.pos += 1;
3719 }
3720 Some(token) => return Err(self.malformed("expected a text line", &token)),
3721 None => return Err(self.malformed("unexpected end of input in line", self.eof())),
3722 }
3723 }
3724 Ok(parts
3725 .join(" ")
3726 .replace(" .", ".")
3727 .replace(". ", ".")
3728 .replace(" : ", ":"))
3729 }
3730
3731 fn consume_phrase(&mut self, expected: &str) -> Result<()> {
3733 let (phrase, _, _) = self.phrase()?;
3734 if phrase != expected {
3735 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
3736 }
3737 Ok(())
3738 }
3739
3740 fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
3741 match self.next() {
3742 Some(Token {
3743 kind: TokenKind::Word(word),
3744 start,
3745 ..
3746 }) if self.canonical_keyword(&word) == expected => Ok(start),
3747 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
3748 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
3749 }
3750 }
3751
3752 fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
3753 match self.next() {
3754 Some(token) if token.kind == kind => Ok(()),
3755 Some(token) => Err(self.malformed(message, &token)),
3756 None => Err(self.malformed(message, self.eof())),
3757 }
3758 }
3759
3760 fn expect_string(&mut self, message: &str) -> Result<String> {
3761 match self.next() {
3762 Some(Token {
3763 kind: TokenKind::String(content),
3764 ..
3765 }) => Ok(content),
3766 Some(token) => Err(self.malformed(message, &token)),
3767 None => Err(self.malformed(message, self.eof())),
3768 }
3769 }
3770
3771 fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
3772 WorkshopError::Malformed {
3773 message: message.to_string(),
3774 span: Some(Span::new(self.file(), token.start, token.end)),
3775 }
3776 }
3777
3778 fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
3779 WorkshopError::Unknown {
3780 kind,
3781 spelling: spelling.to_string(),
3782 locale: self.locale.clone(),
3783 span: None,
3784 }
3785 }
3786
3787 fn peek(&self) -> Option<Token> {
3788 self.tokens.get(self.pos).cloned()
3789 }
3790
3791 fn peek_at(&self, offset: usize) -> Option<Token> {
3792 self.tokens.get(self.pos + offset).cloned()
3793 }
3794
3795 fn next(&mut self) -> Option<Token> {
3796 let token = self.tokens.get(self.pos).cloned();
3797 if token.is_some() {
3798 self.pos += 1;
3799 }
3800 token
3801 }
3802
3803 fn previous(&self) -> &Token {
3804 self.tokens
3805 .get(self.pos.saturating_sub(1))
3806 .unwrap_or_else(|| self.tokens.last().unwrap())
3807 }
3808
3809 fn previous_span(&self) -> (Position, Position) {
3810 let token = self.previous();
3811 (token.start, token.end)
3812 }
3813
3814 fn span_here(&self) -> (Position, Position) {
3815 let token = self
3816 .peek()
3817 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
3818 (token.start, token.end)
3819 }
3820
3821 fn eof(&self) -> &Token {
3822 self.tokens.last().unwrap()
3823 }
3824
3825 fn file(&self) -> crate::ids::Id<SourceFile> {
3826 crate::ids::Id::from_index(0)
3827 }
3828}
3829
3830fn is_comparison(op: &str) -> bool {
3831 matches!(op, "==" | "!=" | "<" | "<=" | ">" | ">=")
3832}
3833
3834fn canonical_keyword(keyword: &str) -> &str {
3835 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
3836 KEYWORDS
3837 .get_or_init(|| {
3838 serde_json::from_str(include_str!("structural_keywords.json"))
3839 .expect("structural keyword data is valid JSON")
3840 })
3841 .get(keyword)
3842 .map(String::as_str)
3843 .unwrap_or(keyword)
3844}
3845
3846fn raw_token_text(kind: &TokenKind) -> String {
3847 match kind {
3848 TokenKind::Word(value) => value.clone(),
3849 TokenKind::Number { text, .. } => text.clone(),
3850 TokenKind::String(value) => format!("\"{}\"", value.replace('"', "\\\"")),
3851 TokenKind::Op(value) => value.clone(),
3852 TokenKind::LParen => "(".to_string(),
3853 TokenKind::RParen => ")".to_string(),
3854 TokenKind::Comma => ",".to_string(),
3855 TokenKind::Semi => ";".to_string(),
3856 TokenKind::LBrace => "{".to_string(),
3857 TokenKind::RBrace => "}".to_string(),
3858 TokenKind::Colon => ":".to_string(),
3859 TokenKind::Dot => ".".to_string(),
3860 TokenKind::LBracket => "[".to_string(),
3861 TokenKind::RBracket => "]".to_string(),
3862 TokenKind::Eof => String::new(),
3863 }
3864}