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