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