1pub(crate) use std::collections::HashMap;
10use std::sync::OnceLock;
11
12pub(crate) use crate::core::signatures::ExpectedDomain;
13pub(crate) use crate::core::source::{Position, SourceFile, Span};
14use crate::program::Program;
15pub(crate) use crate::settings::table::KeyKind;
16pub(crate) use crate::settings::{PathPart, table};
17pub(crate) use crate::settings::{Settings, SettingsListElement, SettingsNode};
18pub(crate) use crate::wir::{
19 self, Action, Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind, Value, ValueNode,
20};
21
22pub(crate) use super::lexer::{Token, TokenKind, tokenize};
23pub(crate) use crate::catalog::{Catalog, Kind, Locale};
24pub(crate) use crate::core::error::{Result, WorkshopError};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub(crate) enum Stop {
29 SectionClosed,
31 End,
33 ElseIf,
35 Else,
37}
38
39pub(crate) enum AssignmentOperator {
40 Set,
41 Modify(ModifyOp),
42}
43
44pub fn parse(input: &str, catalog: &Catalog, locale: &Locale) -> Result<Program> {
50 parse_with_context(input, catalog, locale, catalog)
51}
52
53#[cfg(test)]
54pub(crate) fn parse_wir(input: &str, catalog: &Catalog, locale: &Locale) -> Result<wir::Program> {
55 parse_wir_with_context(input, catalog, locale, catalog)
56}
57
58pub fn parse_with_context(
69 input: &str,
70 catalog: &Catalog,
71 locale: &Locale,
72 context: &dyn ExpectedDomain,
73) -> Result<Program> {
74 parse_wir_with_context(input, catalog, locale, context).and_then(Program::from_wir)
75}
76
77pub(crate) fn parse_wir_with_context(
78 input: &str,
79 catalog: &Catalog,
80 locale: &Locale,
81 context: &dyn ExpectedDomain,
82) -> Result<wir::Program> {
83 let tokens = tokenize(input).map_err(|error| {
84 WorkshopError::malformed(error.message, Some(synthetic_span(error.position)))
85 })?;
86 ParseContext {
87 tokens,
88 pos: 0,
89 source: input,
90 catalog,
91 locale: locale.clone(),
92 context,
93 expected_domain: None,
94 condition_pending: false,
95 in_condition: false,
96 call_stack: Vec::new(),
97 target: wir::Program::default(),
98 globals: HashMap::new(),
99 players: HashMap::new(),
100 subroutines: HashMap::new(),
101 next_global_index: 0,
102 next_player_index: 0,
103 }
104 .program()
105}
106
107pub(crate) fn synthetic_span(position: Position) -> Span {
109 Span::new(crate::core::ids::Id::from_index(0), position, position)
110}
111
112pub(crate) struct ParseContext<'a> {
113 pub(crate) tokens: Vec<Token>,
114 pub(crate) pos: usize,
115 pub(crate) source: &'a str,
116 pub(crate) catalog: &'a Catalog,
117 pub(crate) locale: Locale,
118 pub(crate) context: &'a dyn ExpectedDomain,
121 pub(crate) expected_domain: Option<&'a str>,
124 pub(crate) condition_pending: bool,
128 pub(crate) in_condition: bool,
131 pub(crate) call_stack: Vec<String>,
132 pub(crate) target: wir::Program,
133 pub(crate) globals: HashMap<String, wir::GlobalVarId>,
134 pub(crate) players: HashMap<String, wir::PlayerVarId>,
135 pub(crate) subroutines: HashMap<String, wir::SubroutineId>,
136 pub(crate) next_global_index: u32,
137 pub(crate) next_player_index: u32,
138}
139
140impl<'a> ParseContext<'a> {
141 pub(crate) fn with_domain<T>(
144 &mut self,
145 domain: Option<&'a str>,
146 f: impl FnOnce(&mut Self) -> Result<T>,
147 ) -> Result<T> {
148 let saved = self.expected_domain;
149 self.expected_domain = domain;
150 let result = f(self);
151 self.expected_domain = saved;
152 result
153 }
154
155 pub(crate) fn value_in_domain(&mut self, domain: Option<&'a str>) -> Result<wir::ValueId> {
158 self.with_domain(domain, |p| p.value())
159 }
160
161 pub(crate) fn resolve_entry(
162 &self,
163 kind: Kind,
164 spelling: &str,
165 ) -> Option<&'a crate::catalog::CatalogEntry> {
166 self.catalog
167 .resolve(kind, &self.locale, spelling)
168 .or_else(|| {
169 if self.locale != *self.catalog.primary_locale() {
170 self.catalog
171 .resolve(kind, self.catalog.primary_locale(), spelling)
172 } else {
173 None
174 }
175 })
176 }
177
178 pub(crate) fn canonical_keyword(&self, spelling: &str) -> String {
179 self.catalog
180 .resolve(Kind::Structural, &self.locale, spelling)
181 .or_else(|| {
182 (self.locale != *self.catalog.primary_locale()).then(|| {
183 self.catalog
184 .resolve(Kind::Structural, self.catalog.primary_locale(), spelling)
185 })?
186 })
187 .map(|entry| entry.id.clone())
188 .unwrap_or_else(|| canonical_keyword(spelling).to_string())
189 }
190
191 pub(crate) fn line_has_assignment(&self) -> bool {
192 let tokens: Vec<_> = self.tokens[self.pos..]
193 .iter()
194 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
195 .collect();
196 tokens.iter().any(|token| {
197 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
198 }) || tokens.windows(2).any(|window| {
199 matches!(&window[0].kind, TokenKind::Word(_))
200 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
201 })
202 }
203
204 pub(crate) fn phrase(&mut self) -> Result<(String, Position, Position)> {
207 let mut words = Vec::new();
208 let (start, mut end) = match self.peek() {
209 Some(Token {
210 kind: TokenKind::Word(word),
211 start,
212 end,
213 }) => {
214 words.push(word.clone());
215 (start, end)
216 }
217 Some(Token {
218 kind: TokenKind::Number { text, .. },
219 start,
220 end,
221 }) => {
222 words.push(text.clone());
223 (start, end)
224 }
225 Some(token) => return Err(self.malformed("expected an identifier", &token)),
226 None => return Err(self.malformed("expected an identifier", self.eof())),
227 };
228 self.pos += 1;
229 while let Some(token) = self.peek() {
230 match token {
231 Token {
232 kind: TokenKind::Word(word),
233 end: word_end,
234 ..
235 } => {
236 if matches!(
237 self.peek_at(1).map(|token| token.kind),
238 Some(TokenKind::Op(equal)) if equal == "="
239 ) {
240 break;
241 }
242 words.push(word.clone());
243 end = word_end;
244 self.pos += 1;
245 }
246 Token {
250 kind: TokenKind::Number { text, .. },
251 end: number_end,
252 ..
253 } => {
254 words.push(text.clone());
255 end = number_end;
256 self.pos += 1;
257 }
258 _ => break,
259 }
260 }
261 Ok((words.join(" "), start, end))
262 }
263
264 pub(crate) fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
267 self.phrase_on_line_impl(false)
268 }
269
270 pub(crate) fn settings_list_name(&mut self) -> Result<(String, Position, Position)> {
274 self.phrase_on_line_impl(true)
275 }
276
277 fn phrase_on_line_impl(&mut self, list_name: bool) -> Result<(String, Position, Position)> {
278 let mut words = Vec::new();
279 let (start, mut end, line) = match self.peek() {
280 Some(Token {
281 kind: TokenKind::Word(word),
282 start,
283 end,
284 }) => {
285 words.push(word.clone());
286 (start, end, start.line)
287 }
288 Some(Token {
289 kind: TokenKind::Number { text, .. },
290 start,
291 end,
292 }) => {
293 words.push(text.clone());
294 (start, end, start.line)
295 }
296 Some(token) => return Err(self.malformed("expected an identifier", &token)),
297 None => return Err(self.malformed("expected an identifier", self.eof())),
298 };
299 self.pos += 1;
300 while let Some(token) = self.peek() {
301 let (word, word_start, word_end) = match token {
302 Token {
303 kind: TokenKind::Word(word),
304 start,
305 end,
306 } => (word, start, end),
307 Token {
308 kind: TokenKind::Number { text, .. },
309 start,
310 end,
311 } => (text, start, end),
312 Token {
313 kind: TokenKind::Dot,
314 start,
315 end,
316 } => (".".to_string(), start, end),
317 Token {
318 kind: TokenKind::Op(op),
319 start,
320 end,
321 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
322 Token {
323 kind: TokenKind::Colon,
324 start,
325 end,
326 } if list_name => (":".to_string(), start, end),
327 _ => break,
328 };
329 if word_start.line != line {
330 break;
331 }
332 if list_name && word_start == end {
333 words
334 .last_mut()
335 .expect("phrase has a first token")
336 .push_str(&word);
337 } else {
338 words.push(word);
339 }
340 end = word_end;
341 self.pos += 1;
342 }
343 let joined = words
344 .join(" ")
345 .replace(" .", ".")
346 .replace(". ", ".")
347 .replace(" %", "%");
348 let joined = if list_name {
351 joined
352 } else {
353 joined.replace(" : ", ":")
354 };
355 Ok((joined, start, end))
356 }
357
358 pub(crate) fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
359 let (mut phrase, start, mut end) = self.phrase_on_line()?;
360 if matches!(
361 self.peek(),
362 Some(Token {
363 kind: TokenKind::Colon,
364 ..
365 })
366 ) {
367 let colon_pos = self.pos;
368 self.next();
369 let (rest, _, rest_end) = self.phrase_on_line()?;
370 if matches!(
371 self.peek(),
372 Some(Token {
373 kind: TokenKind::LBrace,
374 ..
375 })
376 ) {
377 phrase.push(':');
378 phrase.push(' ');
379 phrase.push_str(&rest);
380 end = rest_end;
381 } else {
382 self.pos = colon_pos;
383 }
384 }
385 Ok((phrase, start, end))
386 }
387
388 pub(crate) fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
389 let first = self
390 .peek()
391 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
392 let start = first.start;
393 let line = first.start.line;
394 let mut end = first.end;
395 let mut parts = Vec::new();
396 while let Some(token) = self.peek() {
397 if token.start.line != line
398 || matches!(token.kind, TokenKind::RParen | TokenKind::Comma)
399 {
400 break;
401 }
402 self.pos += 1;
403 end = token.end;
404 parts.push(raw_token_text(&token.kind));
405 }
406 if parts.is_empty() {
407 return Err(self.malformed("expected an enum member", &first));
408 }
409 Ok((
410 parts
411 .join(" ")
412 .replace(" : ", ":")
413 .replace(" .", ".")
414 .replace(". ", "."),
415 start,
416 end,
417 ))
418 }
419
420 pub(crate) fn line_text(&mut self) -> Result<(String, Option<Span>)> {
424 let mut parts = Vec::new();
425 let mut start = None;
426 let mut end = None;
427 loop {
428 match self.peek() {
429 Some(Token {
430 kind: TokenKind::Semi,
431 ..
432 }) => {
433 self.pos += 1;
434 break;
435 }
436 Some(token) => {
437 start.get_or_insert(token.start);
438 end = Some(token.end);
439 match &token.kind {
440 TokenKind::Word(word) => parts.push(word.clone()),
441 TokenKind::Op(op) if op == "-" => parts.push("-".to_string()),
442 TokenKind::Number { value, .. } => parts.push(value.to_string()),
443 TokenKind::Dot => parts.push(".".to_string()),
444 TokenKind::Colon => parts.push(":".to_string()),
445 _ => return Err(self.malformed("expected a text line", &token)),
446 }
447 self.pos += 1;
448 }
449 None => {
450 return Err(self.malformed("unexpected end of input in line", self.eof()));
451 }
452 }
453 }
454 Ok((
455 parts
456 .join(" ")
457 .replace(" .", ".")
458 .replace(". ", ".")
459 .replace(" : ", ":"),
460 start
461 .zip(end)
462 .map(|(start, end)| Span::new(self.file(), start, end)),
463 ))
464 }
465
466 pub(crate) fn consume_phrase(&mut self, expected: &str) -> Result<()> {
468 let (phrase, _, _) = self.phrase()?;
469 if phrase != expected {
470 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
471 }
472 Ok(())
473 }
474
475 fn structural_spellings(&self, id: &str) -> Vec<String> {
479 let mut spellings = Vec::new();
480 if let Some(entry) = self.catalog.entry(Kind::Structural, id) {
481 spellings.extend(entry.spellings(&self.locale).iter().cloned());
482 let primary = self.catalog.primary_locale();
483 if self.locale != *primary {
484 spellings.extend(entry.spellings(primary).iter().cloned());
485 }
486 }
487 for (spelling, english) in structural_keyword_map() {
488 if self
489 .catalog
490 .resolve(Kind::Structural, self.catalog.primary_locale(), english)
491 .is_some_and(|entry| entry.id == id)
492 {
493 spellings.push(spelling.clone());
494 }
495 }
496 spellings
497 }
498
499 fn match_spelling_tokens(&self, spelling: &str) -> Option<usize> {
502 let mut offset = 0;
503 for piece in spelling.split(' ') {
504 let (word, dotted) = match piece.strip_suffix('.') {
505 Some(word) => (word, true),
506 None => (piece, false),
507 };
508 if !word.is_empty() {
509 match self.peek_at(offset).map(|token| token.kind) {
510 Some(TokenKind::Word(ref candidate)) if table::names_eq(candidate, word) => {}
511 Some(TokenKind::Number { ref text, .. }) if text == word => {}
512 _ => return None,
513 }
514 offset += 1;
515 }
516 if dotted {
517 match self.peek_at(offset).map(|token| token.kind) {
518 Some(TokenKind::Dot) => offset += 1,
519 _ => return None,
520 }
521 }
522 }
523 (offset > 0).then_some(offset)
524 }
525
526 pub(crate) fn keyword_prefix(&mut self, ids: &[&str]) -> Option<String> {
531 let mut best: Option<(String, usize)> = None;
532 for id in ids {
533 for spelling in self.structural_spellings(id) {
534 if let Some(len) = self.match_spelling_tokens(&spelling) {
535 if best.as_ref().is_none_or(|(_, length)| len > *length) {
536 best = Some(((*id).to_string(), len));
537 }
538 }
539 }
540 }
541 if let Some((id, len)) = best {
542 self.pos += len;
543 Some(id)
544 } else {
545 None
546 }
547 }
548
549 pub(crate) fn consume_keyword(&mut self, id: &str) -> Result<()> {
551 if self.keyword_prefix(&[id]).is_some() {
552 return Ok(());
553 }
554 match self.peek() {
555 Some(token) => Err(self.malformed(&format!("expected '{id}'"), &token)),
556 None => Err(self.malformed(&format!("expected '{id}'"), self.eof())),
557 }
558 }
559
560 pub(crate) fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
561 let start = self.peek().map(|token| token.start);
562 if self.keyword_prefix(&[expected]).is_some() {
563 return Ok(start.unwrap_or_else(|| self.previous().start));
564 }
565 match self.next() {
566 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
567 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
568 }
569 }
570
571 pub(crate) fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
572 match self.next() {
573 Some(token) if token.kind == kind => Ok(()),
574 Some(token) => Err(self.malformed(message, &token)),
575 None => Err(self.malformed(message, self.eof())),
576 }
577 }
578
579 pub(crate) fn expect_semi(&mut self, message: &str) -> Result<()> {
584 match self.peek() {
585 Some(Token {
586 kind: TokenKind::Semi,
587 ..
588 }) => {
589 self.pos += 1;
590 Ok(())
591 }
592 Some(token) if token.start.line > self.previous().end.line => {
593 let end = self.previous().end;
594 Err(WorkshopError::malformed(
595 message.to_string(),
596 Some(Span::new(self.file(), end, end)),
597 ))
598 }
599 Some(token) => Err(self.malformed(message, &token)),
600 None => Err(self.malformed(message, self.eof())),
601 }
602 }
603
604 pub(crate) fn expect_string(&mut self, message: &str) -> Result<String> {
605 match self.next() {
606 Some(Token {
607 kind: TokenKind::String(content),
608 ..
609 }) => Ok(content),
610 Some(token) => Err(self.malformed(message, &token)),
611 None => Err(self.malformed(message, self.eof())),
612 }
613 }
614
615 pub(crate) fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
616 WorkshopError::malformed(
617 message.to_string(),
618 Some(Span::new(self.file(), token.start, token.end)),
619 )
620 }
621
622 pub(crate) fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
623 WorkshopError::unknown(kind, spelling.to_string(), self.locale.clone(), None)
624 }
625
626 pub(crate) fn accepted_spellings(
630 &self,
631 kind: Kind,
632 include_primary: bool,
633 ) -> impl Iterator<Item = &str> {
634 let catalog = self.catalog;
635 let primary = catalog.primary_locale();
636 let include_primary = include_primary && self.locale != *primary;
637 catalog.entries_of(kind).flat_map(move |entry| {
638 let local: &[String] = entry.spellings(&self.locale);
639 let fallback: &[String] = if include_primary {
640 entry.spellings(primary)
641 } else {
642 &[]
643 };
644 local.iter().chain(fallback).map(String::as_str)
645 })
646 }
647
648 pub(crate) fn accepted_member_spellings<'b>(
652 &'b self,
653 domain: &'b str,
654 include_primary: bool,
655 ) -> impl Iterator<Item = &'b str> {
656 let catalog = self.catalog;
657 let primary = catalog.primary_locale();
658 let include_primary = include_primary && self.locale != *primary;
659 catalog
660 .enum_domain(domain)
661 .into_iter()
662 .flat_map(move |domain| {
663 domain.members.iter().flat_map(move |member| {
664 let local: &[String] = member.spellings(&self.locale);
665 let fallback: &[String] = if include_primary {
666 member.spellings(primary)
667 } else {
668 &[]
669 };
670 local.iter().chain(fallback).map(String::as_str)
671 })
672 })
673 }
674
675 pub(crate) fn spelling_candidates(
677 &self,
678 kind: Kind,
679 spelling: &str,
680 include_primary: bool,
681 ) -> Vec<String> {
682 crate::core::suggest::nearest(
683 spelling,
684 self.accepted_spellings(kind, include_primary),
685 crate::core::suggest::CANDIDATE_LIMIT,
686 )
687 }
688
689 pub(crate) fn member_candidates(
692 &self,
693 domain: &str,
694 spelling: &str,
695 include_primary: bool,
696 ) -> Vec<String> {
697 crate::core::suggest::nearest(
698 spelling,
699 self.accepted_member_spellings(domain, include_primary),
700 crate::core::suggest::CANDIDATE_LIMIT,
701 )
702 }
703
704 pub(crate) fn peek(&self) -> Option<Token> {
705 self.tokens.get(self.pos).cloned()
706 }
707
708 pub(crate) fn peek_at(&self, offset: usize) -> Option<Token> {
709 self.tokens.get(self.pos + offset).cloned()
710 }
711
712 pub(crate) fn next(&mut self) -> Option<Token> {
713 let token = self.tokens.get(self.pos).cloned();
714 if token.is_some() {
715 self.pos += 1;
716 }
717 token
718 }
719
720 pub(crate) fn previous(&self) -> &Token {
721 self.tokens
722 .get(self.pos.saturating_sub(1))
723 .unwrap_or_else(|| self.tokens.last().unwrap())
724 }
725
726 pub(crate) fn previous_span(&self) -> (Position, Position) {
727 let token = self.previous();
728 (token.start, token.end)
729 }
730
731 pub(crate) fn span_here(&self) -> (Position, Position) {
732 let token = self
733 .peek()
734 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
735 (token.start, token.end)
736 }
737
738 pub(crate) fn eof(&self) -> &Token {
739 self.tokens.last().unwrap()
740 }
741
742 pub(crate) fn file(&self) -> crate::core::ids::Id<SourceFile> {
743 crate::core::ids::Id::from_index(0)
744 }
745}
746
747fn structural_keyword_map() -> &'static HashMap<String, String> {
748 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
749 KEYWORDS.get_or_init(|| {
750 serde_json::from_str(include_str!("structural_keywords.json"))
751 .expect("structural keyword data is valid JSON")
752 })
753}
754
755pub(crate) fn canonical_keyword(keyword: &str) -> &str {
756 structural_keyword_map()
757 .get(keyword)
758 .map(String::as_str)
759 .unwrap_or(keyword)
760}
761
762pub(crate) fn raw_token_text(kind: &TokenKind) -> String {
763 match kind {
764 TokenKind::Word(value) => value.clone(),
765 TokenKind::Number { text, .. } => text.clone(),
766 TokenKind::String(value) => format!(
767 "\"{}\"",
768 crate::output::emitter::escape_settings_string(value)
769 ),
770 TokenKind::Op(value) => value.clone(),
771 TokenKind::LParen => "(".to_string(),
772 TokenKind::RParen => ")".to_string(),
773 TokenKind::Comma => ",".to_string(),
774 TokenKind::Semi => ";".to_string(),
775 TokenKind::LBrace => "{".to_string(),
776 TokenKind::RBrace => "}".to_string(),
777 TokenKind::Colon => ":".to_string(),
778 TokenKind::Dot => ".".to_string(),
779 TokenKind::LBracket => "[".to_string(),
780 TokenKind::RBracket => "]".to_string(),
781 TokenKind::Eof => String::new(),
782 }
783}