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