1pub(crate) use std::collections::HashMap;
2use std::sync::OnceLock;
3
4pub(crate) use crate::core::signatures::ExpectedDomain;
5pub(crate) use crate::core::source::{Position, SourceFile, Span};
6use crate::program::Program;
7pub(crate) use crate::settings::table::KeyKind;
8pub(crate) use crate::settings::{PathPart, table};
9pub(crate) use crate::settings::{Settings, SettingsListElement, SettingsNode};
10pub(crate) use crate::wir::{
11 self, Action, Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind, Value, ValueNode,
12};
13
14pub(crate) use super::lexer::{Token, TokenKind, tokenize};
15pub(crate) use crate::catalog::{Catalog, Kind, Locale};
16pub(crate) use crate::core::error::{Result, WorkshopError};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(crate) enum Stop {
20 SectionClosed,
21 End,
22 ElseIf,
23 Else,
24}
25
26pub(crate) enum AssignmentOperator {
27 Set,
28 Modify(ModifyOp),
29}
30
31pub fn parse(input: &str, catalog: &Catalog, locale: &Locale) -> Result<Program> {
37 parse_with_context(input, catalog, locale, catalog)
38}
39
40#[cfg(test)]
41pub(crate) fn parse_wir(input: &str, catalog: &Catalog, locale: &Locale) -> Result<wir::Program> {
42 parse_wir_with_context(input, catalog, locale, catalog)
43}
44
45pub fn parse_with_context(
56 input: &str,
57 catalog: &Catalog,
58 locale: &Locale,
59 context: &dyn ExpectedDomain,
60) -> Result<Program> {
61 parse_wir_with_context(input, catalog, locale, context).and_then(Program::from_wir)
62}
63
64pub(crate) fn parse_wir_with_context(
65 input: &str,
66 catalog: &Catalog,
67 locale: &Locale,
68 context: &dyn ExpectedDomain,
69) -> Result<wir::Program> {
70 let tokens = tokenize(input).map_err(|error| {
71 WorkshopError::malformed(error.message, Some(synthetic_span(error.position)))
72 })?;
73 ParseContext {
74 tokens,
75 pos: 0,
76 source: input,
77 catalog,
78 locale: locale.clone(),
79 context,
80 expected_domain: None,
81 condition_pending: false,
82 in_condition: false,
83 call_stack: Vec::new(),
84 target: wir::Program::default(),
85 globals: HashMap::new(),
86 players: HashMap::new(),
87 subroutines: HashMap::new(),
88 next_global_index: 0,
89 next_player_index: 0,
90 structural_spellings: HashMap::new(),
91 }
92 .program()
93}
94
95pub(crate) fn synthetic_span(position: Position) -> Span {
96 Span::new(crate::core::ids::Id::from_index(0), position, position)
97}
98
99pub(crate) struct ParseContext<'a> {
100 pub(crate) tokens: Vec<Token>,
101 pub(crate) pos: usize,
102 pub(crate) source: &'a str,
103 pub(crate) catalog: &'a Catalog,
104 pub(crate) locale: Locale,
105 pub(crate) context: &'a dyn ExpectedDomain,
106 pub(crate) expected_domain: Option<&'a str>,
107 pub(crate) condition_pending: bool,
108 pub(crate) in_condition: bool,
109 pub(crate) call_stack: Vec<String>,
110 pub(crate) target: wir::Program,
111 pub(crate) globals: HashMap<String, wir::GlobalVarId>,
112 pub(crate) players: HashMap<String, wir::PlayerVarId>,
113 pub(crate) subroutines: HashMap<String, wir::SubroutineId>,
114 pub(crate) next_global_index: u32,
115 pub(crate) next_player_index: u32,
116 structural_spellings: HashMap<String, Vec<String>>,
117}
118
119impl<'a> ParseContext<'a> {
120 pub(crate) fn with_domain<T>(
121 &mut self,
122 domain: Option<&'a str>,
123 f: impl FnOnce(&mut Self) -> Result<T>,
124 ) -> Result<T> {
125 let saved = self.expected_domain;
126 self.expected_domain = domain;
127 let result = f(self);
128 self.expected_domain = saved;
129 result
130 }
131
132 pub(crate) fn value_in_domain(&mut self, domain: Option<&'a str>) -> Result<wir::ValueId> {
133 self.with_domain(domain, |p| p.value())
134 }
135
136 pub(crate) fn resolve_entry(
137 &self,
138 kind: Kind,
139 spelling: &str,
140 ) -> Option<&'a crate::catalog::CatalogEntry> {
141 self.catalog
142 .resolve(kind, &self.locale, spelling)
143 .or_else(|| {
144 if self.locale != *self.catalog.primary_locale() {
145 self.catalog
146 .resolve(kind, self.catalog.primary_locale(), spelling)
147 } else {
148 None
149 }
150 })
151 }
152
153 pub(crate) fn canonical_keyword(&self, spelling: &str) -> String {
154 self.catalog
155 .resolve(Kind::Structural, &self.locale, spelling)
156 .or_else(|| {
157 (self.locale != *self.catalog.primary_locale()).then(|| {
158 self.catalog
159 .resolve(Kind::Structural, self.catalog.primary_locale(), spelling)
160 })?
161 })
162 .map(|entry| entry.id.clone())
163 .unwrap_or_else(|| canonical_keyword(spelling).to_string())
164 }
165
166 pub(crate) fn line_has_assignment(&self) -> bool {
167 let tokens: Vec<_> = self.tokens[self.pos..]
168 .iter()
169 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
170 .collect();
171 tokens.iter().any(|token| {
172 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
173 }) || tokens.windows(2).any(|window| {
174 matches!(&window[0].kind, TokenKind::Word(_))
175 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
176 })
177 }
178
179 pub(crate) fn phrase(&mut self) -> Result<(String, Position, Position)> {
183 let mut parts: Vec<(String, Position, Position)> = Vec::new();
184 let (start, mut end) = match self.peek() {
185 Some(Token {
186 kind: TokenKind::Word(word),
187 start,
188 end,
189 }) => {
190 parts.push((word.clone(), start, end));
191 (start, end)
192 }
193 Some(Token {
194 kind: TokenKind::Number { text, .. },
195 start,
196 end,
197 }) => {
198 parts.push((text.clone(), start, end));
199 (start, end)
200 }
201 Some(token) => return Err(self.malformed("expected an identifier", &token)),
202 None => return Err(self.malformed("expected an identifier", self.eof())),
203 };
204 self.pos += 1;
205 while let Some(token) = self.peek() {
206 match token {
207 Token {
208 kind: TokenKind::Word(word),
209 start: word_start,
210 end: word_end,
211 } => {
212 if matches!(
213 self.peek_kind_at(1),
214 Some(TokenKind::Op(equal)) if equal == "="
215 ) {
216 break;
217 }
218 parts.push((word.clone(), word_start, word_end));
219 end = word_end;
220 self.pos += 1;
221 }
222 Token {
226 kind: TokenKind::Number { text, .. },
227 start: number_start,
228 end: number_end,
229 } => {
230 parts.push((text.clone(), number_start, number_end));
231 end = number_end;
232 self.pos += 1;
233 }
234 _ => break,
235 }
236 }
237 Ok((join_as_written(&parts), start, end))
238 }
239
240 pub(crate) fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
241 self.phrase_on_line_impl(false)
242 }
243
244 pub(crate) fn settings_list_name(&mut self) -> Result<(String, Position, Position)> {
248 self.phrase_on_line_impl(true)
249 }
250
251 fn phrase_on_line_impl(&mut self, list_name: bool) -> Result<(String, Position, Position)> {
252 let mut parts: Vec<(String, Position, Position)> = Vec::new();
253 let (start, mut end, line) = match self.peek() {
254 Some(Token {
255 kind: TokenKind::Word(word),
256 start,
257 end,
258 }) => {
259 parts.push((word.clone(), start, end));
260 (start, end, start.line)
261 }
262 Some(Token {
263 kind: TokenKind::Number { text, .. },
264 start,
265 end,
266 }) => {
267 parts.push((text.clone(), start, end));
268 (start, end, start.line)
269 }
270 Some(token) => return Err(self.malformed("expected an identifier", &token)),
271 None => return Err(self.malformed("expected an identifier", self.eof())),
272 };
273 self.pos += 1;
274 while let Some(token) = self.peek() {
275 let (text, word_start, word_end) = match token {
276 Token {
277 kind: TokenKind::Word(word),
278 start,
279 end,
280 } => (word.clone(), start, end),
281 Token {
282 kind: TokenKind::Number { text, .. },
283 start,
284 end,
285 } => (text.clone(), start, end),
286 Token {
287 kind: TokenKind::Dot,
288 start,
289 end,
290 } => (".".to_string(), start, end),
291 Token {
292 kind: TokenKind::Op(op),
293 start,
294 end,
295 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
296 Token {
297 kind: TokenKind::Colon,
298 start,
299 end,
300 } if list_name => (":".to_string(), start, end),
301 _ => break,
302 };
303 if word_start.line != line {
304 break;
305 }
306 parts.push((text, word_start, word_end));
307 end = word_end;
308 self.pos += 1;
309 }
310 Ok((join_as_written(&parts), start, end))
315 }
316
317 pub(crate) fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
318 let (mut phrase, start, mut end) = self.phrase_on_line()?;
319 if matches!(
320 self.peek(),
321 Some(Token {
322 kind: TokenKind::Colon,
323 ..
324 })
325 ) {
326 let colon_pos = self.pos;
327 self.next();
328 let (rest, _, rest_end) = self.phrase_on_line()?;
329 if matches!(
330 self.peek(),
331 Some(Token {
332 kind: TokenKind::LBrace,
333 ..
334 })
335 ) {
336 phrase.push(':');
337 phrase.push(' ');
338 phrase.push_str(&rest);
339 end = rest_end;
340 } else {
341 self.pos = colon_pos;
342 }
343 }
344 Ok((phrase, start, end))
345 }
346
347 pub(crate) fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
352 let first = self
353 .peek()
354 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
355 let start = first.start;
356 let line = first.start.line;
357 let mut end = first.end;
358 let mut parts: Vec<(String, Position, Position)> = Vec::new();
359 while let Some(token) = self.peek() {
360 if token.start.line != line
365 || matches!(
366 token.kind,
367 TokenKind::RParen | TokenKind::Comma | TokenKind::LParen
368 )
369 {
370 break;
371 }
372 self.pos += 1;
373 end = token.end;
374 parts.push((raw_token_text(&token.kind), token.start, token.end));
375 }
376 if parts.is_empty() {
377 return Err(self.malformed("expected an enum member", &first));
378 }
379 Ok((join_as_written(&parts), start, end))
380 }
381
382 pub(crate) fn line_text(&mut self) -> Result<(String, Option<Span>)> {
388 let mut parts: Vec<(String, Position, Position)> = Vec::new();
389 let mut start = None;
390 let mut end = None;
391 loop {
392 match self.peek() {
393 Some(Token {
394 kind: TokenKind::Semi,
395 ..
396 }) => {
397 self.pos += 1;
398 break;
399 }
400 Some(token) => {
401 start.get_or_insert(token.start);
402 end = Some(token.end);
403 match &token.kind {
404 TokenKind::Word(_)
405 | TokenKind::Number { .. }
406 | TokenKind::Dot
407 | TokenKind::Colon
408 | TokenKind::Op(_) => {}
409 _ => return Err(self.malformed("expected a text line", &token)),
410 }
411 parts.push((raw_token_text(&token.kind), token.start, token.end));
412 self.pos += 1;
413 }
414 None => {
415 return Err(self.malformed("unexpected end of input in line", self.eof()));
416 }
417 }
418 }
419 Ok((
420 join_as_written(&parts),
421 start
422 .zip(end)
423 .map(|(start, end)| Span::new(self.file(), start, end)),
424 ))
425 }
426
427 pub(crate) fn spelling_run(
436 &self,
437 pos: usize,
438 attaches_to: Option<Position>,
439 ) -> Vec<(usize, String, Position)> {
440 let mut ends = Vec::new();
441 let mut text = String::new();
442 let mut index = pos;
443 let mut prev_end = attaches_to;
444 while let Some(token) = self.tokens.get(index) {
445 let fragment = match &token.kind {
446 TokenKind::Word(word) => word.as_str(),
447 TokenKind::Number { text, .. } => text.as_str(),
448 TokenKind::Dot => ".",
449 TokenKind::Colon => ":",
450 TokenKind::Op(op) if op == "not" => "!",
451 TokenKind::Op(op) if op == "-" => "-",
455 _ => break,
456 };
457 let needs_adjacent = matches!(token.kind, TokenKind::Dot | TokenKind::Colon)
461 || matches!(&token.kind, TokenKind::Op(op) if op == "not");
462 if needs_adjacent && prev_end.is_none_or(|end| token.start != end) {
463 break;
464 }
465 if prev_end.is_some_and(|end| token.start != end) {
466 text.push(' ');
467 }
468 text.push_str(fragment);
469 prev_end = Some(token.end);
470 index += 1;
471 ends.push((text.len(), token.end));
472 }
473 ends.into_iter()
474 .enumerate()
475 .rev()
476 .map(|(i, (len, end))| (i + 1, text[..len].to_string(), end))
477 .collect()
478 }
479
480 pub(crate) fn catalog_spelling(
487 &mut self,
488 accept: impl Fn(&str) -> bool,
489 ) -> Option<(String, Position, Position)> {
490 let start = self.tokens.get(self.pos)?.start;
491 for (count, text, end) in self.spelling_run(self.pos, None) {
492 if count < 2 {
493 break;
494 }
495 if accept(&text) {
496 self.pos += count;
497 return Some((text, start, end));
498 }
499 }
500 None
501 }
502
503 pub(crate) fn extend_catalog_spelling(
509 &mut self,
510 phrase: &str,
511 phrase_end: Position,
512 accept: impl Fn(&str) -> bool,
513 ) -> Option<(String, Position)> {
514 for (count, tail, end) in self.spelling_run(self.pos, Some(phrase_end)) {
515 let joined = format!("{phrase}{tail}");
516 if accept(&joined) {
517 self.pos += count;
518 return Some((joined, end));
519 }
520 }
521 None
522 }
523
524 pub(crate) fn extend_entry_spelling(
525 &mut self,
526 phrase: &str,
527 phrase_end: Position,
528 kinds: &[Kind],
529 ) -> Option<(String, Position)> {
530 let catalog = self.catalog;
531 let locale = self.locale.clone();
532 let primary = catalog.primary_locale();
533 self.extend_catalog_spelling(phrase, phrase_end, |spelling| {
534 kinds.iter().any(|kind| {
535 catalog.resolve(*kind, &locale, spelling).is_some()
536 || (locale != *primary && catalog.resolve(*kind, primary, spelling).is_some())
537 })
538 })
539 }
540
541 pub(crate) fn consume_phrase(&mut self, expected: &str) -> Result<()> {
542 let (phrase, _, _) = self.phrase()?;
543 if phrase != expected {
544 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
545 }
546 Ok(())
547 }
548
549 fn compute_structural_spellings(&self, id: &str) -> Vec<String> {
550 let mut spellings = Vec::new();
551 if let Some(entry) = self.catalog.entry(Kind::Structural, id) {
552 spellings.extend(entry.spellings(&self.locale).iter().cloned());
553 let primary = self.catalog.primary_locale();
554 if self.locale != *primary {
555 spellings.extend(entry.spellings(primary).iter().cloned());
556 }
557 }
558 for (spelling, english) in structural_keyword_map() {
559 if self
560 .catalog
561 .resolve(Kind::Structural, self.catalog.primary_locale(), english)
562 .is_some_and(|entry| entry.id == id)
563 {
564 spellings.push(spelling.clone());
565 }
566 }
567 spellings
568 }
569
570 fn match_spelling_tokens(&self, spelling: &str) -> Option<usize> {
571 let mut offset = 0;
572 for piece in spelling.split(' ') {
573 let (word, dotted) = match piece.strip_suffix('.') {
574 Some(word) => (word, true),
575 None => (piece, false),
576 };
577 if !word.is_empty() {
578 match self.peek_kind_at(offset) {
579 Some(TokenKind::Word(candidate)) if table::names_eq(candidate, word) => {}
580 Some(TokenKind::Number { text, .. }) if text == word => {}
581 _ => return None,
582 }
583 offset += 1;
584 }
585 if dotted {
586 match self.peek_kind_at(offset) {
587 Some(TokenKind::Dot) => offset += 1,
588 _ => return None,
589 }
590 }
591 }
592 (offset > 0).then_some(offset)
593 }
594
595 pub(crate) fn keyword_prefix(&mut self, ids: &[&str]) -> Option<String> {
600 let mut best: Option<(String, usize)> = None;
601 for id in ids {
602 if !self.structural_spellings.contains_key(*id) {
603 let spellings = self.compute_structural_spellings(id);
604 self.structural_spellings
605 .insert((*id).to_string(), spellings);
606 }
607 for spelling in &self.structural_spellings[*id] {
608 if let Some(len) = self.match_spelling_tokens(spelling)
609 && best.as_ref().is_none_or(|(_, length)| len > *length)
610 {
611 best = Some(((*id).to_string(), len));
612 }
613 }
614 }
615 if let Some((id, len)) = best {
616 self.pos += len;
617 Some(id)
618 } else {
619 None
620 }
621 }
622
623 pub(crate) fn consume_keyword(&mut self, id: &str) -> Result<()> {
624 if self.keyword_prefix(&[id]).is_some() {
625 return Ok(());
626 }
627 match self.peek() {
628 Some(token) => Err(self.malformed(&format!("expected '{id}'"), &token)),
629 None => Err(self.malformed(&format!("expected '{id}'"), self.eof())),
630 }
631 }
632
633 pub(crate) fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
634 let start = self.peek().map(|token| token.start);
635 if self.keyword_prefix(&[expected]).is_some() {
636 return Ok(start.unwrap_or_else(|| self.previous().start));
637 }
638 match self.next() {
639 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
640 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
641 }
642 }
643
644 pub(crate) fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
645 match self.next() {
646 Some(token) if token.kind == kind => Ok(()),
647 Some(token) => Err(self.malformed(message, &token)),
648 None => Err(self.malformed(message, self.eof())),
649 }
650 }
651
652 pub(crate) fn expect_semi(&mut self, message: &str) -> Result<()> {
653 match self.peek() {
654 Some(Token {
655 kind: TokenKind::Semi,
656 ..
657 }) => {
658 self.pos += 1;
659 Ok(())
660 }
661 Some(token) if token.start.line > self.previous().end.line => {
662 let end = self.previous().end;
663 Err(WorkshopError::malformed(
664 message.to_string(),
665 Some(Span::new(self.file(), end, end)),
666 ))
667 }
668 Some(token) => Err(self.malformed(message, &token)),
669 None => Err(self.malformed(message, self.eof())),
670 }
671 }
672
673 pub(crate) fn expect_string(&mut self, message: &str) -> Result<String> {
674 match self.next() {
675 Some(Token {
676 kind: TokenKind::String(content),
677 ..
678 }) => Ok(content),
679 Some(token) => Err(self.malformed(message, &token)),
680 None => Err(self.malformed(message, self.eof())),
681 }
682 }
683
684 pub(crate) fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
685 WorkshopError::malformed(
686 message.to_string(),
687 Some(Span::new(self.file(), token.start, token.end)),
688 )
689 }
690
691 pub(crate) fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
692 WorkshopError::unknown(kind, spelling.to_string(), self.locale.clone(), None)
693 }
694
695 pub(crate) fn unknown_at(
696 &self,
697 kind: &'static str,
698 spelling: &str,
699 start: Position,
700 end: Position,
701 ) -> WorkshopError {
702 WorkshopError::unknown(
703 kind,
704 spelling.to_string(),
705 self.locale.clone(),
706 Some(Span::new(self.file(), start, end)),
707 )
708 }
709
710 pub(crate) fn accepted_spellings(
711 &self,
712 kind: Kind,
713 include_primary: bool,
714 ) -> impl Iterator<Item = &str> {
715 let catalog = self.catalog;
716 let primary = catalog.primary_locale();
717 let include_primary = include_primary && self.locale != *primary;
718 catalog.entries_of(kind).flat_map(move |entry| {
719 let local: &[String] = entry.spellings(&self.locale);
720 let fallback: &[String] = if include_primary {
721 entry.spellings(primary)
722 } else {
723 &[]
724 };
725 local.iter().chain(fallback).map(String::as_str)
726 })
727 }
728
729 pub(crate) fn accepted_member_spellings<'b>(
730 &'b self,
731 domain: &'b str,
732 include_primary: bool,
733 ) -> impl Iterator<Item = &'b str> {
734 let catalog = self.catalog;
735 let primary = catalog.primary_locale();
736 let include_primary = include_primary && self.locale != *primary;
737 catalog
738 .enum_domain(domain)
739 .into_iter()
740 .flat_map(move |domain| {
741 domain.members.iter().flat_map(move |member| {
742 let local: &[String] = member.spellings(&self.locale);
743 let fallback: &[String] = if include_primary {
744 member.spellings(primary)
745 } else {
746 &[]
747 };
748 local.iter().chain(fallback).map(String::as_str)
749 })
750 })
751 }
752
753 pub(crate) fn spelling_candidates(
754 &self,
755 kind: Kind,
756 spelling: &str,
757 include_primary: bool,
758 ) -> Vec<String> {
759 crate::core::suggest::nearest(
760 spelling,
761 self.accepted_spellings(kind, include_primary),
762 crate::core::suggest::CANDIDATE_LIMIT,
763 )
764 }
765
766 pub(crate) fn member_candidates(
767 &self,
768 domain: &str,
769 spelling: &str,
770 include_primary: bool,
771 ) -> Vec<String> {
772 crate::core::suggest::nearest(
773 spelling,
774 self.accepted_member_spellings(domain, include_primary),
775 crate::core::suggest::CANDIDATE_LIMIT,
776 )
777 }
778
779 pub(crate) fn peek(&self) -> Option<Token> {
780 self.tokens.get(self.pos).cloned()
781 }
782
783 pub(crate) fn peek_at(&self, offset: usize) -> Option<Token> {
784 self.tokens.get(self.pos + offset).cloned()
785 }
786
787 pub(crate) fn peek_kind_at(&self, offset: usize) -> Option<&TokenKind> {
788 self.tokens.get(self.pos + offset).map(|token| &token.kind)
789 }
790
791 pub(crate) fn next(&mut self) -> Option<Token> {
792 let token = self.tokens.get(self.pos).cloned();
793 if token.is_some() {
794 self.pos += 1;
795 }
796 token
797 }
798
799 pub(crate) fn previous(&self) -> &Token {
800 self.tokens
801 .get(self.pos.saturating_sub(1))
802 .unwrap_or_else(|| self.tokens.last().unwrap())
803 }
804
805 pub(crate) fn previous_span(&self) -> (Position, Position) {
806 let token = self.previous();
807 (token.start, token.end)
808 }
809
810 pub(crate) fn span_here(&self) -> (Position, Position) {
811 let token = self
812 .peek()
813 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
814 (token.start, token.end)
815 }
816
817 pub(crate) fn eof(&self) -> &Token {
818 self.tokens.last().unwrap()
819 }
820
821 pub(crate) fn file(&self) -> crate::core::ids::Id<SourceFile> {
822 crate::core::ids::Id::from_index(0)
823 }
824}
825
826fn structural_keyword_map() -> &'static HashMap<String, String> {
827 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
828 KEYWORDS.get_or_init(|| {
829 serde_json::from_str(include_str!("structural_keywords.json"))
830 .expect("structural keyword data is valid JSON")
831 })
832}
833
834pub(crate) fn canonical_keyword(keyword: &str) -> &str {
835 structural_keyword_map()
836 .get(keyword)
837 .map(String::as_str)
838 .unwrap_or(keyword)
839}
840
841pub(crate) fn join_as_written(parts: &[(String, Position, Position)]) -> String {
845 let mut text = String::new();
846 let mut prev_end: Option<Position> = None;
847 for (fragment, start, end) in parts {
848 if prev_end.is_some_and(|prev| prev != *start) {
849 text.push(' ');
850 }
851 text.push_str(fragment);
852 prev_end = Some(*end);
853 }
854 text
855}
856
857pub(crate) fn raw_token_text(kind: &TokenKind) -> String {
858 match kind {
859 TokenKind::Word(value) => value.clone(),
860 TokenKind::Number { text, .. } => text.clone(),
861 TokenKind::String(value) => format!("\"{}\"", crate::output::emitter::escape_string(value)),
862 TokenKind::Op(value) => match value.as_str() {
863 "not" => "!".to_string(),
864 "and" => "&&".to_string(),
865 "or" => "||".to_string(),
866 _ => value.clone(),
867 },
868 TokenKind::LParen => "(".to_string(),
869 TokenKind::RParen => ")".to_string(),
870 TokenKind::Comma => ",".to_string(),
871 TokenKind::Semi => ";".to_string(),
872 TokenKind::LBrace => "{".to_string(),
873 TokenKind::RBrace => "}".to_string(),
874 TokenKind::Colon => ":".to_string(),
875 TokenKind::Dot => ".".to_string(),
876 TokenKind::LBracket => "[".to_string(),
877 TokenKind::RBracket => "]".to_string(),
878 TokenKind::Eof => String::new(),
879 }
880}