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::{self, KeyKind, PathPart};
16pub(crate) use crate::settings::{Settings, SettingsListElement, SettingsNode};
17pub(crate) use crate::wir::{
18 self, Action, Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind, Value, ValueNode,
19};
20
21pub(crate) use super::lexer::{Token, TokenKind, tokenize};
22pub(crate) use crate::catalog::{Catalog, Kind, Locale, ParamCoercions};
23pub(crate) use crate::core::error::{Result, WorkshopError};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(crate) enum Stop {
28 SectionClosed,
30 End,
32 ElseIf,
34 Else,
36}
37
38pub(crate) enum AssignmentOperator {
39 Set,
40 Modify(ModifyOp),
41}
42
43pub fn parse(input: &str, catalog: &Catalog, locale: &Locale) -> Result<Program> {
49 parse_with_context(input, catalog, locale, catalog)
50}
51
52#[doc(hidden)]
55pub fn parse_wir(input: &str, catalog: &Catalog, locale: &Locale) -> Result<wir::Program> {
56 parse_wir_with_context(input, catalog, locale, catalog)
57}
58
59pub fn parse_with_context(
68 input: &str,
69 catalog: &Catalog,
70 locale: &Locale,
71 context: &dyn ExpectedDomain,
72) -> Result<Program> {
73 parse_wir_with_context(input, catalog, locale, context).and_then(Program::from_wir)
74}
75
76#[doc(hidden)]
77pub 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 call_stack: Vec::new(),
96 target: wir::Program::default(),
97 globals: HashMap::new(),
98 players: HashMap::new(),
99 subroutines: HashMap::new(),
100 }
101 .program()
102}
103
104pub(crate) fn synthetic_span(position: Position) -> Span {
106 Span::new(crate::core::ids::Id::from_index(0), position, position)
107}
108
109pub(crate) struct ParseContext<'a> {
110 pub(crate) tokens: Vec<Token>,
111 pub(crate) pos: usize,
112 pub(crate) source: &'a str,
113 pub(crate) catalog: &'a Catalog,
114 pub(crate) locale: Locale,
115 pub(crate) context: &'a dyn ExpectedDomain,
118 pub(crate) expected_domain: Option<&'a str>,
121 pub(crate) call_stack: Vec<String>,
122 pub(crate) target: wir::Program,
123 pub(crate) globals: HashMap<String, wir::GlobalVarId>,
124 pub(crate) players: HashMap<String, wir::PlayerVarId>,
125 pub(crate) subroutines: HashMap<String, wir::SubroutineId>,
126}
127
128impl<'a> ParseContext<'a> {
129 pub(crate) fn resolve_entry(
130 &self,
131 kind: Kind,
132 spelling: &str,
133 ) -> Option<&'a crate::catalog::CatalogEntry> {
134 self.catalog
135 .resolve(kind, &self.locale, spelling)
136 .or_else(|| {
137 if self.locale != *self.catalog.primary_locale() {
138 self.catalog
139 .resolve(kind, self.catalog.primary_locale(), spelling)
140 } else {
141 None
142 }
143 })
144 }
145
146 pub(crate) fn canonical_keyword(&self, spelling: &str) -> String {
147 self.catalog
148 .resolve(Kind::Structural, &self.locale, spelling)
149 .or_else(|| {
150 (self.locale != *self.catalog.primary_locale()).then(|| {
151 self.catalog
152 .resolve(Kind::Structural, self.catalog.primary_locale(), spelling)
153 })?
154 })
155 .map(|entry| entry.id.clone())
156 .unwrap_or_else(|| canonical_keyword(spelling).to_string())
157 }
158
159 pub(crate) fn line_has_assignment(&self) -> bool {
160 let tokens: Vec<_> = self.tokens[self.pos..]
161 .iter()
162 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
163 .collect();
164 tokens.iter().any(|token| {
165 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
166 }) || tokens.windows(2).any(|window| {
167 matches!(&window[0].kind, TokenKind::Word(_))
168 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
169 })
170 }
171
172 pub(crate) fn phrase(&mut self) -> Result<(String, Position, Position)> {
175 let mut words = Vec::new();
176 let (start, mut end) = match self.peek() {
177 Some(Token {
178 kind: TokenKind::Word(word),
179 start,
180 end,
181 }) => {
182 words.push(word.clone());
183 (start, end)
184 }
185 Some(Token {
186 kind: TokenKind::Number { text, .. },
187 start,
188 end,
189 }) => {
190 words.push(text.clone());
191 (start, end)
192 }
193 Some(token) => return Err(self.malformed("expected an identifier", &token)),
194 None => return Err(self.malformed("expected an identifier", self.eof())),
195 };
196 self.pos += 1;
197 while let Some(token) = self.peek() {
198 match token {
199 Token {
200 kind: TokenKind::Word(word),
201 end: word_end,
202 ..
203 } => {
204 if matches!(
205 self.peek_at(1).map(|token| token.kind),
206 Some(TokenKind::Op(equal)) if equal == "="
207 ) {
208 break;
209 }
210 words.push(word.clone());
211 end = word_end;
212 self.pos += 1;
213 }
214 Token {
218 kind: TokenKind::Number { text, .. },
219 end: number_end,
220 ..
221 } => {
222 words.push(text.clone());
223 end = number_end;
224 self.pos += 1;
225 }
226 _ => break,
227 }
228 }
229 Ok((words.join(" "), start, end))
230 }
231
232 pub(crate) fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
235 let mut words = Vec::new();
236 let (start, mut end, line) = match self.peek() {
237 Some(Token {
238 kind: TokenKind::Word(word),
239 start,
240 end,
241 }) => {
242 words.push(word.clone());
243 (start, end, start.line)
244 }
245 Some(Token {
246 kind: TokenKind::Number { text, .. },
247 start,
248 end,
249 }) => {
250 words.push(text.clone());
251 (start, end, start.line)
252 }
253 Some(token) => return Err(self.malformed("expected an identifier", &token)),
254 None => return Err(self.malformed("expected an identifier", self.eof())),
255 };
256 self.pos += 1;
257 while let Some(token) = self.peek() {
258 let (word, word_start, word_end) = match token {
259 Token {
260 kind: TokenKind::Word(word),
261 start,
262 end,
263 } => (word, start, end),
264 Token {
265 kind: TokenKind::Number { text, .. },
266 start,
267 end,
268 } => (text, start, end),
269 Token {
270 kind: TokenKind::Dot,
271 start,
272 end,
273 } => (".".to_string(), start, end),
274 Token {
275 kind: TokenKind::Op(op),
276 start,
277 end,
278 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
279 _ => break,
280 };
281 if word_start.line != line {
282 break;
283 }
284 words.push(word);
285 end = word_end;
286 self.pos += 1;
287 }
288 Ok((
289 words
290 .join(" ")
291 .replace(" .", ".")
292 .replace(". ", ".")
293 .replace(" : ", ":")
294 .replace(" %", "%"),
295 start,
296 end,
297 ))
298 }
299
300 pub(crate) fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
301 let (mut phrase, start, mut end) = self.phrase_on_line()?;
302 if matches!(
303 self.peek(),
304 Some(Token {
305 kind: TokenKind::Colon,
306 ..
307 })
308 ) {
309 let colon_pos = self.pos;
310 self.next();
311 let (rest, _, rest_end) = self.phrase_on_line()?;
312 if matches!(
313 self.peek(),
314 Some(Token {
315 kind: TokenKind::LBrace,
316 ..
317 })
318 ) {
319 phrase.push(':');
320 phrase.push(' ');
321 phrase.push_str(&rest);
322 end = rest_end;
323 } else {
324 self.pos = colon_pos;
325 }
326 }
327 Ok((phrase, start, end))
328 }
329
330 pub(crate) fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
331 let first = self
332 .peek()
333 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
334 let start = first.start;
335 let line = first.start.line;
336 let mut end = first.end;
337 let mut parts = Vec::new();
338 while let Some(token) = self.peek() {
339 if token.start.line != line
340 || matches!(token.kind, TokenKind::RParen | TokenKind::Comma)
341 {
342 break;
343 }
344 self.pos += 1;
345 end = token.end;
346 parts.push(raw_token_text(&token.kind));
347 }
348 if parts.is_empty() {
349 return Err(self.malformed("expected an enum member", &first));
350 }
351 Ok((
352 parts
353 .join(" ")
354 .replace(" : ", ":")
355 .replace(" .", ".")
356 .replace(". ", "."),
357 start,
358 end,
359 ))
360 }
361
362 pub(crate) fn line_text(&mut self) -> Result<String> {
365 let mut parts = Vec::new();
366 loop {
367 match self.peek() {
368 Some(Token {
369 kind: TokenKind::Semi,
370 ..
371 }) => {
372 self.pos += 1;
373 break;
374 }
375 Some(Token {
376 kind: TokenKind::Word(word),
377 ..
378 }) => {
379 parts.push(word.clone());
380 self.pos += 1;
381 }
382 Some(Token {
383 kind: TokenKind::Op(op),
384 ..
385 }) if op == "-" => {
386 parts.push("-".to_string());
387 self.pos += 1;
388 }
389 Some(Token {
390 kind: TokenKind::Number { value, .. },
391 ..
392 }) => {
393 parts.push(value.to_string());
394 self.pos += 1;
395 }
396 Some(Token {
397 kind: TokenKind::Dot,
398 ..
399 }) => {
400 parts.push(".".to_string());
401 self.pos += 1;
402 }
403 Some(Token {
404 kind: TokenKind::Colon,
405 ..
406 }) => {
407 parts.push(":".to_string());
408 self.pos += 1;
409 }
410 Some(token) => return Err(self.malformed("expected a text line", &token)),
411 None => return Err(self.malformed("unexpected end of input in line", self.eof())),
412 }
413 }
414 Ok(parts
415 .join(" ")
416 .replace(" .", ".")
417 .replace(". ", ".")
418 .replace(" : ", ":"))
419 }
420
421 pub(crate) fn consume_phrase(&mut self, expected: &str) -> Result<()> {
423 let (phrase, _, _) = self.phrase()?;
424 if phrase != expected {
425 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
426 }
427 Ok(())
428 }
429
430 pub(crate) fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
431 match self.next() {
432 Some(Token {
433 kind: TokenKind::Word(word),
434 start,
435 ..
436 }) if self.canonical_keyword(&word) == expected => Ok(start),
437 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
438 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
439 }
440 }
441
442 pub(crate) fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
443 match self.next() {
444 Some(token) if token.kind == kind => Ok(()),
445 Some(token) => Err(self.malformed(message, &token)),
446 None => Err(self.malformed(message, self.eof())),
447 }
448 }
449
450 pub(crate) fn expect_string(&mut self, message: &str) -> Result<String> {
451 match self.next() {
452 Some(Token {
453 kind: TokenKind::String(content),
454 ..
455 }) => Ok(content),
456 Some(token) => Err(self.malformed(message, &token)),
457 None => Err(self.malformed(message, self.eof())),
458 }
459 }
460
461 pub(crate) fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
462 WorkshopError::Malformed {
463 message: message.to_string(),
464 span: Some(Span::new(self.file(), token.start, token.end)),
465 }
466 }
467
468 pub(crate) fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
469 WorkshopError::Unknown {
470 kind,
471 spelling: spelling.to_string(),
472 locale: self.locale.clone(),
473 span: None,
474 }
475 }
476
477 pub(crate) fn peek(&self) -> Option<Token> {
478 self.tokens.get(self.pos).cloned()
479 }
480
481 pub(crate) fn peek_at(&self, offset: usize) -> Option<Token> {
482 self.tokens.get(self.pos + offset).cloned()
483 }
484
485 pub(crate) fn next(&mut self) -> Option<Token> {
486 let token = self.tokens.get(self.pos).cloned();
487 if token.is_some() {
488 self.pos += 1;
489 }
490 token
491 }
492
493 pub(crate) fn previous(&self) -> &Token {
494 self.tokens
495 .get(self.pos.saturating_sub(1))
496 .unwrap_or_else(|| self.tokens.last().unwrap())
497 }
498
499 pub(crate) fn previous_span(&self) -> (Position, Position) {
500 let token = self.previous();
501 (token.start, token.end)
502 }
503
504 pub(crate) fn span_here(&self) -> (Position, Position) {
505 let token = self
506 .peek()
507 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
508 (token.start, token.end)
509 }
510
511 pub(crate) fn eof(&self) -> &Token {
512 self.tokens.last().unwrap()
513 }
514
515 pub(crate) fn file(&self) -> crate::core::ids::Id<SourceFile> {
516 crate::core::ids::Id::from_index(0)
517 }
518}
519
520pub(crate) fn is_comparison(op: &str) -> bool {
521 matches!(op, "==" | "!=" | "<" | "<=" | ">" | ">=")
522}
523
524pub(crate) fn canonical_keyword(keyword: &str) -> &str {
525 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
526 KEYWORDS
527 .get_or_init(|| {
528 serde_json::from_str(include_str!("structural_keywords.json"))
529 .expect("structural keyword data is valid JSON")
530 })
531 .get(keyword)
532 .map(String::as_str)
533 .unwrap_or(keyword)
534}
535
536pub(crate) fn raw_token_text(kind: &TokenKind) -> String {
537 match kind {
538 TokenKind::Word(value) => value.clone(),
539 TokenKind::Number { text, .. } => text.clone(),
540 TokenKind::String(value) => format!("\"{}\"", value.replace('"', "\\\"")),
541 TokenKind::Op(value) => value.clone(),
542 TokenKind::LParen => "(".to_string(),
543 TokenKind::RParen => ")".to_string(),
544 TokenKind::Comma => ",".to_string(),
545 TokenKind::Semi => ";".to_string(),
546 TokenKind::LBrace => "{".to_string(),
547 TokenKind::RBrace => "}".to_string(),
548 TokenKind::Colon => ":".to_string(),
549 TokenKind::Dot => ".".to_string(),
550 TokenKind::LBracket => "[".to_string(),
551 TokenKind::RBracket => "]".to_string(),
552 TokenKind::Eof => String::new(),
553 }
554}