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| WorkshopError::Malformed {
84 message: error.message,
85 span: Some(synthetic_span(error.position)),
86 })?;
87 ParseContext {
88 tokens,
89 pos: 0,
90 source: input,
91 catalog,
92 locale: locale.clone(),
93 context,
94 expected_domain: None,
95 condition_pending: false,
96 in_condition: false,
97 call_stack: Vec::new(),
98 target: wir::Program::default(),
99 globals: HashMap::new(),
100 players: HashMap::new(),
101 subroutines: HashMap::new(),
102 next_global_index: 0,
103 next_player_index: 0,
104 }
105 .program()
106}
107
108pub(crate) fn synthetic_span(position: Position) -> Span {
110 Span::new(crate::core::ids::Id::from_index(0), position, position)
111}
112
113pub(crate) struct ParseContext<'a> {
114 pub(crate) tokens: Vec<Token>,
115 pub(crate) pos: usize,
116 pub(crate) source: &'a str,
117 pub(crate) catalog: &'a Catalog,
118 pub(crate) locale: Locale,
119 pub(crate) context: &'a dyn ExpectedDomain,
122 pub(crate) expected_domain: Option<&'a str>,
125 pub(crate) condition_pending: bool,
129 pub(crate) in_condition: bool,
132 pub(crate) call_stack: Vec<String>,
133 pub(crate) target: wir::Program,
134 pub(crate) globals: HashMap<String, wir::GlobalVarId>,
135 pub(crate) players: HashMap<String, wir::PlayerVarId>,
136 pub(crate) subroutines: HashMap<String, wir::SubroutineId>,
137 pub(crate) next_global_index: u32,
138 pub(crate) next_player_index: u32,
139}
140
141impl<'a> ParseContext<'a> {
142 pub(crate) fn with_domain<T>(
145 &mut self,
146 domain: Option<&'a str>,
147 f: impl FnOnce(&mut Self) -> Result<T>,
148 ) -> Result<T> {
149 let saved = self.expected_domain;
150 self.expected_domain = domain;
151 let result = f(self);
152 self.expected_domain = saved;
153 result
154 }
155
156 pub(crate) fn value_in_domain(&mut self, domain: Option<&'a str>) -> Result<wir::ValueId> {
159 self.with_domain(domain, |p| p.value())
160 }
161
162 pub(crate) fn resolve_entry(
163 &self,
164 kind: Kind,
165 spelling: &str,
166 ) -> Option<&'a crate::catalog::CatalogEntry> {
167 self.catalog
168 .resolve(kind, &self.locale, spelling)
169 .or_else(|| {
170 if self.locale != *self.catalog.primary_locale() {
171 self.catalog
172 .resolve(kind, self.catalog.primary_locale(), spelling)
173 } else {
174 None
175 }
176 })
177 }
178
179 pub(crate) fn canonical_keyword(&self, spelling: &str) -> String {
180 self.catalog
181 .resolve(Kind::Structural, &self.locale, spelling)
182 .or_else(|| {
183 (self.locale != *self.catalog.primary_locale()).then(|| {
184 self.catalog
185 .resolve(Kind::Structural, self.catalog.primary_locale(), spelling)
186 })?
187 })
188 .map(|entry| entry.id.clone())
189 .unwrap_or_else(|| canonical_keyword(spelling).to_string())
190 }
191
192 pub(crate) fn line_has_assignment(&self) -> bool {
193 let tokens: Vec<_> = self.tokens[self.pos..]
194 .iter()
195 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
196 .collect();
197 tokens.iter().any(|token| {
198 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
199 }) || tokens.windows(2).any(|window| {
200 matches!(&window[0].kind, TokenKind::Word(_))
201 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
202 })
203 }
204
205 pub(crate) fn phrase(&mut self) -> Result<(String, Position, Position)> {
208 let mut words = Vec::new();
209 let (start, mut end) = match self.peek() {
210 Some(Token {
211 kind: TokenKind::Word(word),
212 start,
213 end,
214 }) => {
215 words.push(word.clone());
216 (start, end)
217 }
218 Some(Token {
219 kind: TokenKind::Number { text, .. },
220 start,
221 end,
222 }) => {
223 words.push(text.clone());
224 (start, end)
225 }
226 Some(token) => return Err(self.malformed("expected an identifier", &token)),
227 None => return Err(self.malformed("expected an identifier", self.eof())),
228 };
229 self.pos += 1;
230 while let Some(token) = self.peek() {
231 match token {
232 Token {
233 kind: TokenKind::Word(word),
234 end: word_end,
235 ..
236 } => {
237 if matches!(
238 self.peek_at(1).map(|token| token.kind),
239 Some(TokenKind::Op(equal)) if equal == "="
240 ) {
241 break;
242 }
243 words.push(word.clone());
244 end = word_end;
245 self.pos += 1;
246 }
247 Token {
251 kind: TokenKind::Number { text, .. },
252 end: number_end,
253 ..
254 } => {
255 words.push(text.clone());
256 end = number_end;
257 self.pos += 1;
258 }
259 _ => break,
260 }
261 }
262 Ok((words.join(" "), start, end))
263 }
264
265 pub(crate) fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
268 self.phrase_on_line_impl(false)
269 }
270
271 pub(crate) fn settings_list_name(&mut self) -> Result<(String, Position, Position)> {
275 self.phrase_on_line_impl(true)
276 }
277
278 fn phrase_on_line_impl(&mut self, list_name: bool) -> Result<(String, Position, Position)> {
279 let mut words = Vec::new();
280 let (start, mut end, line) = match self.peek() {
281 Some(Token {
282 kind: TokenKind::Word(word),
283 start,
284 end,
285 }) => {
286 words.push(word.clone());
287 (start, end, start.line)
288 }
289 Some(Token {
290 kind: TokenKind::Number { text, .. },
291 start,
292 end,
293 }) => {
294 words.push(text.clone());
295 (start, end, start.line)
296 }
297 Some(token) => return Err(self.malformed("expected an identifier", &token)),
298 None => return Err(self.malformed("expected an identifier", self.eof())),
299 };
300 self.pos += 1;
301 while let Some(token) = self.peek() {
302 let (word, word_start, word_end) = match token {
303 Token {
304 kind: TokenKind::Word(word),
305 start,
306 end,
307 } => (word, start, end),
308 Token {
309 kind: TokenKind::Number { text, .. },
310 start,
311 end,
312 } => (text, start, end),
313 Token {
314 kind: TokenKind::Dot,
315 start,
316 end,
317 } => (".".to_string(), start, end),
318 Token {
319 kind: TokenKind::Op(op),
320 start,
321 end,
322 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
323 Token {
324 kind: TokenKind::Colon,
325 start,
326 end,
327 } if list_name => (":".to_string(), start, end),
328 _ => break,
329 };
330 if word_start.line != line {
331 break;
332 }
333 if list_name && word_start == end {
334 words
335 .last_mut()
336 .expect("phrase has a first token")
337 .push_str(&word);
338 } else {
339 words.push(word);
340 }
341 end = word_end;
342 self.pos += 1;
343 }
344 let joined = words
345 .join(" ")
346 .replace(" .", ".")
347 .replace(". ", ".")
348 .replace(" %", "%");
349 let joined = if list_name {
352 joined
353 } else {
354 joined.replace(" : ", ":")
355 };
356 Ok((joined, start, end))
357 }
358
359 pub(crate) fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
360 let (mut phrase, start, mut end) = self.phrase_on_line()?;
361 if matches!(
362 self.peek(),
363 Some(Token {
364 kind: TokenKind::Colon,
365 ..
366 })
367 ) {
368 let colon_pos = self.pos;
369 self.next();
370 let (rest, _, rest_end) = self.phrase_on_line()?;
371 if matches!(
372 self.peek(),
373 Some(Token {
374 kind: TokenKind::LBrace,
375 ..
376 })
377 ) {
378 phrase.push(':');
379 phrase.push(' ');
380 phrase.push_str(&rest);
381 end = rest_end;
382 } else {
383 self.pos = colon_pos;
384 }
385 }
386 Ok((phrase, start, end))
387 }
388
389 pub(crate) fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
390 let first = self
391 .peek()
392 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
393 let start = first.start;
394 let line = first.start.line;
395 let mut end = first.end;
396 let mut parts = Vec::new();
397 while let Some(token) = self.peek() {
398 if token.start.line != line
399 || matches!(token.kind, TokenKind::RParen | TokenKind::Comma)
400 {
401 break;
402 }
403 self.pos += 1;
404 end = token.end;
405 parts.push(raw_token_text(&token.kind));
406 }
407 if parts.is_empty() {
408 return Err(self.malformed("expected an enum member", &first));
409 }
410 Ok((
411 parts
412 .join(" ")
413 .replace(" : ", ":")
414 .replace(" .", ".")
415 .replace(". ", "."),
416 start,
417 end,
418 ))
419 }
420
421 pub(crate) fn line_text(&mut self) -> Result<(String, Option<Span>)> {
425 let mut parts = Vec::new();
426 let mut start = None;
427 let mut end = None;
428 loop {
429 match self.peek() {
430 Some(Token {
431 kind: TokenKind::Semi,
432 ..
433 }) => {
434 self.pos += 1;
435 break;
436 }
437 Some(token) => {
438 start.get_or_insert(token.start);
439 end = Some(token.end);
440 match &token.kind {
441 TokenKind::Word(word) => parts.push(word.clone()),
442 TokenKind::Op(op) if op == "-" => parts.push("-".to_string()),
443 TokenKind::Number { value, .. } => parts.push(value.to_string()),
444 TokenKind::Dot => parts.push(".".to_string()),
445 TokenKind::Colon => parts.push(":".to_string()),
446 _ => return Err(self.malformed("expected a text line", &token)),
447 }
448 self.pos += 1;
449 }
450 None => {
451 return Err(self.malformed("unexpected end of input in line", self.eof()));
452 }
453 }
454 }
455 Ok((
456 parts
457 .join(" ")
458 .replace(" .", ".")
459 .replace(". ", ".")
460 .replace(" : ", ":"),
461 start
462 .zip(end)
463 .map(|(start, end)| Span::new(self.file(), start, end)),
464 ))
465 }
466
467 pub(crate) fn consume_phrase(&mut self, expected: &str) -> Result<()> {
469 let (phrase, _, _) = self.phrase()?;
470 if phrase != expected {
471 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
472 }
473 Ok(())
474 }
475
476 pub(crate) fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
477 match self.next() {
478 Some(Token {
479 kind: TokenKind::Word(word),
480 start,
481 ..
482 }) if self.canonical_keyword(&word) == expected => Ok(start),
483 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
484 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
485 }
486 }
487
488 pub(crate) fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
489 match self.next() {
490 Some(token) if token.kind == kind => Ok(()),
491 Some(token) => Err(self.malformed(message, &token)),
492 None => Err(self.malformed(message, self.eof())),
493 }
494 }
495
496 pub(crate) fn expect_semi(&mut self, message: &str) -> Result<()> {
501 match self.peek() {
502 Some(Token {
503 kind: TokenKind::Semi,
504 ..
505 }) => {
506 self.pos += 1;
507 Ok(())
508 }
509 Some(token) if token.start.line > self.previous().end.line => {
510 let end = self.previous().end;
511 Err(WorkshopError::Malformed {
512 message: message.to_string(),
513 span: Some(Span::new(self.file(), end, end)),
514 })
515 }
516 Some(token) => Err(self.malformed(message, &token)),
517 None => Err(self.malformed(message, self.eof())),
518 }
519 }
520
521 pub(crate) fn expect_string(&mut self, message: &str) -> Result<String> {
522 match self.next() {
523 Some(Token {
524 kind: TokenKind::String(content),
525 ..
526 }) => Ok(content),
527 Some(token) => Err(self.malformed(message, &token)),
528 None => Err(self.malformed(message, self.eof())),
529 }
530 }
531
532 pub(crate) fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
533 WorkshopError::Malformed {
534 message: message.to_string(),
535 span: Some(Span::new(self.file(), token.start, token.end)),
536 }
537 }
538
539 pub(crate) fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
540 WorkshopError::Unknown {
541 kind,
542 spelling: spelling.to_string(),
543 locale: self.locale.clone(),
544 span: None,
545 }
546 }
547
548 pub(crate) fn peek(&self) -> Option<Token> {
549 self.tokens.get(self.pos).cloned()
550 }
551
552 pub(crate) fn peek_at(&self, offset: usize) -> Option<Token> {
553 self.tokens.get(self.pos + offset).cloned()
554 }
555
556 pub(crate) fn next(&mut self) -> Option<Token> {
557 let token = self.tokens.get(self.pos).cloned();
558 if token.is_some() {
559 self.pos += 1;
560 }
561 token
562 }
563
564 pub(crate) fn previous(&self) -> &Token {
565 self.tokens
566 .get(self.pos.saturating_sub(1))
567 .unwrap_or_else(|| self.tokens.last().unwrap())
568 }
569
570 pub(crate) fn previous_span(&self) -> (Position, Position) {
571 let token = self.previous();
572 (token.start, token.end)
573 }
574
575 pub(crate) fn span_here(&self) -> (Position, Position) {
576 let token = self
577 .peek()
578 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
579 (token.start, token.end)
580 }
581
582 pub(crate) fn eof(&self) -> &Token {
583 self.tokens.last().unwrap()
584 }
585
586 pub(crate) fn file(&self) -> crate::core::ids::Id<SourceFile> {
587 crate::core::ids::Id::from_index(0)
588 }
589}
590
591pub(crate) fn canonical_keyword(keyword: &str) -> &str {
592 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
593 KEYWORDS
594 .get_or_init(|| {
595 serde_json::from_str(include_str!("structural_keywords.json"))
596 .expect("structural keyword data is valid JSON")
597 })
598 .get(keyword)
599 .map(String::as_str)
600 .unwrap_or(keyword)
601}
602
603pub(crate) fn raw_token_text(kind: &TokenKind) -> String {
604 match kind {
605 TokenKind::Word(value) => value.clone(),
606 TokenKind::Number { text, .. } => text.clone(),
607 TokenKind::String(value) => format!("\"{}\"", value.replace('"', "\\\"")),
608 TokenKind::Op(value) => value.clone(),
609 TokenKind::LParen => "(".to_string(),
610 TokenKind::RParen => ")".to_string(),
611 TokenKind::Comma => ",".to_string(),
612 TokenKind::Semi => ";".to_string(),
613 TokenKind::LBrace => "{".to_string(),
614 TokenKind::RBrace => "}".to_string(),
615 TokenKind::Colon => ":".to_string(),
616 TokenKind::Dot => ".".to_string(),
617 TokenKind::LBracket => "[".to_string(),
618 TokenKind::RBracket => "]".to_string(),
619 TokenKind::Eof => String::new(),
620 }
621}