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