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 pub(crate) fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
476 match self.next() {
477 Some(Token {
478 kind: TokenKind::Word(word),
479 start,
480 ..
481 }) if self.canonical_keyword(&word) == expected => Ok(start),
482 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
483 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
484 }
485 }
486
487 pub(crate) fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
488 match self.next() {
489 Some(token) if token.kind == kind => Ok(()),
490 Some(token) => Err(self.malformed(message, &token)),
491 None => Err(self.malformed(message, self.eof())),
492 }
493 }
494
495 pub(crate) fn expect_semi(&mut self, message: &str) -> Result<()> {
500 match self.peek() {
501 Some(Token {
502 kind: TokenKind::Semi,
503 ..
504 }) => {
505 self.pos += 1;
506 Ok(())
507 }
508 Some(token) if token.start.line > self.previous().end.line => {
509 let end = self.previous().end;
510 Err(WorkshopError::malformed(
511 message.to_string(),
512 Some(Span::new(self.file(), end, end)),
513 ))
514 }
515 Some(token) => Err(self.malformed(message, &token)),
516 None => Err(self.malformed(message, self.eof())),
517 }
518 }
519
520 pub(crate) fn expect_string(&mut self, message: &str) -> Result<String> {
521 match self.next() {
522 Some(Token {
523 kind: TokenKind::String(content),
524 ..
525 }) => Ok(content),
526 Some(token) => Err(self.malformed(message, &token)),
527 None => Err(self.malformed(message, self.eof())),
528 }
529 }
530
531 pub(crate) fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
532 WorkshopError::malformed(
533 message.to_string(),
534 Some(Span::new(self.file(), token.start, token.end)),
535 )
536 }
537
538 pub(crate) fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
539 WorkshopError::unknown(kind, spelling.to_string(), self.locale.clone(), None)
540 }
541
542 pub(crate) fn peek(&self) -> Option<Token> {
543 self.tokens.get(self.pos).cloned()
544 }
545
546 pub(crate) fn peek_at(&self, offset: usize) -> Option<Token> {
547 self.tokens.get(self.pos + offset).cloned()
548 }
549
550 pub(crate) fn next(&mut self) -> Option<Token> {
551 let token = self.tokens.get(self.pos).cloned();
552 if token.is_some() {
553 self.pos += 1;
554 }
555 token
556 }
557
558 pub(crate) fn previous(&self) -> &Token {
559 self.tokens
560 .get(self.pos.saturating_sub(1))
561 .unwrap_or_else(|| self.tokens.last().unwrap())
562 }
563
564 pub(crate) fn previous_span(&self) -> (Position, Position) {
565 let token = self.previous();
566 (token.start, token.end)
567 }
568
569 pub(crate) fn span_here(&self) -> (Position, Position) {
570 let token = self
571 .peek()
572 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
573 (token.start, token.end)
574 }
575
576 pub(crate) fn eof(&self) -> &Token {
577 self.tokens.last().unwrap()
578 }
579
580 pub(crate) fn file(&self) -> crate::core::ids::Id<SourceFile> {
581 crate::core::ids::Id::from_index(0)
582 }
583}
584
585pub(crate) fn canonical_keyword(keyword: &str) -> &str {
586 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
587 KEYWORDS
588 .get_or_init(|| {
589 serde_json::from_str(include_str!("structural_keywords.json"))
590 .expect("structural keyword data is valid JSON")
591 })
592 .get(keyword)
593 .map(String::as_str)
594 .unwrap_or(keyword)
595}
596
597pub(crate) fn raw_token_text(kind: &TokenKind) -> String {
598 match kind {
599 TokenKind::Word(value) => value.clone(),
600 TokenKind::Number { text, .. } => text.clone(),
601 TokenKind::String(value) => format!("\"{}\"", value.replace('"', "\\\"")),
602 TokenKind::Op(value) => value.clone(),
603 TokenKind::LParen => "(".to_string(),
604 TokenKind::RParen => ")".to_string(),
605 TokenKind::Comma => ",".to_string(),
606 TokenKind::Semi => ";".to_string(),
607 TokenKind::LBrace => "{".to_string(),
608 TokenKind::RBrace => "}".to_string(),
609 TokenKind::Colon => ":".to_string(),
610 TokenKind::Dot => ".".to_string(),
611 TokenKind::LBracket => "[".to_string(),
612 TokenKind::RBracket => "]".to_string(),
613 TokenKind::Eof => String::new(),
614 }
615}