1use std::collections::HashMap;
18
19use crate::cst;
20use crate::diag::{OpyError, OpyResult, Position, Span};
21use crate::hir;
22
23#[derive(Debug, Clone)]
25pub struct SettingsBlock {
26 pub text: String,
28 pub span: Span,
30 pub keyword_span: Span,
32 pub start: usize,
34 pub end: usize,
36 pub text_start: Position,
38}
39
40pub fn find_blocks(text: &str, file_id: u32) -> OpyResult<Vec<SettingsBlock>> {
49 let chars: Vec<char> = text.chars().collect();
50 #[cfg(test)]
51 crate::resource_metrics::record_settings_chars(chars.len());
52 let mut scanner = Scanner {
53 chars: &chars,
54 pos: 0,
55 line: 1,
56 col: 1,
57 };
58 let mut blocks = Vec::new();
59 let mut in_block_comment = false;
60 let mut string_quote = None;
61 let mut escaped = false;
62 let mut seen_first_construct = false;
63 while scanner.pos < scanner.chars.len() {
64 let ch = scanner.chars[scanner.pos];
65 if let Some(quote) = string_quote {
66 if escaped {
67 escaped = false;
68 } else if ch == '\\' {
69 escaped = true;
70 } else if ch == quote {
71 string_quote = None;
72 }
73 scanner.advance(1);
74 continue;
75 }
76 if in_block_comment {
77 if ch == '*' && scanner.peek(1) == Some('/') {
78 in_block_comment = false;
79 scanner.advance(2);
80 } else {
81 scanner.advance(1);
82 }
83 continue;
84 }
85 if matches!(ch, '"' | '\'') {
86 string_quote = Some(ch);
87 scanner.advance(1);
88 continue;
89 }
90 if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' {
91 scanner.advance(1);
92 continue;
93 }
94 if ch == '#' {
95 scanner.skip_to_eol();
96 continue;
97 }
98 if ch == '/' && scanner.peek(1) == Some('*') {
99 scanner.advance(2);
100 in_block_comment = true;
101 continue;
102 }
103 if is_ident_start(ch) {
105 let keyword_start = scanner.here();
106 let keyword_offset = scanner.pos;
107 let word = scanner.read_word();
108 if word == "settings" && keyword_start.col == 1 {
109 let keyword_span = Span::new(file_id, keyword_start, scanner.here());
110 if seen_first_construct || !blocks.is_empty() {
111 return Err(OpyError::at(
112 "settings-placement",
113 "settings block must be the first construct in the file".to_string(),
114 keyword_span,
115 ));
116 }
117 let block = match_block(&mut scanner, keyword_start, keyword_offset, keyword_span)?;
118 blocks.push(block);
119 seen_first_construct = true;
120 continue;
121 }
122 seen_first_construct = true;
123 continue;
124 }
125 seen_first_construct = true;
126 scanner.advance(1);
127 }
128 Ok(blocks)
129}
130
131fn match_block(
134 scanner: &mut Scanner<'_>,
135 keyword_start: Position,
136 keyword_offset: usize,
137 keyword_span: Span,
138) -> OpyResult<SettingsBlock> {
139 scanner.skip_whitespace();
140 if scanner.chars.get(scanner.pos) != Some(&'{') {
141 return Err(OpyError::at(
142 "settings-invalid",
143 "settings block must be a `settings { ... }` block (the `settings \"file\"` form is not supported)"
144 .to_string(),
145 keyword_span,
146 ));
147 }
148 let mut depth = 0usize;
149 let mut string_quote: Option<char> = None;
150 let mut escaped = false;
151 let mut text_start_offset = None;
152 let mut text_start = None;
153 loop {
154 let Some(ch) = scanner.chars.get(scanner.pos).copied() else {
155 return Err(OpyError::at(
156 "settings-invalid",
157 "unterminated settings block (missing closing brace)".to_string(),
158 keyword_span,
159 ));
160 };
161 if let Some(quote) = string_quote {
162 if escaped {
163 escaped = false;
164 } else if ch == '\\' {
165 escaped = true;
166 } else if ch == quote {
167 string_quote = None;
168 }
169 scanner.advance(1);
170 continue;
171 }
172 match ch {
173 '"' | '\'' => string_quote = Some(ch),
174 '{' => {
175 if depth == 0 {
176 text_start_offset = Some(scanner.pos + 1);
177 text_start = Some(scanner.here_after(1));
178 }
179 depth += 1;
180 }
181 '}' => {
182 depth -= 1;
183 if depth == 0 {
184 let text = scanner
185 .chars
186 .get(text_start_offset.expect("text start offset set on '{'")..scanner.pos)
187 .map(|slice| slice.iter().collect::<String>())
188 .unwrap_or_default();
189 return Ok(SettingsBlock {
190 text,
191 span: Span::new(keyword_span.file, keyword_start, scanner.here_after(1)),
192 keyword_span,
193 start: keyword_offset,
194 end: scanner.pos + 1,
195 text_start: text_start.expect("text start set on '{'"),
196 });
197 }
198 }
199 _ => {}
200 }
201 scanner.advance(1);
202 }
203}
204
205pub fn sanitize_for_lex(text: &str, block: &SettingsBlock) -> String {
208 let mut out = String::with_capacity(text.len());
209 for (index, ch) in text.chars().enumerate() {
210 if index >= block.start && index < block.end {
211 out.push(if ch == '\n' { '\n' } else { ' ' });
212 } else {
213 out.push(ch);
214 }
215 }
216 out
217}
218
219pub fn parse_block(block: &SettingsBlock) -> OpyResult<cst::Settings> {
226 let mut parser = Jsonc {
227 text: &block.text,
228 pos: 0,
229 line: block.text_start.line,
230 col: block.text_start.col,
231 file: block.span.file,
232 };
233 parser.skip_whitespace();
234 let children = parser.parse_members(true)?;
237 parser.skip_whitespace();
238 if parser.pos < parser.text.len() {
239 return Err(parser.error(
240 "settings-invalid",
241 "unexpected content after the settings object".to_string(),
242 ));
243 }
244 if !children
245 .iter()
246 .any(|node| matches!(node, cst::SettingsNode::Group { name, .. } if name == "gamemodes"))
247 {
248 return Err(OpyError::at(
249 "settings-invalid",
250 "settings block must contain a gamemodes group".to_string(),
251 block.span,
252 ));
253 }
254 Ok(cst::Settings {
255 span: block.span,
256 children,
257 })
258}
259
260pub(crate) fn resolve_hir_settings(
266 program: &mut hir::Program,
267 cst_program: &cst::Program,
268) -> OpyResult<()> {
269 let constants = program
270 .declarations
271 .iter()
272 .filter_map(|declaration| match declaration {
273 hir::Declaration::Constant { name, value, .. } => {
274 Some((name.clone(), value.as_ref().clone()))
275 }
276 _ => None,
277 })
278 .collect::<HashMap<_, _>>();
279 let constant_refs = constants
280 .iter()
281 .map(|(name, value)| (name.clone(), value))
282 .collect::<HashMap<_, _>>();
283 let mut expander = crate::compiler::MacroExpander::from_program(program);
284 let Some(settings) = program.settings.take() else {
285 return Ok(());
286 };
287 let children = settings
288 .children
289 .into_iter()
290 .map(|node| resolve_hir_node(node, cst_program, &constant_refs, &mut expander))
291 .collect::<OpyResult<Vec<_>>>()?;
292 program.settings = Some(hir::Settings {
293 span: settings.span,
294 children,
295 });
296 Ok(())
297}
298
299fn resolve_hir_node(
300 node: hir::SettingsNode,
301 cst_program: &cst::Program,
302 constants: &HashMap<String, &hir::Expr>,
303 expander: &mut crate::compiler::MacroExpander,
304) -> OpyResult<hir::SettingsNode> {
305 match node {
306 hir::SettingsNode::Group {
307 name,
308 children,
309 span,
310 } => Ok(hir::SettingsNode::Group {
311 name,
312 children: children
313 .into_iter()
314 .map(|child| resolve_hir_node(child, cst_program, constants, expander))
315 .collect::<OpyResult<Vec<_>>>()?,
316 span,
317 }),
318 hir::SettingsNode::Raw { name, value, span } => {
319 let Some(span) = span else {
320 return Err(OpyError::new(
321 "settings-expression",
322 "settings value has no source span",
323 ));
324 };
325 let diag_span = hir_span_to_diag_span(span);
326 let expression = crate::lower::lower_settings_expression(
327 cst_program,
328 &value,
329 span.file,
330 Position::new(span.start.line, span.start.col),
331 )
332 .map_err(|error| settings_expression_error(diag_span, error.message))?;
333 let expression =
334 expander
335 .expand_expr(&expression, &HashMap::new())
336 .map_err(|error| {
337 let error_span = error
338 .diagnostic
339 .span
340 .map(hir_span_to_diag_span)
341 .unwrap_or(diag_span);
342 OpyError::at(error.diagnostic.code, error.diagnostic.message, error_span)
343 })?;
344 let mut stack = Vec::new();
345 let value =
346 crate::compile_time::evaluate(&expression, constants, &HashMap::new(), &mut stack)
347 .ok_or_else(|| {
348 settings_expression_error(
349 diag_span,
350 "expression is not a compile-time value".to_string(),
351 )
352 })?;
353 node_from_value(name, value, diag_span)
354 }
355 node => Ok(node),
356 }
357}
358
359fn hir_span_to_diag_span(span: hir::Span) -> Span {
360 Span::new(
361 span.file,
362 Position::new(span.start.line, span.start.col),
363 Position::new(span.end.line, span.end.col),
364 )
365}
366
367fn settings_expression_error(span: Span, message: String) -> OpyError {
368 OpyError::at(
369 "settings-expression",
370 format!("settings value is not a compile-time value: {message}"),
371 span,
372 )
373}
374
375fn node_from_value(
376 name: String,
377 value: crate::compile_time::Value,
378 span: Span,
379) -> OpyResult<hir::SettingsNode> {
380 let node = match value {
381 crate::compile_time::Value::Number(value) if value.is_finite() => {
382 hir::SettingsNode::Number {
383 name,
384 value,
385 span: Some(span.into()),
386 }
387 }
388 crate::compile_time::Value::Number(_) => {
389 return Err(settings_expression_error(
390 span,
391 "compile-time number is not finite".to_string(),
392 ));
393 }
394 crate::compile_time::Value::String(value) => hir::SettingsNode::String {
395 name,
396 value,
397 span: Some(span.into()),
398 },
399 crate::compile_time::Value::Bool(value) => hir::SettingsNode::Bool {
400 name,
401 value,
402 span: Some(span.into()),
403 },
404 crate::compile_time::Value::Array(values) => hir::SettingsNode::List {
405 name,
406 elements: values
407 .into_iter()
408 .map(|value| {
409 Ok(hir::SettingsListElement {
410 value: display_value(&value)?,
411 span: Some(span.into()),
412 })
413 })
414 .collect::<Result<Vec<_>, String>>()
415 .map_err(|message| settings_expression_error(span, message))?,
416 span: Some(span.into()),
417 },
418 crate::compile_time::Value::Object(entries) => hir::SettingsNode::Group {
419 name,
420 children: entries
421 .into_iter()
422 .map(|(key, value)| {
423 let crate::compile_time::Value::String(name) = key else {
424 return Err(settings_expression_error(
425 span,
426 "compile-time object keys must be strings".to_string(),
427 ));
428 };
429 node_from_value(name, value, span)
430 })
431 .collect::<OpyResult<Vec<_>>>()?,
432 span: Some(span.into()),
433 },
434 };
435 Ok(node)
436}
437
438fn display_value(value: &crate::compile_time::Value) -> Result<String, String> {
439 match value {
440 crate::compile_time::Value::Number(value) if value.is_finite() => Ok(value.to_string()),
441 crate::compile_time::Value::String(value) => Ok(value.clone()),
442 crate::compile_time::Value::Bool(value) => Ok(value.to_string()),
443 _ => Err("settings list can only contain primitive values".to_string()),
444 }
445}
446struct Scanner<'a> {
447 chars: &'a [char],
448 pos: usize,
449 line: u32,
450 col: u32,
451}
452
453impl Scanner<'_> {
454 fn peek(&self, ahead: usize) -> Option<char> {
455 self.chars.get(self.pos + ahead).copied()
456 }
457
458 fn here(&self) -> Position {
459 Position::new(self.line, self.col)
460 }
461
462 fn here_after(&self, n: usize) -> Position {
463 let mut line = self.line;
464 let mut col = self.col;
465 for i in 0..n {
466 if self.chars.get(self.pos + i) == Some(&'\n') {
467 line += 1;
468 col = 1;
469 } else {
470 col += 1;
471 }
472 }
473 Position::new(line, col)
474 }
475
476 fn advance(&mut self, n: usize) {
477 for _ in 0..n {
478 if self.pos >= self.chars.len() {
479 return;
480 }
481 if self.chars[self.pos] == '\n' {
482 self.line += 1;
483 self.col = 1;
484 } else {
485 self.col += 1;
486 }
487 self.pos += 1;
488 }
489 }
490
491 fn skip_to_eol(&mut self) {
492 while self.pos < self.chars.len() && self.chars[self.pos] != '\n' {
493 self.advance(1);
494 }
495 }
496
497 fn skip_whitespace(&mut self) {
498 while self.pos < self.chars.len() && matches!(self.chars[self.pos], ' ' | '\t' | '\r') {
499 self.advance(1);
500 }
501 }
502
503 fn read_word(&mut self) -> String {
504 let mut word = String::new();
505 while self.pos < self.chars.len() && is_ident_continue(self.chars[self.pos]) {
506 word.push(self.chars[self.pos]);
507 self.advance(1);
508 }
509 word
510 }
511}
512
513struct Jsonc<'a> {
515 text: &'a str,
516 pos: usize,
517 line: u32,
518 col: u32,
519 file: u32,
520}
521
522impl Jsonc<'_> {
523 fn here(&self) -> Position {
524 Position::new(self.line, self.col)
525 }
526
527 fn peek(&self) -> Option<char> {
528 self.text[self.pos..].chars().next()
529 }
530
531 fn advance(&mut self) -> Option<char> {
532 let ch = self.peek()?;
533 self.pos += ch.len_utf8();
534 if ch == '\n' {
535 self.line += 1;
536 self.col = 1;
537 } else {
538 self.col += 1;
539 }
540 Some(ch)
541 }
542
543 fn skip_whitespace(&mut self) {
544 loop {
545 while let Some(ch) = self.peek() {
546 if ch.is_whitespace() {
547 self.advance();
548 } else {
549 break;
550 }
551 }
552 if self.peek() == Some('#') {
553 while self.peek().is_some_and(|ch| ch != '\n') {
554 self.advance();
555 }
556 continue;
557 }
558 if self.text[self.pos..].starts_with("//") {
559 self.advance();
560 self.advance();
561 while self.peek().is_some_and(|ch| ch != '\n') {
562 self.advance();
563 }
564 continue;
565 }
566 if self.text[self.pos..].starts_with("/*") {
567 self.advance();
568 self.advance();
569 while self.peek().is_some() && !self.text[self.pos..].starts_with("*/") {
570 self.advance();
571 }
572 if self.text[self.pos..].starts_with("*/") {
573 self.advance();
574 self.advance();
575 }
576 continue;
577 }
578 break;
579 }
580 }
581
582 fn error(&self, code: &str, message: String) -> OpyError {
583 OpyError::at(
584 code,
585 message,
586 Span::new(self.file, self.here(), self.here()),
587 )
588 }
589
590 fn error_at(&self, code: &str, message: String, span: Span) -> OpyError {
591 OpyError::at(code, message, span)
592 }
593
594 fn parse_object(&mut self) -> OpyResult<(Vec<cst::SettingsNode>, Span)> {
595 let open = self.here();
596 if self.advance() != Some('{') {
597 return Err(self.error(
598 "settings-invalid",
599 "settings block must be a JSONC object".to_string(),
600 ));
601 }
602 let members = self.parse_members(false)?;
603 let span = Span::new(self.file, open, self.here());
604 Ok((members, span))
605 }
606
607 fn parse_members(&mut self, root: bool) -> OpyResult<Vec<cst::SettingsNode>> {
611 let mut nodes = Vec::new();
612 let mut names = Vec::new();
613 self.skip_whitespace();
614 if (!root && self.peek() == Some('}')) || (root && self.pos >= self.text.len()) {
615 if !root {
616 self.advance();
617 }
618 return Ok(nodes);
619 }
620 loop {
621 self.skip_whitespace();
622 let key_start = self.here();
623 let key = match self.parse_string_value() {
624 Some(value) => value,
625 None => {
626 return Err(self.error(
627 "settings-invalid",
628 "settings keys must be quoted strings".to_string(),
629 ));
630 }
631 };
632 let key_span = Span::new(self.file, key_start, self.here());
633 if names.contains(&key) {
634 return Err(self.error_at(
635 "settings-invalid",
636 format!("duplicate settings key '{key}'"),
637 key_span,
638 ));
639 }
640 names.push(key.clone());
641 self.skip_whitespace();
642 if self.advance() != Some(':') {
643 return Err(self.error_at(
644 "settings-invalid",
645 format!("expected ':' after settings key '{key}'"),
646 key_span,
647 ));
648 }
649 self.skip_whitespace();
650 let (node, value_end) = self.parse_value()?;
651 let node = build_node(key, node, value_end, key_start, self.file);
652 nodes.push(node);
653 self.skip_whitespace();
654 match self.peek() {
655 Some(',') => {
656 self.advance();
657 self.skip_whitespace();
658 if (!root && self.peek() == Some('}')) || (root && self.pos >= self.text.len())
659 {
660 if !root {
661 self.advance();
662 }
663 return Ok(nodes);
664 }
665 }
666 Some('}') if !root => {
667 self.advance();
668 return Ok(nodes);
669 }
670 None if root => return Ok(nodes),
671 _ => {
672 return Err(self.error(
673 "settings-invalid",
674 "expected ',' or '}' in settings object".to_string(),
675 ));
676 }
677 }
678 }
679 }
680
681 fn parse_value(&mut self) -> OpyResult<(cst::SettingsNode, Position)> {
684 let start = self.here();
685 let ch = self.peek();
686 let node = match ch {
687 Some('"') | Some('\'') => {
688 let saved = (self.pos, self.line, self.col);
689 let value = self.parse_string_expression().ok_or_else(|| {
690 self.error(
691 "settings-invalid",
692 "unterminated string in settings value".to_string(),
693 )
694 })?;
695 self.skip_inline_whitespace();
696 if self.is_expression_continuation() {
697 self.pos = saved.0;
698 self.line = saved.1;
699 self.col = saved.2;
700 cst::SettingsNode::Raw {
701 name: String::new(),
702 value: self.parse_expression_value(),
703 span: Span::new(self.file, start, self.here()),
704 }
705 } else {
706 cst::SettingsNode::String {
707 name: String::new(),
708 value,
709 span: Span::new(self.file, start, self.here()),
710 }
711 }
712 }
713 Some('t') => {
714 let saved = (self.pos, self.line, self.col);
715 self.expect_word("true")?;
716 self.skip_inline_whitespace();
717 if self.is_expression_continuation() {
718 self.pos = saved.0;
719 self.line = saved.1;
720 self.col = saved.2;
721 cst::SettingsNode::Raw {
722 name: String::new(),
723 value: self.parse_expression_value(),
724 span: Span::new(self.file, start, self.here()),
725 }
726 } else {
727 cst::SettingsNode::Bool {
728 name: String::new(),
729 value: true,
730 span: Span::new(self.file, start, self.here()),
731 }
732 }
733 }
734 Some('f') => {
735 let saved = (self.pos, self.line, self.col);
736 self.expect_word("false")?;
737 self.skip_inline_whitespace();
738 if self.is_expression_continuation() {
739 self.pos = saved.0;
740 self.line = saved.1;
741 self.col = saved.2;
742 cst::SettingsNode::Raw {
743 name: String::new(),
744 value: self.parse_expression_value(),
745 span: Span::new(self.file, start, self.here()),
746 }
747 } else {
748 cst::SettingsNode::Bool {
749 name: String::new(),
750 value: false,
751 span: Span::new(self.file, start, self.here()),
752 }
753 }
754 }
755 Some(c) if c.is_ascii_digit() || c == '-' => {
756 let saved = (self.pos, self.line, self.col);
757 let value = self.parse_number()?;
758 self.skip_inline_whitespace();
759 if self.is_expression_continuation() {
760 self.pos = saved.0;
761 self.line = saved.1;
762 self.col = saved.2;
763 let value = self.parse_expression_value();
764 cst::SettingsNode::Raw {
765 name: String::new(),
766 value,
767 span: Span::new(self.file, start, self.here()),
768 }
769 } else {
770 cst::SettingsNode::Number {
771 name: String::new(),
772 value,
773 span: Span::new(self.file, start, self.here()),
774 }
775 }
776 }
777 Some('[') => {
778 let elements = self.parse_list()?;
779 cst::SettingsNode::List {
780 name: String::new(),
781 elements,
782 span: Span::new(self.file, start, self.here()),
783 }
784 }
785 Some('{') => {
786 let (children, _) = self.parse_object()?;
787 cst::SettingsNode::Group {
788 name: String::new(),
789 children,
790 span: Span::new(self.file, start, self.here()),
791 }
792 }
793 _ => {
794 let value = self.parse_expression_value();
795 if value.trim().is_empty() {
796 return Err(self.error(
797 "settings-invalid",
798 "expected a value in settings block".to_string(),
799 ));
800 }
801 cst::SettingsNode::Raw {
802 name: String::new(),
803 value,
804 span: Span::new(self.file, start, self.here()),
805 }
806 }
807 };
808 let end = self.here();
809 Ok((node, end))
810 }
811
812 fn parse_expression_value(&mut self) -> String {
816 let mut value = String::new();
817 let mut depth = 0usize;
818 let mut quote = None;
819 while let Some(ch) = self.peek() {
820 if let Some(expected) = quote {
821 value.push(self.advance().expect("peeked character exists"));
822 if ch == expected {
823 quote = None;
824 }
825 continue;
826 }
827 match ch {
828 '"' | '\'' => {
829 quote = Some(ch);
830 value.push(self.advance().expect("peeked character exists"));
831 }
832 '(' | '[' | '{' => {
833 depth += 1;
834 value.push(self.advance().expect("peeked character exists"));
835 }
836 ']' if depth == 0 => break,
837 ')' | ']' => {
838 depth = depth.saturating_sub(1);
839 value.push(self.advance().expect("peeked character exists"));
840 }
841 ',' | '}' if depth == 0 => break,
842 _ => value.push(self.advance().expect("peeked character exists")),
843 }
844 }
845 value.trim().to_string()
846 }
847
848 fn expect_word(&mut self, word: &str) -> OpyResult<()> {
849 let start = self.here();
850 for expected in word.chars() {
851 if self.advance() != Some(expected) {
852 return Err(self.error_at(
853 "settings-invalid",
854 format!("expected '{word}' in settings block"),
855 Span::new(self.file, start, self.here()),
856 ));
857 }
858 }
859 Ok(())
860 }
861
862 fn parse_number(&mut self) -> OpyResult<f64> {
863 let start = self.here();
864 let mut text = String::new();
865 if self.peek() == Some('-') {
866 text.push(self.advance().unwrap());
867 }
868 while let Some(c) = self.peek() {
869 if c.is_ascii_digit() {
870 text.push(self.advance().unwrap());
871 } else {
872 break;
873 }
874 }
875 if self.peek() == Some('.') {
876 text.push(self.advance().unwrap());
877 while let Some(c) = self.peek() {
878 if c.is_ascii_digit() {
879 text.push(self.advance().unwrap());
880 } else {
881 break;
882 }
883 }
884 }
885 text.parse::<f64>().map_err(|_| {
886 self.error_at(
887 "settings-invalid",
888 format!("invalid number '{text}' in settings block"),
889 Span::new(self.file, start, self.here()),
890 )
891 })
892 }
893
894 fn parse_list(&mut self) -> OpyResult<Vec<cst::SettingsListElement>> {
895 self.advance(); let mut elements = Vec::new();
897 self.skip_whitespace();
898 if self.peek() == Some(']') {
899 self.advance();
900 return Ok(elements);
901 }
902 loop {
903 self.skip_whitespace();
904 let start = self.here();
905 let value = match self.peek() {
906 Some('"') | Some('\'') => self.parse_string_expression().ok_or_else(|| {
907 self.error(
908 "settings-invalid",
909 "unterminated string in settings list".to_string(),
910 )
911 })?,
912 Some(']') | Some(',') | None => {
913 return Err(self.error(
914 "settings-invalid",
915 "settings list elements must not be empty".to_string(),
916 ));
917 }
918 _ => self.parse_expression_value(),
919 };
920 let span = Span::new(self.file, start, self.here());
921 elements.push(cst::SettingsListElement { value, span });
922 self.skip_whitespace();
923 match self.peek() {
924 Some(',') => {
925 self.advance();
926 self.skip_whitespace();
927 if self.peek() == Some(']') {
928 self.advance();
929 return Ok(elements);
930 }
931 }
932 Some(']') => {
933 self.advance();
934 return Ok(elements);
935 }
936 _ => {
937 return Err(self.error(
938 "settings-invalid",
939 "expected ',' or ']' in settings list".to_string(),
940 ));
941 }
942 }
943 }
944 }
945
946 fn parse_string_value(&mut self) -> Option<String> {
949 let quote = self.peek()?;
950 if quote != '"' && quote != '\'' {
951 return None;
952 }
953 self.advance();
954 let mut value = String::new();
955 loop {
956 let ch = self.advance()?;
957 if ch == '\n' {
958 return None;
959 }
960 if ch == quote {
961 return Some(value);
962 }
963 if ch == '\\' {
964 let escaped = self.advance()?;
965 match escaped {
966 'n' => value.push('\n'),
967 't' => value.push('\t'),
968 'r' => value.push('\r'),
969 other => value.push(other),
970 }
971 } else {
972 value.push(ch);
973 }
974 }
975 }
976
977 fn parse_string_expression(&mut self) -> Option<String> {
981 let mut value = self.parse_string_value()?;
982 loop {
983 let saved = (self.pos, self.line, self.col);
984 self.skip_whitespace();
985 if self.peek() != Some('"') && self.peek() != Some('\'') {
986 self.pos = saved.0;
987 self.line = saved.1;
988 self.col = saved.2;
989 break;
990 }
991 let next = self.parse_string_value()?;
992 value.push_str(&next);
993 }
994 Some(value)
995 }
996
997 fn skip_inline_whitespace(&mut self) {
998 while matches!(self.peek(), Some(' ' | '\t' | '\r')) {
999 self.advance();
1000 }
1001 }
1002
1003 fn is_expression_continuation(&self) -> bool {
1004 if matches!(
1005 self.peek(),
1006 Some('+' | '-' | '*' | '/' | '%' | '<' | '>' | '=')
1007 ) {
1008 return true;
1009 }
1010 ["and", "or", "if"].iter().any(|word| {
1011 self.text[self.pos..]
1012 .strip_prefix(word)
1013 .is_some_and(|rest| {
1014 rest.chars().next().is_none_or(|character| {
1015 !character.is_ascii_alphanumeric() && character != '_'
1016 })
1017 })
1018 })
1019 }
1020}
1021
1022fn build_node(
1024 key: String,
1025 node: cst::SettingsNode,
1026 value_end: Position,
1027 key_start: Position,
1028 file: u32,
1029) -> cst::SettingsNode {
1030 let span = Span::new(file, key_start, value_end);
1031 match node {
1032 cst::SettingsNode::Group { children, .. } => cst::SettingsNode::Group {
1033 name: key,
1034 children,
1035 span,
1036 },
1037 cst::SettingsNode::Number { value, .. } => cst::SettingsNode::Number {
1038 name: key,
1039 value,
1040 span,
1041 },
1042 cst::SettingsNode::Bool { value, .. } => cst::SettingsNode::Bool {
1043 name: key,
1044 value,
1045 span,
1046 },
1047 cst::SettingsNode::String { value, .. } => cst::SettingsNode::String {
1048 name: key,
1049 value,
1050 span,
1051 },
1052 cst::SettingsNode::Raw { value, .. } => cst::SettingsNode::Raw {
1053 name: key,
1054 value,
1055 span,
1056 },
1057 cst::SettingsNode::List { elements, .. } => cst::SettingsNode::List {
1058 name: key,
1059 elements,
1060 span,
1061 },
1062 }
1063}
1064
1065fn is_ident_start(c: char) -> bool {
1066 c.is_ascii_alphabetic() || c == '_'
1067}
1068
1069fn is_ident_continue(c: char) -> bool {
1070 c.is_ascii_alphanumeric() || c == '_'
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075 use super::*;
1076
1077 fn block(text: &str) -> SettingsBlock {
1078 let mut blocks = find_blocks(text, 0).unwrap();
1079 assert_eq!(blocks.len(), 1, "one block expected in: {text}");
1080 blocks.pop().unwrap()
1081 }
1082
1083 #[test]
1084 fn finds_block_after_comments_and_blanks() {
1085 let text = "/* header */\n# comment\n\nsettings {\n \"gamemodes\": {}\n}\nrule \"r\":\n";
1086 let found = block(text);
1087 assert!(found.text.contains("gamemodes"));
1088 assert_eq!(found.keyword_span.start.line, 4);
1089 assert_eq!(found.span.end.line, 6);
1090 }
1091
1092 #[test]
1093 fn no_blocks_for_plain_program() {
1094 assert!(
1095 find_blocks("rule \"r\":\n pass\n", 0)
1096 .unwrap()
1097 .is_empty()
1098 );
1099 }
1100
1101 #[test]
1102 fn ignores_settings_words_in_strings_and_indented_code() {
1103 let text = "rule \"settings\":\n x = \"settings\"\n";
1104 assert!(find_blocks(text, 0).unwrap().is_empty());
1105 }
1106
1107 #[test]
1108 fn settings_not_first_construct_is_placement_error() {
1109 let error = find_blocks("rule \"r\":\n pass\nsettings {\n}\n", 0).unwrap_err();
1110 assert_eq!(error.code, "settings-placement");
1111 assert_eq!(error.span.unwrap().start.line, 3);
1112 }
1113
1114 #[test]
1115 fn second_block_is_placement_error() {
1116 let error =
1117 find_blocks("settings {\n \"gamemodes\": {}\n}\nsettings {\n}\n", 0).unwrap_err();
1118 assert_eq!(error.code, "settings-placement");
1119 assert_eq!(error.span.unwrap().start.line, 4);
1120 }
1121
1122 #[test]
1123 fn settings_file_form_is_invalid() {
1124 let error = find_blocks("settings \"file.opy\"\n", 0).unwrap_err();
1125 assert_eq!(error.code, "settings-invalid");
1126 }
1127
1128 #[test]
1129 fn unterminated_block_is_invalid() {
1130 let error = find_blocks("settings {\n \"gamemodes\": {\n", 0).unwrap_err();
1131 assert_eq!(error.code, "settings-invalid");
1132 }
1133
1134 #[test]
1135 fn braces_inside_strings_do_not_unbalance() {
1136 let found =
1137 block("settings {\n \"description\": \"a { b }\",\n \"gamemodes\": {}\n}\n");
1138 assert!(found.text.contains("a { b }"));
1139 }
1140
1141 #[test]
1142 fn sanitize_preserves_post_block_positions() {
1143 let text = "settings {\n \"gamemodes\": {}\n}\nrule \"r\":\n pass\n";
1144 let found = block(text);
1145 let sanitized = sanitize_for_lex(text, &found);
1146 let lines: Vec<&str> = sanitized.lines().collect();
1147 assert_eq!(lines.len(), 5);
1148 assert_eq!(lines[3], "rule \"r\":");
1149 assert_eq!(lines[4], " pass");
1150 assert_eq!(sanitized.find("rule"), text.find("rule"));
1152 }
1153
1154 #[test]
1155 fn non_object_after_settings_is_invalid() {
1156 let error = find_blocks("settings [1, 2]\n", 0).unwrap_err();
1158 assert_eq!(error.code, "settings-invalid");
1159 }
1160
1161 #[test]
1162 fn parse_block_rejects_missing_gamemodes() {
1163 let found = block("settings {\n \"main\": { \"description\": \"x\" }\n}\n");
1164 let error = parse_block(&found).unwrap_err();
1165 assert_eq!(error.code, "settings-invalid");
1166 assert!(error.message.contains("gamemodes"));
1167 }
1168
1169 #[test]
1170 fn parse_block_rejects_duplicate_keys() {
1171 let found = block("settings {\n \"gamemodes\": {},\n \"gamemodes\": {}\n}\n");
1172 let error = parse_block(&found).unwrap_err();
1173 assert_eq!(error.code, "settings-invalid");
1174 assert!(error.message.contains("duplicate"));
1175 }
1176
1177 #[test]
1178 fn parse_block_accepts_trailing_commas() {
1179 let found = block(
1180 "settings {\n \"gamemodes\": {\n \"general\": {\n \"heroLimit\": \"off\",\n },\n },\n}\n",
1181 );
1182 let parsed = parse_block(&found).unwrap();
1183 assert_eq!(parsed.children.len(), 1);
1184 }
1185
1186 #[test]
1187 fn parse_block_handles_escapes_and_quotes() {
1188 let found = block(
1189 "settings {\n \"main\": { \"description\": \"line\\n\\t\\\"quoted\\\"\" },\n \"gamemodes\": {}\n}\n",
1190 );
1191 let parsed = parse_block(&found).unwrap();
1192 let cst::SettingsNode::Group { children, .. } = &parsed.children[0] else {
1193 panic!("main group");
1194 };
1195 let cst::SettingsNode::String { value, .. } = &children[0] else {
1196 panic!("description");
1197 };
1198 assert_eq!(value, "line\n\t\"quoted\"");
1199 }
1200
1201 #[test]
1202 fn parse_block_accepts_comments_and_expression_values() {
1203 let found = block(
1204 "settings {\n # keep the source expression intact\n \"lobby\": {\n \"modeName\": GAMEMODE_NAME\" \"GAMEMODE_VERSION,\n },\n \"gamemodes\": {}\n}\n",
1205 );
1206 let parsed = parse_block(&found).unwrap();
1207 let cst::SettingsNode::Group { children, .. } = &parsed.children[0] else {
1208 panic!("lobby group");
1209 };
1210 let cst::SettingsNode::Raw { value, .. } = &children[0] else {
1211 panic!("expression-valued setting");
1212 };
1213 assert_eq!(value, "GAMEMODE_NAME\" \"GAMEMODE_VERSION");
1214 }
1215
1216 #[test]
1217 fn parse_block_types_values() {
1218 let found = block(
1219 "settings {\n \"lobby\": { \"ffaSlots\": 6 },\n \"gamemodes\": { \"general\": { \"enableRandomHeroes\": true, \"respawnTime%\": 30, \"heroLimit\": \"off\" } },\n \"heroes\": { \"allTeams\": { \"enabledHeroes\": [\"mei\"] } }\n}\n",
1220 );
1221 let parsed = parse_block(&found).unwrap();
1222 let lobby = match &parsed.children[0] {
1223 cst::SettingsNode::Group { name, children, .. } => {
1224 assert_eq!(name, "lobby");
1225 children
1226 }
1227 other => panic!("{other:?}"),
1228 };
1229 assert!(matches!(
1230 lobby[0],
1231 cst::SettingsNode::Number { value: 6.0, .. }
1232 ));
1233 }
1234
1235 #[test]
1236 fn spans_are_computed_from_block_base() {
1237 let text = "settings {\n \"lobby\": {\n \"ffaSlots\": 6\n },\n \"gamemodes\": {}\n}\n";
1238 let found = block(text);
1239 let parsed = parse_block(&found).unwrap();
1240 let cst::SettingsNode::Group { children, .. } = &parsed.children[0] else {
1241 panic!("lobby");
1242 };
1243 let cst::SettingsNode::Number { span, .. } = &children[0] else {
1244 panic!("ffaSlots");
1245 };
1246 assert_eq!(span.start.line, 3);
1247 assert_eq!(span.start.col, 9);
1248 }
1249
1250 #[test]
1251 fn keyword_span_carries_the_file_id() {
1252 let found = block("settings {\n \"gamemodes\": {}\n}\n");
1253 assert_eq!(found.keyword_span.file, 0);
1254 assert_eq!(found.keyword_span.start.col, 1);
1255 assert_eq!(found.keyword_span.end.col, 9);
1256 }
1257}