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