1mod lexer;
23
24use lexer::ESCAPED_DOLLAR;
25pub use lexer::{Lexed, Lexer, Token, TokenKind};
26
27fn span_of(tok: &Token) -> Span {
29 Span {
30 line: tok.line,
31 column: tok.col,
32 end_line: tok.end_line,
33 end_column: tok.end_col,
34 }
35}
36
37fn parse_annotation_string(payload: &str) -> Result<String, String> {
43 let mut chars = payload.chars().peekable();
44 let quote = chars.next().ok_or("expected a string literal")?;
45 if quote != '"' && quote != '\'' {
46 return Err("expected a string literal".to_string());
47 }
48 let mut out = String::new();
49 let mut escaped = false;
50 for c in chars.by_ref() {
51 if escaped {
52 match (quote, c) {
53 ('"', 'n') => out.push('\n'),
54 ('"', 't') => out.push('\t'),
55 ('"', 'r') => out.push('\r'),
56 ('"', '0') => out.push('\0'),
57 ('"', '$') => out.push('$'),
58 (_, '\\') => out.push('\\'),
59 ('"', '"') => out.push('"'),
60 ('\'', '\'') => out.push('\''),
61 _ => {
62 out.push('\\');
63 out.push(c);
64 }
65 }
66 escaped = false;
67 } else if c == '\\' {
68 escaped = true;
69 } else if c == quote {
70 for rest in chars.by_ref() {
72 if !rest.is_whitespace() {
73 return Err("unexpected text after the string literal".to_string());
74 }
75 }
76 return Ok(out);
77 } else {
78 out.push(c);
79 }
80 }
81 Err("unterminated string literal".to_string())
82}
83
84const MAX_EXPR_DEPTH: usize = 64;
89
90const MAX_BLOCK_DEPTH: usize = 64;
93
94const MAX_SCHEMA_DEPTH: usize = 64;
98
99use crate::diag::{thresholds, Diagnostic, Repair, RepairKind, Severity, Span};
100use crate::ir::{NodeId, SchemaField, SemanticTag, *};
101use crate::types::{ActiveType, Unit};
102use indexmap::IndexMap;
103
104#[derive(Debug, Clone, Copy)]
106pub struct ParseOptions {
107 pub resilient: bool,
111 pub enable_includes: bool,
115 pub resolve_includes: bool,
119 pub allow_parent_escapes: bool,
122}
123
124impl Default for ParseOptions {
125 fn default() -> Self {
126 Self {
127 resilient: false,
128 enable_includes: false,
129 resolve_includes: true,
130 allow_parent_escapes: false,
131 }
132 }
133}
134
135impl ParseOptions {
136 pub const fn strict() -> Self {
138 Self {
139 resilient: false,
140 enable_includes: false,
141 resolve_includes: true,
142 allow_parent_escapes: false,
143 }
144 }
145
146 pub const fn resilient_mode() -> Self {
148 Self {
149 resilient: true,
150 enable_includes: false,
151 resolve_includes: true,
152 allow_parent_escapes: false,
153 }
154 }
155}
156
157pub fn parse(source: &str, name: &str) -> Result<Module, Vec<Diagnostic>> {
158 parse_with(source, name, ParseOptions::default())
159}
160
161pub fn parse_with(
162 source: &str,
163 name: &str,
164 options: ParseOptions,
165) -> Result<Module, Vec<Diagnostic>> {
166 let (module, errors) = parse_recovering(source, name, options);
167 if errors.is_empty() {
168 Ok(module)
169 } else {
170 Err(errors)
171 }
172}
173
174pub fn parse_text_with_file(
182 source: &str,
183 name: &str,
184 options: ParseOptions,
185 path: &std::path::Path,
186) -> Result<Module, Vec<Diagnostic>> {
187 let mut options = options;
188 options.enable_includes = true;
189 let (module, errors) = parse_recovering_impl(source, name, options, Some(path), None);
190 if errors.is_empty() {
191 Ok(module)
192 } else {
193 Err(errors)
194 }
195}
196
197pub fn parse_text_recovering_with_file(
203 source: &str,
204 name: &str,
205 options: ParseOptions,
206 path: &std::path::Path,
207) -> (Module, Vec<Diagnostic>) {
208 let mut options = options;
209 options.enable_includes = true;
210 parse_recovering_impl(source, name, options, Some(path), None)
211}
212
213pub fn parse_file(
218 path: &std::path::Path,
219 options: ParseOptions,
220) -> Result<Module, Vec<Diagnostic>> {
221 let src = std::fs::read_to_string(path).map_err(|e| {
222 vec![Diagnostic {
223 severity: Severity::Error,
224 code: "E017".into(),
225 message: format!("cannot read file {}: {e}", path.display()),
226 path: None,
227 span: None,
228 repair: None,
229 confidence: 1.0,
230 file: Some(path.to_string_lossy().into_owned()),
231 }]
232 })?;
233 let mut options = options;
234 options.enable_includes = true;
235 let (module, errors) =
236 parse_recovering_impl(&src, &path.to_string_lossy(), options, Some(path), None);
237 if errors.is_empty() {
238 Ok(module)
239 } else {
240 Err(errors)
241 }
242}
243
244pub fn parse_file_with_trust(
250 path: &std::path::Path,
251 options: ParseOptions,
252 trusted: &crate::sign::TrustedKeys,
253) -> Result<Module, Vec<Diagnostic>> {
254 let src = std::fs::read_to_string(path).map_err(|e| {
255 vec![Diagnostic {
256 severity: Severity::Error,
257 code: "E017".into(),
258 message: format!("cannot read file {}: {e}", path.display()),
259 path: None,
260 span: None,
261 repair: None,
262 confidence: 1.0,
263 file: Some(path.to_string_lossy().into_owned()),
264 }]
265 })?;
266 let mut options = options;
267 options.enable_includes = true;
268 let (module, errors) = parse_recovering_impl(
269 &src,
270 &path.to_string_lossy(),
271 options,
272 Some(path),
273 Some(trusted.clone()),
274 );
275 if errors.is_empty() {
276 Ok(module)
277 } else {
278 Err(errors)
279 }
280}
281
282pub fn parse_recovering(
288 source: &str,
289 name: &str,
290 options: ParseOptions,
291) -> (Module, Vec<Diagnostic>) {
292 parse_recovering_impl(source, name, options, None, None)
293}
294
295fn parse_recovering_impl(
296 source: &str,
297 name: &str,
298 options: ParseOptions,
299 root_file: Option<&std::path::Path>,
300 trust: Option<crate::sign::TrustedKeys>,
301) -> (Module, Vec<Diagnostic>) {
302 let mut lexer = Lexer::new(source);
303 let mut lexed = lexer.tokenize();
304 let n_comments = lexed.comments.len();
305 let root_stamp = root_file.map(|p| p.to_string_lossy().into_owned());
309 let mut parser = Parser {
310 tokens: lexed.tokens,
311 pos: 0,
312 module: Module::new(name),
313 errors: Vec::new(),
314 ident_scopes: Vec::new(),
315 depth: 0,
316 options,
317 block_depth: 0,
318 schema_depth: 0,
319 comments: lexed.comments,
320 comment_files: vec![root_stamp; n_comments],
321 statements: Vec::new(),
322 blocks: Vec::new(),
323 current_path: Vec::new(),
324 brace_tables: 0,
325 indent_tables: 0,
326 current_file: root_file.map(|p| p.to_path_buf()),
327 include_stack: Vec::new(),
328 in_include: false,
329 in_profile: false,
330 array_depth: 0,
331 binding_spliced: std::collections::HashSet::new(),
332 pending_profiles: Vec::new(),
333 trust,
334 };
335 let lex_file = parser.current_file_name();
338 for d in lexed.errors.iter_mut().chain(lexed.repairs.iter_mut()) {
339 if d.file.is_none() {
340 d.file = lex_file.clone();
341 }
342 }
343 parser.module.healing.extend(lexed.repairs);
344 parser.errors.extend(lexed.errors);
345 parser.parse_document();
346 parser.extract_schemas();
350 parser.extract_depends_on();
352 parser.attach_trivia();
353 parser.module.source_style = if parser.indent_tables > parser.brace_tables {
354 Style::Indent
355 } else {
356 Style::Brace
357 };
358 parser.build_graph();
359 (parser.module, parser.errors)
360}
361
362impl Parser {
365 fn attach_trivia(&mut self) {
366 let comments = std::mem::take(&mut self.comments);
367 let comment_files = std::mem::take(&mut self.comment_files);
370 let statements = std::mem::take(&mut self.statements);
371 let blocks = std::mem::take(&mut self.blocks);
372
373 let mut trivia = Vec::with_capacity(comments.len());
374 for ((text, span), file) in comments.into_iter().zip(comment_files) {
375 let attachment = self.attach_one(&span, &file, &statements, &blocks);
376 trivia.push(TriviaComment {
377 text,
378 span,
379 attachment,
380 file,
381 });
382 }
383 self.module.trivia = trivia;
384 }
385
386 fn attach_one(
387 &self,
388 span: &Span,
389 file: &Option<String>,
390 stmts: &[StmtInfo],
391 blocks: &[BlockInfo],
392 ) -> Attachment {
393 let line = span.line;
394 let level = span.column.saturating_sub(1) as usize;
395
396 if let Some(stmt) = stmts
401 .iter()
402 .filter(|s| &s.file == file && (s.start_line == line || s.end_line == line))
403 .max_by_key(|s| s.start_line)
404 {
405 return Attachment::Trailing(qualified(&stmt.path, &stmt.key));
406 }
407
408 let owner = self.owning_block(line, level, file, stmts, blocks);
412 if let Some(b) = &owner {
413 if let Some(stmt) = stmts
416 .iter()
417 .filter(|s| &s.file == file && s.start_line > line && s.path == b.path)
418 .min_by_key(|s| s.start_line)
419 {
420 return Attachment::Leading(qualified(&stmt.path, &stmt.key));
421 }
422 return Attachment::TrailingBlock(b.path.clone());
424 }
425
426 if let Some(stmt) = stmts
428 .iter()
429 .filter(|s| &s.file == file && s.start_line > line && s.path.is_empty())
430 .min_by_key(|s| s.start_line)
431 {
432 return Attachment::Leading(qualified(&stmt.path, &stmt.key));
433 }
434 Attachment::TrailingDocument
435 }
436
437 fn owning_block(
439 &self,
440 line: u32,
441 level: usize,
442 file: &Option<String>,
443 stmts: &[StmtInfo],
444 blocks: &[BlockInfo],
445 ) -> Option<BlockInfo> {
446 fn score(b: &BlockInfo, level: usize) -> (u8, usize, u32) {
449 (if b.level == level { 1 } else { 0 }, b.level, b.start_line)
450 }
451 let mut best: Option<&BlockInfo> = None;
452 for b in blocks {
453 if &b.file != file || b.start_line > line {
454 continue;
455 }
456 let next_sibling = stmts
459 .iter()
460 .filter(|s| &s.file == file && s.start_line > line && s.level <= b.level)
461 .min_by_key(|s| s.start_line)
462 .map(|s| s.start_line)
463 .unwrap_or(u32::MAX);
464 if line >= next_sibling {
465 continue;
466 }
467 let better = match best {
468 None => true,
469 Some(prev) => score(b, level) >= score(prev, level),
470 };
471 if better {
472 best = Some(b);
473 }
474 }
475 best.cloned()
476 }
477}
478
479fn qualified(path: &str, key: &str) -> String {
482 if path.is_empty() {
483 key.to_string()
484 } else {
485 format!("{path}.{key}")
486 }
487}
488
489pub fn validate_patch_path(path: &str) -> Result<Vec<PathSeg>, String> {
496 fn is_key(s: &str) -> bool {
497 let mut chars = s.chars();
498 match chars.next() {
499 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
500 _ => return false,
501 }
502 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
503 }
504 if path.is_empty() {
505 return Err("empty path".into());
506 }
507 let mut segs = Vec::new();
508 for part in path.split('.') {
509 if part.is_empty() {
510 return Err("empty segment".into());
511 }
512 let mut rest = part;
515 let key_end = rest.find('[').unwrap_or(rest.len());
516 let key = &rest[..key_end];
517 if !is_key(key) {
518 return Err(format!("invalid key `{key}` in segment `{part}`"));
519 }
520 segs.push(PathSeg::Key(key.to_string()));
521 rest = &rest[key_end..];
522 while !rest.is_empty() {
523 if !rest.starts_with('[') {
524 return Err(format!("unexpected text in segment `{part}`"));
525 }
526 let close = rest
527 .find(']')
528 .ok_or_else(|| format!("unclosed bracket in segment `{part}`"))?;
529 let idx: usize = rest[1..close]
530 .parse()
531 .map_err(|_| format!("array index must be a non-negative integer in `{part}`"))?;
532 segs.push(PathSeg::Index(idx));
533 rest = &rest[close + 1..];
534 }
535 }
536 Ok(segs)
537}
538
539#[derive(Debug, Clone, PartialEq, Eq)]
542pub enum PathSeg {
543 Key(String),
544 Index(usize),
545}
546struct Parser {
547 tokens: Vec<Token>,
548 pos: usize,
549 module: Module,
550 errors: Vec<Diagnostic>,
551 ident_scopes: Vec<Vec<(String, NodeId)>>,
556 depth: usize,
558 options: ParseOptions,
560 block_depth: usize,
562 schema_depth: usize,
566 comments: Vec<(String, Span)>,
568 comment_files: Vec<Option<String>>,
573 statements: Vec<StmtInfo>,
575 blocks: Vec<BlockInfo>,
577 current_path: Vec<String>,
579 brace_tables: usize,
581 indent_tables: usize,
582 current_file: Option<std::path::PathBuf>,
585 include_stack: Vec<std::path::PathBuf>,
587 in_include: bool,
590 in_profile: bool,
593 array_depth: usize,
598 binding_spliced: std::collections::HashSet<String>,
602 pending_profiles: Vec<ProfileScope>,
606 trust: Option<crate::sign::TrustedKeys>,
610}
611
612impl Parser {
613 fn path_str(&self) -> String {
615 self.current_path.join(".")
616 }
617
618 fn last_end_line(&self) -> u32 {
620 self.tokens
621 .get(self.pos.saturating_sub(1))
622 .map(|t| t.end_line)
623 .unwrap_or(1)
624 }
625
626 fn record_stmt(&mut self, key: &str, start_line: u32, end_line: u32, level: usize) {
628 self.statements.push(StmtInfo {
629 key: key.to_string(),
630 path: self.path_str(),
631 start_line,
632 end_line,
633 level,
634 file: self.current_file_name(),
635 });
636 }
637}
638
639#[derive(Debug, Clone)]
641struct StmtInfo {
642 key: String,
643 path: String,
644 start_line: u32,
645 end_line: u32,
646 level: usize,
647 file: Option<String>,
650}
651
652#[derive(Debug, Clone)]
654struct BlockInfo {
655 path: String,
656 level: usize,
657 start_line: u32,
658 file: Option<String>,
660}
661
662#[derive(Debug, Clone)]
665struct PendingInclude {
666 path: String,
667 alias: Option<String>,
668 private: bool,
669 tok: Token,
670 patch_mark: usize,
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678enum SchemaEnd {
679 Brace,
681 Indent,
683}
684
685type ProfileScope = (Vec<(String, NodeId)>, indexmap::IndexMap<String, NodeId>);
688
689#[derive(Debug, Clone, Copy, PartialEq, Eq)]
691enum BodyEnd {
692 Document,
694 Block(usize),
697 Brace,
699 HeaderBody(usize),
702}
703
704fn indent_level(tok: &Token) -> usize {
706 tok.text.parse().unwrap_or(0)
707}
708
709impl Parser {
710 fn peek(&self) -> &Token {
715 self.tokens.get(self.pos).unwrap_or_else(|| {
719 self.tokens
720 .last()
721 .expect("token stream is never empty; the lexer appends Eof")
722 })
723 }
724
725 fn peek_kind(&self) -> &TokenKind {
726 &self.peek().kind
727 }
728
729 fn bump(&mut self) -> Token {
730 let t = self.peek().clone();
731 if self.pos < self.tokens.len() {
732 self.pos += 1;
733 }
734 t
735 }
736
737 fn expect(&mut self, kind: TokenKind) -> Option<Token> {
738 if std::mem::discriminant(self.peek_kind()) == std::mem::discriminant(&kind) {
740 Some(self.bump())
741 } else {
742 self.error(
743 "E001",
744 format!("expected {:?}, found {:?}", kind, self.peek_kind()),
745 );
746 None
747 }
748 }
749
750 fn error(&mut self, code: &str, message: impl Into<String>) {
751 let t = self.peek();
752 self.errors.push(Diagnostic {
753 severity: Severity::Error,
754 code: code.into(),
755 message: message.into(),
756 path: None,
757 span: Some(Span {
758 line: t.line,
759 column: t.col,
760 end_line: t.end_line,
761 end_column: t.end_col,
762 }),
763 repair: None,
764 confidence: 1.0,
765 file: self.current_file_name(),
766 });
767 }
768
769 fn skip_newlines(&mut self) {
770 while matches!(self.peek_kind(), TokenKind::Newline) {
771 self.bump();
772 }
773 }
774
775 fn peek_kind_at(&self, n: usize) -> TokenKind {
778 self.peek_at(n)
779 .map(|t| t.kind.clone())
780 .unwrap_or(TokenKind::Eof)
781 }
782
783 fn peek_at(&self, n: usize) -> Option<Token> {
784 self.tokens.get(self.pos + n).cloned()
785 }
786
787 fn parse_body(
793 &mut self,
794 fields: &mut indexmap::IndexMap<String, NodeId>,
795 end: BodyEnd,
796 pending: &mut Vec<PendingInclude>,
797 ) {
798 self.block_depth += 1;
799 if self.block_depth > MAX_BLOCK_DEPTH {
800 self.error(
801 "E011",
802 format!("maximum block nesting depth ({MAX_BLOCK_DEPTH}) exceeded"),
803 );
804 self.block_depth -= 1;
805 self.recover_block(end);
806 return;
807 }
808 self.parse_body_inner(fields, end, pending);
809 self.block_depth -= 1;
810 }
811
812 fn recover_block(&mut self, end: BodyEnd) {
817 let mut open_braces = 0usize;
818 loop {
819 match self.peek_kind() {
820 TokenKind::Eof => return,
821 TokenKind::LBrace => {
822 open_braces += 1;
823 self.bump();
824 }
825 TokenKind::RBrace => {
826 if open_braces > 0 {
827 open_braces -= 1;
828 self.bump();
829 } else if end == BodyEnd::Brace {
830 self.bump();
831 return;
832 } else {
833 self.bump();
834 }
835 }
836 TokenKind::Dedent => {
837 let tok = self.peek().clone();
838 let target = indent_level(&tok);
839 match end {
840 BodyEnd::Block(level) | BodyEnd::HeaderBody(level) => {
841 self.bump();
842 if target < level {
843 return;
844 }
845 }
846 _ => {
847 self.bump();
848 }
849 }
850 }
851 TokenKind::LBracket | TokenKind::Let if matches!(end, BodyEnd::HeaderBody(_)) => {
852 return
853 }
854 _ => {
855 self.bump();
856 }
857 }
858 }
859 }
860
861 fn parse_body_inner(
862 &mut self,
863 fields: &mut indexmap::IndexMap<String, NodeId>,
864 end: BodyEnd,
865 pending: &mut Vec<PendingInclude>,
866 ) {
867 loop {
868 self.skip_newlines();
869 match self.peek_kind() {
870 TokenKind::Eof => {
871 if end == BodyEnd::Brace {
872 self.error("E001", "expected `}`, found end of input");
873 }
874 return;
875 }
876 TokenKind::Dedent => {
877 let tok = self.peek().clone();
878 let target = indent_level(&tok);
879 match end {
880 BodyEnd::Document | BodyEnd::Brace => {
883 self.bump();
884 }
885 BodyEnd::Block(level) => {
886 self.bump();
887 if target >= level {
888 continue;
891 }
892 return;
893 }
894 BodyEnd::HeaderBody(level) => {
895 if target >= level {
896 self.bump();
897 continue;
898 }
899 return;
900 }
901 }
902 }
903 TokenKind::RBrace => match end {
904 BodyEnd::Brace => {
905 self.bump();
906 return;
907 }
908 BodyEnd::Block(_) => {
910 self.error("E001", "expected Dedent to close this block, found `}`");
911 self.bump();
912 return;
913 }
914 BodyEnd::HeaderBody(_) => return,
915 BodyEnd::Document => {
916 self.error("E002", "unexpected `}`");
917 self.bump();
918 }
919 },
920 TokenKind::LBracket | TokenKind::Let if matches!(end, BodyEnd::HeaderBody(_)) => {
928 return
929 }
930 TokenKind::Ident => {
931 let t = self.peek().clone();
932 if let BodyEnd::HeaderBody(level) = end {
933 if (t.text == "profile" || t.text == "include")
934 && t.col.saturating_sub(1) as usize <= level
935 && !matches!(
936 self.peek_kind_at(1),
937 TokenKind::Assign | TokenKind::Colon | TokenKind::LBrace
938 )
939 {
940 return;
941 }
942 }
943 self.parse_ident_statement(fields, pending);
944 }
945 TokenKind::Let => self.parse_let(),
946 TokenKind::LBracket => self.parse_table_header(fields),
947 TokenKind::At => {
948 if let BodyEnd::HeaderBody(level) = end {
955 let is_patch = matches!(self.peek_kind_at(1), TokenKind::Ident) && {
956 let tag = self.peek_at(1).map(|t| t.text.clone()).unwrap_or_default();
957 (tag == "patch" || tag == "remove" || tag == "rename")
958 && matches!(self.peek_kind_at(2), TokenKind::LParen)
959 };
960 let t = self.peek().clone();
961 if is_patch && t.col.saturating_sub(1) as usize <= level {
962 return;
963 }
964 }
965 let at_tok = self.peek().clone();
966 if matches!(self.peek_kind_at(1), TokenKind::Ident) {
967 let tag = self.peek_at(1).map(|t| t.text.clone()).unwrap_or_default();
968 if tag == "patch" || tag == "remove" || tag == "rename" {
969 if !matches!(self.peek_kind_at(2), TokenKind::LParen) {
973 let tag_tok = self.peek_at(1).unwrap_or_else(|| at_tok.clone());
974 self.error_at(
975 "E019",
976 format!("expected `(` after `@{tag}`"),
977 &tag_tok,
978 );
979 self.bump();
980 self.bump();
981 continue;
982 }
983 self.parse_patch_statement(&at_tok);
984 continue;
985 }
986 }
987 self.bump();
988 let id = self.parse_semantic_block(&at_tok);
989 if let Some(Node::Semantic { tag, .. }) = self.module.get(id) {
990 let key = tag.as_str().to_string();
991 self.record_stmt(
992 &key,
993 at_tok.line,
994 self.last_end_line(),
995 at_tok.col.saturating_sub(1) as usize,
996 );
997 fields.insert(key, id);
998 }
999 }
1000 TokenKind::Indent => {
1001 self.error("E010", "unexpected indentation");
1006 self.bump();
1007 }
1008 TokenKind::String(_)
1009 | TokenKind::SingleString
1010 | TokenKind::MlString(_)
1011 | TokenKind::MlSingleString => {
1012 let tok = self.peek().clone();
1017 self.error_at(
1018 "E002",
1019 "unexpected string literal at statement position (keys must be identifiers)",
1020 &tok,
1021 );
1022 self.bump();
1023 }
1024 _ => {
1025 let tok = self.peek().clone();
1039 let stmt_col = match end {
1040 BodyEnd::Block(l) | BodyEnd::HeaderBody(l) => (l + 1) as u32,
1041 _ => 1,
1042 };
1043 match &tok.kind {
1044 TokenKind::Integer
1045 | TokenKind::Float
1046 | TokenKind::True
1047 | TokenKind::False
1048 | TokenKind::Null
1049 | TokenKind::Env
1050 | TokenKind::Secret
1051 | TokenKind::Minus
1052 | TokenKind::Bang
1053 | TokenKind::LParen
1054 if tok.col == stmt_col =>
1055 {
1056 self.error_at(
1057 "E002",
1058 "unexpected value at statement position (values need a key: `name = …`)",
1059 &tok,
1060 );
1061 }
1062 _ => {}
1063 }
1064 self.bump();
1065 }
1066 }
1067 }
1068 }
1069
1070 fn parse_ident_statement(
1075 &mut self,
1076 fields: &mut indexmap::IndexMap<String, NodeId>,
1077 pending: &mut Vec<PendingInclude>,
1078 ) {
1079 let start = self.peek().clone();
1080 let level = start.col.saturating_sub(1) as usize;
1081 if start.text == "include"
1082 && !matches!(
1083 self.peek_kind_at(1),
1084 TokenKind::Assign | TokenKind::Colon | TokenKind::LBrace
1085 )
1086 {
1087 self.parse_include(pending);
1088 return;
1089 }
1090 if start.text == "profile"
1091 && !matches!(
1092 self.peek_kind_at(1),
1093 TokenKind::Assign | TokenKind::Colon | TokenKind::LBrace
1094 )
1095 {
1096 self.parse_profile();
1097 return;
1098 }
1099 match self.peek_kind_at(1) {
1100 TokenKind::Assign => {
1101 if let Some((k, id)) = self.parse_key_value_sep(TokenKind::Assign) {
1102 let end = self.last_end_line();
1103 self.record_stmt(&k, start.line, end, level);
1104 self.extract_annotations(&k, id, start.line);
1105 if self.check_depends_on_statement(&k, id) {
1106 self.note_duplicate_key(&k, Some(span_of(&start)), fields.contains_key(&k));
1107 fields.insert(k, id);
1108 }
1109 }
1110 }
1111 TokenKind::Colon => {
1112 if matches!(self.peek_kind_at(2), TokenKind::Newline | TokenKind::Eof) {
1113 self.parse_block_header(fields);
1114 } else if let Some((k, id)) = self.parse_key_value_sep(TokenKind::Colon) {
1115 let end = self.last_end_line();
1116 self.record_stmt(&k, start.line, end, level);
1117 self.extract_annotations(&k, id, start.line);
1118 if self.check_depends_on_statement(&k, id) {
1119 self.note_duplicate_key(&k, Some(span_of(&start)), fields.contains_key(&k));
1120 fields.insert(k, id);
1121 }
1122 }
1123 }
1124 TokenKind::LBrace => self.parse_table_block(fields),
1125 _ => {
1126 let tok = self.peek().clone();
1128 self.error_at("E002", "unexpected token after identifier", &tok);
1129 self.bump();
1130 }
1131 }
1132 }
1133
1134 fn parse_profile(&mut self) {
1143 let kw = self.bump(); if !self.current_path.is_empty() {
1145 self.error_at(
1146 "E018",
1147 "profile blocks may only appear at document root",
1148 &kw,
1149 );
1150 return;
1151 }
1152 let name_tok = self.peek().clone();
1153 let name = match &name_tok.kind {
1154 TokenKind::String(_)
1155 | TokenKind::SingleString
1156 | TokenKind::MlString(_)
1157 | TokenKind::MlSingleString => {
1158 self.bump();
1159 if name_tok.text.contains("${") {
1162 self.error_at(
1163 "E018",
1164 "profile names must be plain string literals",
1165 &name_tok,
1166 );
1167 return;
1168 }
1169 name_tok.text.clone()
1170 }
1171 _ => {
1172 self.error_at("E018", "profile blocks need a string name", &name_tok);
1173 return;
1174 }
1175 };
1176 let name = name.replace(ESCAPED_DOLLAR, "$");
1181 if self.module.profiles.contains_key(&name) {
1182 self.error_at("E018", format!("duplicate profile \"{name}\""), &name_tok);
1183 return;
1184 }
1185 enum ProfileBody {
1188 Brace,
1189 Block(usize),
1190 }
1191 let body = match self.peek_kind() {
1192 TokenKind::LBrace => {
1193 self.bump();
1194 ProfileBody::Brace
1195 }
1196 TokenKind::Colon => {
1197 let colon = self.bump();
1198 self.skip_newlines();
1199 match self.peek_kind() {
1200 TokenKind::Indent => {
1201 let indent = self.bump();
1202 ProfileBody::Block(indent_level(&indent))
1203 }
1204 _ => {
1205 self.error_at("E018", "profile block has no indented body", &colon);
1206 return;
1207 }
1208 }
1209 }
1210 _ => {
1211 let tok = self.peek().clone();
1212 self.error_at("E018", "expected `{` or `:` after the profile name", &tok);
1213 return;
1214 }
1215 };
1216 self.record_stmt(
1217 &name,
1218 kw.line,
1219 self.last_end_line(),
1220 kw.col.saturating_sub(1) as usize,
1221 );
1222 let profile_stmt = self.statements.len() - 1;
1223 self.current_path.push(name.clone());
1224 self.ident_scopes.push(Vec::new());
1225 let mut fields = indexmap::IndexMap::new();
1226 let mut pending = Vec::new();
1227 let saved_in_profile = self.in_profile;
1228 self.in_profile = true;
1229 match body {
1230 ProfileBody::Brace => self.parse_body(&mut fields, BodyEnd::Brace, &mut pending),
1231 ProfileBody::Block(level) => {
1232 self.parse_body(&mut fields, BodyEnd::Block(level), &mut pending)
1233 }
1234 }
1235 self.in_profile = saved_in_profile;
1236 self.splice_includes(&mut fields, pending, true);
1237 let idents = self.ident_scopes.pop().unwrap_or_default();
1238 self.pending_profiles.push((idents, fields.clone()));
1242 let table_id = self.module.add_node(Node::Table { fields });
1243 self.statements[profile_stmt].end_line = self.last_end_line();
1244 self.current_path.pop();
1245 self.module.profiles.insert(name, table_id);
1246 }
1247
1248 const MAX_INCLUDE_DEPTH: usize = 32;
1254
1255 fn parse_include(&mut self, pending: &mut Vec<PendingInclude>) {
1260 let kw = self.bump(); let path_tok = self.peek().clone();
1262 let path = match &path_tok.kind {
1263 TokenKind::String(_)
1264 | TokenKind::SingleString
1265 | TokenKind::MlString(_)
1266 | TokenKind::MlSingleString => {
1267 self.bump();
1268 path_tok.text.clone()
1269 }
1270 _ => {
1271 self.error_at("E015", "include paths must be string literals", &path_tok);
1272 self.bump(); return;
1274 }
1275 };
1276 if path.contains("${") || path.contains(ESCAPED_DOLLAR) {
1277 self.error_at(
1278 "E015",
1279 "interpolation in include paths is a hard error; paths must be static string literals",
1280 &path_tok,
1281 );
1282 return;
1283 }
1284 let mut alias: Option<String> = None;
1286 let mut private = false;
1287 loop {
1288 let t = self.peek().clone();
1289 match &t.kind {
1290 TokenKind::Ident if t.text == "as" && alias.is_none() => {
1291 self.bump();
1292 match self.expect(TokenKind::Ident) {
1293 Some(name) => alias = Some(name.text),
1294 None => return,
1295 }
1296 }
1297 TokenKind::Ident if t.text == "private" && !private => {
1298 self.bump();
1299 private = true;
1300 }
1301 _ => break,
1302 }
1303 }
1304 let key = alias.clone().unwrap_or_else(|| path.clone());
1305 self.record_stmt(
1306 &key,
1307 kw.line,
1308 self.last_end_line(),
1309 kw.col.saturating_sub(1) as usize,
1310 );
1311 self.module.includes.push(IncludeStmt {
1312 path: path.clone(),
1313 alias: alias.clone(),
1314 private,
1315 scope_path: self.current_path.clone(),
1316 line: kw.line,
1317 });
1318 pending.push(PendingInclude {
1319 path,
1320 alias,
1321 private,
1322 tok: path_tok,
1323 patch_mark: self.module.patches.len(),
1324 });
1325 }
1326
1327 fn load_include(&mut self, inc: &PendingInclude) -> Option<NodeId> {
1333 use std::path::{Component, PathBuf};
1334 let fail = |me: &mut Parser, code: &str, msg: String| {
1335 me.error_at(code, msg, &inc.tok);
1336 None
1337 };
1338 let raw = std::path::Path::new(&inc.path);
1340 if !self.options.allow_parent_escapes {
1341 if raw.is_absolute() {
1342 return fail(
1343 self,
1344 "E015",
1345 format!(
1346 "include paths must be relative (got `{}`); pass --allow-parent-escapes to override",
1347 inc.path
1348 ),
1349 );
1350 }
1351 if raw.components().any(|c| matches!(c, Component::ParentDir)) {
1352 return fail(
1353 self,
1354 "E015",
1355 format!(
1356 "include paths must not escape the including directory via `..` (got `{}`); pass --allow-parent-escapes to override",
1357 inc.path
1358 ),
1359 );
1360 }
1361 }
1362 let base_dir: PathBuf = self
1363 .current_file
1364 .as_ref()
1365 .and_then(|f| f.parent().map(|p| p.to_path_buf()))
1366 .unwrap_or_else(|| PathBuf::from("."));
1367 let joined = base_dir.join(raw);
1368 let canonical = match joined.canonicalize() {
1369 Ok(p) => p,
1370 Err(e) => {
1371 return fail(
1372 self,
1373 "E017",
1374 format!("cannot read include file `{}`: {e}", joined.display()),
1375 );
1376 }
1377 };
1378 if !self.options.allow_parent_escapes {
1379 if let Ok(base_canon) = base_dir.canonicalize() {
1380 if !canonical.starts_with(&base_canon) {
1381 return fail(
1382 self,
1383 "E015",
1384 format!(
1385 "include target `{}` escapes the including directory; pass --allow-parent-escapes to override",
1386 canonical.display()
1387 ),
1388 );
1389 }
1390 }
1391 }
1392 if self.include_stack.contains(&canonical) {
1393 return fail(
1394 self,
1395 "E016",
1396 format!(
1397 "circular include detected: `{}` is already being parsed ({})",
1398 canonical.display(),
1399 self.include_stack
1400 .iter()
1401 .map(|p| p.display().to_string())
1402 .collect::<Vec<_>>()
1403 .join(" -> ")
1404 ),
1405 );
1406 }
1407 if self.include_stack.len() >= Self::MAX_INCLUDE_DEPTH {
1408 return fail(
1409 self,
1410 "E016",
1411 format!(
1412 "maximum include depth ({}) exceeded",
1413 Self::MAX_INCLUDE_DEPTH
1414 ),
1415 );
1416 }
1417 let trust = self.trust.clone();
1425 let src = match std::fs::read_to_string(&canonical) {
1426 Ok(s) => s,
1427 Err(e) => {
1428 return fail(
1429 self,
1430 "E017",
1431 format!("cannot read include file `{}`: {e}", canonical.display()),
1432 );
1433 }
1434 };
1435 if let Some(keys) = &trust {
1436 let label = canonical.to_string_lossy().into_owned();
1437 match crate::sign::read_sidecar(&canonical, &label) {
1438 Err(reason) => return fail(self, "E032", reason),
1439 Ok(sidecar) => {
1440 if let Err(reason) =
1441 crate::sign::verify_bytes(&label, src.as_bytes(), &sidecar, keys)
1442 {
1443 return fail(self, "E032", reason);
1444 }
1445 }
1446 }
1447 }
1448 let saved_file = self.current_file.clone();
1451 let saved_root = self.module.root;
1452 let saved_scopes = std::mem::take(&mut self.ident_scopes);
1453 let saved_path = self.current_path.clone();
1454 let saved_brace = self.brace_tables;
1455 let saved_indent = self.indent_tables;
1456 let saved_in_include = self.in_include;
1457 let saved_in_profile = self.in_profile;
1458 self.current_file = Some(canonical.clone());
1459 self.include_stack.push(canonical);
1460 self.in_include = true;
1461 self.in_profile = false;
1464 let mut lexer = Lexer::new(&src);
1466 let lexed = lexer.tokenize();
1467 let lex_file = self.current_file_name();
1468 let n_lex_comments = lexed.comments.len();
1472 self.comments.extend(lexed.comments);
1473 self.comment_files
1474 .extend(std::iter::repeat_with(|| lex_file.clone()).take(n_lex_comments));
1475 if let Some(alias) = inc.alias.clone() {
1478 self.current_path.push(alias);
1479 }
1480 let mut sub_errors = lexed.errors;
1481 let mut sub_repairs = lexed.repairs;
1482 for d in sub_errors.iter_mut().chain(sub_repairs.iter_mut()) {
1483 if d.file.is_none() {
1484 d.file = lex_file.clone();
1485 }
1486 }
1487 self.errors.extend(sub_errors);
1488 self.module.healing.extend(sub_repairs);
1489 let saved_tokens = std::mem::take(&mut self.tokens);
1492 let saved_pos = self.pos;
1493 self.tokens = lexed.tokens;
1494 self.pos = 0;
1495 self.parse_document();
1496 self.tokens = saved_tokens;
1497 self.pos = saved_pos;
1498 let sub_root = self.module.root;
1501 self.module.root = saved_root;
1502 self.current_file = saved_file;
1503 self.include_stack.pop();
1504 self.current_path = saved_path;
1505 self.brace_tables = saved_brace;
1506 self.indent_tables = saved_indent;
1507 self.in_include = saved_in_include;
1508 self.in_profile = saved_in_profile;
1509 self.ident_scopes = saved_scopes;
1510 sub_root
1511 }
1512
1513 fn splice_includes(
1523 &mut self,
1524 fields: &mut indexmap::IndexMap<String, NodeId>,
1525 mut pending: Vec<PendingInclude>,
1526 profile_scope: bool,
1527 ) {
1528 if pending.is_empty() {
1529 return;
1530 }
1531 if !self.options.enable_includes {
1532 for inc in &pending {
1533 self.error_at(
1534 "E015",
1535 "include statements require file-based parsing (parse_file); string input stays hermetic",
1536 &inc.tok,
1537 );
1538 }
1539 return;
1540 }
1541 if !self.options.resolve_includes {
1542 return;
1544 }
1545 let mut spliced: std::collections::HashSet<String> = std::collections::HashSet::new();
1550 let mut pi = 0usize;
1551 while pi < pending.len() {
1552 let inc = pending[pi].clone();
1553 let patches_before = self.module.patches.len();
1554 let loaded = self.load_include(&inc);
1555 let mark = inc.patch_mark.min(patches_before);
1563 let old_tail_len = patches_before - mark;
1564 let mut inserted = 0usize;
1565 if self.module.patches.len() > patches_before {
1566 let mut tail: Vec<NodeId> = self.module.patches.drain(mark..).collect();
1567 let new_part: Vec<NodeId> = tail.split_off(old_tail_len.min(tail.len()));
1568 inserted = new_part.len();
1569 self.module.patches.extend(new_part);
1570 self.module.patches.extend(tail);
1571 }
1572 for later in pending.iter_mut().skip(pi + 1) {
1573 if later.patch_mark >= mark {
1574 later.patch_mark += inserted;
1575 }
1576 }
1577 pi += 1;
1578 let Some(sub_root) = loaded else {
1579 continue;
1580 };
1581 let sub_fields = match self.module.get(sub_root) {
1582 Some(Node::Table { fields }) => fields.clone(),
1583 _ => continue,
1584 };
1585 if inc.private {
1586 match inc.alias {
1587 Some(name) => {
1588 if !self.module.bindings.contains_key(&name)
1592 || self.binding_spliced.contains(&name)
1593 {
1594 self.module.bindings.insert(name.clone(), sub_root);
1595 self.binding_spliced.insert(name);
1596 }
1597 }
1598 None => {
1599 for (k, v) in sub_fields {
1600 if !self.module.bindings.contains_key(&k)
1601 || self.binding_spliced.contains(&k)
1602 {
1603 self.module.bindings.insert(k.clone(), v);
1604 self.binding_spliced.insert(k);
1605 }
1606 }
1607 }
1608 }
1609 continue;
1610 }
1611 match inc.alias {
1612 Some(name) => {
1613 self.merge_key(fields, name, sub_root, &mut spliced);
1616 }
1617 None => {
1618 for (k, v) in sub_fields {
1619 self.merge_key(fields, k, v, &mut spliced);
1620 }
1621 }
1622 }
1623 }
1624 if profile_scope {
1625 self.reject_profile_schemas(fields);
1626 }
1627 }
1628
1629 fn reject_profile_schemas(&mut self, fields: &mut IndexMap<String, NodeId>) {
1639 let keys: Vec<(String, NodeId)> = fields.iter().map(|(k, v)| (k.clone(), *v)).collect();
1640 for (key, id) in keys {
1641 if matches!(self.module.get(id), Some(Node::SchemaTable { .. })) {
1642 self.report_profile_schema(id);
1643 fields.shift_remove(&key);
1644 continue;
1645 }
1646 let mut hits = Vec::new();
1647 self.strip_schema_tables(id, &mut hits);
1648 for hit in hits {
1649 self.report_profile_schema(hit);
1650 }
1651 }
1652 }
1653
1654 fn report_profile_schema(&mut self, schema_id: NodeId) {
1656 let span = self.module.node_spans.get(&schema_id).cloned();
1657 let file = self.module.node_files.get(&schema_id).cloned();
1658 self.errors.push(Diagnostic {
1659 severity: Severity::Error,
1660 code: "E020".into(),
1661 message: "schemas may not appear inside profiles, including through includes; move the schema to the document root".into(),
1662 path: None,
1663 span,
1664 repair: None,
1665 confidence: 1.0,
1666 file,
1667 });
1668 }
1669
1670 fn strip_schema_tables(&mut self, id: NodeId, out: &mut Vec<NodeId>) {
1676 let children: Vec<(String, NodeId)> = match self.module.get(id) {
1677 Some(Node::Table { fields }) => fields.clone().into_iter().collect(),
1678 Some(Node::SchemaTable { .. }) => {
1681 out.push(id);
1682 return;
1683 }
1684 _ => return,
1685 };
1686 for (k, v) in &children {
1687 if matches!(self.module.get(*v), Some(Node::SchemaTable { .. })) {
1688 if let Some(Node::Table { fields }) = self.module.get_mut(id) {
1689 fields.shift_remove(k);
1690 }
1691 out.push(*v);
1692 } else {
1693 self.strip_schema_tables(*v, out);
1694 }
1695 }
1696 }
1697
1698 fn merge_key(
1704 &mut self,
1705 fields: &mut indexmap::IndexMap<String, NodeId>,
1706 key: String,
1707 inc_id: NodeId,
1708 spliced: &mut std::collections::HashSet<String>,
1709 ) {
1710 match fields.get(&key) {
1711 None => {
1712 fields.insert(key.clone(), inc_id);
1713 self.mark_subtree(&key, inc_id, spliced);
1714 }
1715 Some(_) if spliced.contains(&key) => {
1716 self.merge_tables_at(fields, &key, inc_id, spliced, true);
1717 }
1718 Some(_) => {
1719 self.merge_tables_at(fields, &key, inc_id, spliced, false);
1720 }
1721 }
1722 }
1723
1724 fn merge_tables_at(
1730 &mut self,
1731 fields: &mut indexmap::IndexMap<String, NodeId>,
1732 key: &str,
1733 inc_id: NodeId,
1734 spliced: &mut std::collections::HashSet<String>,
1735 overwrite: bool,
1736 ) {
1737 if matches!(self.module.get(inc_id), Some(Node::SchemaTable { .. })) {
1742 if let Some(&local_id) = fields.get(key) {
1743 if local_id != inc_id
1744 && matches!(self.module.get(local_id), Some(Node::SchemaTable { .. }))
1745 {
1746 if self.merge_schemas_inner(local_id, inc_id, key, spliced, overwrite) {
1747 spliced.insert(key.to_string());
1748 }
1749 return;
1750 }
1751 }
1752 }
1753 let Some(Node::Table { fields: inc_fields }) = self.module.get(inc_id) else {
1754 if overwrite {
1758 if let Some(&local_id) = fields.get(key) {
1759 if !self.has_unmarked(local_id, key, spliced) {
1760 fields.insert(key.to_string(), inc_id);
1761 self.mark_subtree(key, inc_id, spliced);
1762 }
1763 }
1764 }
1765 return;
1766 };
1767 let inc_fields = inc_fields.clone();
1768 let Some(&local_id) = fields.get(key) else {
1769 return;
1770 };
1771 if local_id == inc_id {
1772 return;
1773 }
1774 let Some(Node::Table { .. }) = self.module.get(local_id) else {
1775 if overwrite && spliced.contains(key) {
1778 fields.insert(key.to_string(), inc_id);
1779 self.mark_subtree(key, inc_id, spliced);
1780 }
1781 return;
1782 };
1783 let mut merged = match self.module.get(local_id) {
1784 Some(Node::Table { fields: lf }) => lf.clone(),
1785 _ => return,
1786 };
1787 let mut changed = false;
1788 for (k, v) in &inc_fields {
1789 let child = format!("{key}.{k}");
1790 match merged.get(k) {
1791 None => {
1792 merged.insert(k.clone(), *v);
1793 self.module.add_edge(*v, local_id);
1794 self.mark_subtree(&child, *v, spliced);
1795 changed = true;
1796 }
1797 Some(&sub_local) => {
1798 if sub_local == *v {
1799 continue;
1800 }
1801 let local_kind = self.module.get(sub_local).cloned();
1802 let inc_kind = self.module.get(*v).cloned();
1803 if matches!(
1805 (&local_kind, &inc_kind),
1806 (
1807 Some(Node::SchemaTable { .. }),
1808 Some(Node::SchemaTable { .. })
1809 )
1810 ) {
1811 let sub_overwrite = overwrite && spliced.contains(&child);
1812 if self.merge_schemas_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1813 changed = true;
1814 }
1815 continue;
1816 }
1817 let both_tables = matches!(
1818 (&local_kind, &inc_kind),
1819 (Some(Node::Table { .. }), Some(Node::Table { .. }))
1820 );
1821 if !both_tables {
1822 if overwrite && spliced.contains(&child) {
1823 merged.insert(k.clone(), *v);
1824 self.module.add_edge(*v, local_id);
1825 changed = true;
1826 }
1827 continue;
1828 }
1829 let sub_overwrite = overwrite && spliced.contains(&child);
1833 if self.merge_tables_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1834 changed = true;
1835 }
1836 }
1837 }
1838 }
1839 if changed {
1840 if let Some(Node::Table { fields: slot }) = self.module.get_mut(local_id) {
1841 *slot = merged;
1842 }
1843 spliced.insert(key.to_string());
1844 }
1845 }
1846
1847 fn merge_tables_inner(
1851 &mut self,
1852 local_id: NodeId,
1853 inc_id: NodeId,
1854 path: &str,
1855 spliced: &mut std::collections::HashSet<String>,
1856 overwrite: bool,
1857 ) -> bool {
1858 let Some(Node::Table { fields: inc_fields }) = self.module.get(inc_id) else {
1859 return false;
1860 };
1861 let inc_fields = inc_fields.clone();
1862 let mut merged = match self.module.get(local_id) {
1863 Some(Node::Table { fields: lf }) => lf.clone(),
1864 _ => return false,
1865 };
1866 let mut changed = false;
1867 for (k, v) in &inc_fields {
1868 let child = format!("{path}.{k}");
1869 match merged.get(k) {
1870 None => {
1871 merged.insert(k.clone(), *v);
1872 self.module.add_edge(*v, local_id);
1873 self.mark_subtree(&child, *v, spliced);
1874 changed = true;
1875 }
1876 Some(&sub_local) => {
1877 if sub_local == *v {
1878 continue;
1879 }
1880 let local_kind = self.module.get(sub_local).cloned();
1881 let inc_kind = self.module.get(*v).cloned();
1882 if matches!(
1884 (&local_kind, &inc_kind),
1885 (
1886 Some(Node::SchemaTable { .. }),
1887 Some(Node::SchemaTable { .. })
1888 )
1889 ) {
1890 let sub_overwrite = overwrite && spliced.contains(&child);
1891 if self.merge_schemas_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1892 changed = true;
1893 }
1894 continue;
1895 }
1896 let both_tables = matches!(
1897 (&local_kind, &inc_kind),
1898 (Some(Node::Table { .. }), Some(Node::Table { .. }))
1899 );
1900 if !both_tables {
1901 if overwrite && spliced.contains(&child) {
1902 merged.insert(k.clone(), *v);
1903 self.module.add_edge(*v, local_id);
1904 changed = true;
1905 }
1906 continue;
1907 }
1908 let sub_overwrite = overwrite && spliced.contains(&child);
1909 if self.merge_tables_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1910 changed = true;
1911 }
1912 }
1913 }
1914 }
1915 if changed {
1916 if let Some(Node::Table { fields: slot }) = self.module.get_mut(local_id) {
1917 *slot = merged;
1918 }
1919 }
1920 changed
1921 }
1922
1923 fn merge_schemas_inner(
1929 &mut self,
1930 local_id: NodeId,
1931 inc_id: NodeId,
1932 path: &str,
1933 spliced: &mut std::collections::HashSet<String>,
1934 overwrite: bool,
1935 ) -> bool {
1936 let (local_fields, local_closed) = match self.module.get(local_id) {
1937 Some(Node::SchemaTable { fields, closed }) => (fields.clone(), *closed),
1938 _ => return false,
1939 };
1940 let (inc_fields, inc_closed) = match self.module.get(inc_id) {
1941 Some(Node::SchemaTable { fields, closed }) => (fields.clone(), *closed),
1942 _ => return false,
1943 };
1944 let mut merged = local_fields;
1945 let mut merged_closed = local_closed;
1946 let mut changed = false;
1947 if overwrite && merged_closed != inc_closed {
1948 merged_closed = inc_closed;
1949 changed = true;
1950 }
1951 for (k, v) in &inc_fields {
1952 let child = format!("{path}.{k}");
1953 match merged.get(k) {
1954 None => {
1955 merged.insert(k.clone(), *v);
1956 self.module.add_edge(*v, local_id);
1957 self.mark_subtree(&child, *v, spliced);
1958 changed = true;
1959 }
1960 Some(&sub_local) => {
1961 if sub_local == *v {
1962 continue;
1963 }
1964 let both_schema = matches!(
1965 (self.module.get(sub_local), self.module.get(*v)),
1966 (
1967 Some(Node::SchemaTable { .. }),
1968 Some(Node::SchemaTable { .. })
1969 )
1970 );
1971 if both_schema {
1972 let sub_overwrite = overwrite && spliced.contains(&child);
1973 if self.merge_schemas_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1974 changed = true;
1975 }
1976 continue;
1977 }
1978 if overwrite && spliced.contains(&child) {
1979 merged.insert(k.clone(), *v);
1980 self.module.add_edge(*v, local_id);
1981 changed = true;
1982 }
1983 }
1984 }
1985 }
1986 if changed {
1987 if let Some(Node::SchemaTable {
1988 fields: slot,
1989 closed,
1990 }) = self.module.get_mut(local_id)
1991 {
1992 *slot = merged;
1993 *closed = merged_closed;
1994 }
1995 }
1996 changed
1997 }
1998
1999 fn has_unmarked(
2003 &self,
2004 id: NodeId,
2005 path: &str,
2006 spliced: &std::collections::HashSet<String>,
2007 ) -> bool {
2008 if !spliced.contains(path) {
2009 return true;
2010 }
2011 match self.module.get(id) {
2012 Some(Node::Table { fields }) => {
2013 let fields = fields.clone();
2014 fields.iter().any(|(k, v)| {
2015 let child = format!("{path}.{k}");
2016 self.has_unmarked(*v, &child, spliced)
2017 })
2018 }
2019 _ => false,
2020 }
2021 }
2022
2023 fn mark_subtree(
2028 &self,
2029 path: &str,
2030 id: NodeId,
2031 spliced: &mut std::collections::HashSet<String>,
2032 ) {
2033 spliced.insert(path.to_string());
2034 let children: Vec<(String, NodeId)> = match self.module.get(id) {
2035 Some(Node::Table { fields }) | Some(Node::SchemaTable { fields, .. }) => {
2036 fields.clone().into_iter().collect()
2037 }
2038 _ => return,
2039 };
2040 for (k, v) in &children {
2041 let child = format!("{path}.{k}");
2042 self.mark_subtree(&child, *v, spliced);
2043 }
2044 }
2045
2046 fn parse_block_header(&mut self, parent: &mut indexmap::IndexMap<String, NodeId>) {
2048 let key_tok = self.peek().clone();
2049 let key = self
2050 .expect(TokenKind::Ident)
2051 .map(|t| t.text)
2052 .unwrap_or_default();
2053 let colon = self.expect(TokenKind::Colon);
2054 self.skip_newlines();
2055 let level = if matches!(self.peek_kind(), TokenKind::Indent) {
2056 let indent = self.bump();
2057 indent_level(&indent)
2058 } else {
2059 let tok = colon.unwrap_or_else(|| self.peek().clone());
2061 self.error_at(
2062 "E008",
2063 format!("block header `{key}:` has no indented body"),
2064 &tok,
2065 );
2066 return;
2067 };
2068 if key == "schema" {
2071 if self.in_profile {
2072 self.error_at("E020", "schemas may not appear inside profiles", &key_tok);
2073 return;
2074 }
2075 self.indent_tables += 1;
2076 self.record_stmt(
2077 &key,
2078 key_tok.line,
2079 self.last_end_line(),
2080 key_tok.col.saturating_sub(1) as usize,
2081 );
2082 let node = self.parse_schema_indent(&key_tok, level);
2083 parent.insert(key, node);
2084 return;
2085 }
2086 self.indent_tables += 1;
2087 let stmt_idx = self.statements.len();
2088 self.statements.push(StmtInfo {
2089 key: key.clone(),
2090 path: self.path_str(),
2091 start_line: key_tok.line,
2092 end_line: key_tok.line,
2093 level: key_tok.col.saturating_sub(1) as usize,
2094 file: self.current_file_name(),
2095 });
2096 self.current_path.push(key.clone());
2097 self.blocks.push(BlockInfo {
2098 path: self.path_str(),
2099 level,
2100 start_line: key_tok.line,
2101 file: self.current_file_name(),
2102 });
2103 self.ident_scopes.push(Vec::new());
2104 let mut fields = indexmap::IndexMap::new();
2105 let mut pending = Vec::new();
2106 self.parse_body(&mut fields, BodyEnd::Block(level), &mut pending);
2107 self.splice_includes(&mut fields, pending, false);
2108 let idents = self.ident_scopes.pop().unwrap_or_default();
2109 let _ = self.resolve_sibling_refs(idents, &fields);
2110 let table_id = self.add_spanned(Node::Table { fields }, &key_tok);
2113 self.statements[stmt_idx].end_line = self.last_end_line();
2114 self.current_path.pop();
2115 self.extract_annotations(&key, table_id, key_tok.line);
2116 self.note_duplicate_key(&key, Some(span_of(&key_tok)), parent.contains_key(&key));
2117 parent.insert(key, table_id);
2118 }
2119
2120 fn parse_table_block(&mut self, parent: &mut indexmap::IndexMap<String, NodeId>) {
2122 let key_tok = self.peek().clone();
2123 let key = self
2124 .expect(TokenKind::Ident)
2125 .map(|t| t.text)
2126 .unwrap_or_default();
2127 if key == "schema" {
2130 if self.in_profile {
2131 self.error_at("E020", "schemas may not appear inside profiles", &key_tok);
2132 return;
2133 }
2134 self.brace_tables += 1;
2135 self.record_stmt(
2136 &key,
2137 key_tok.line,
2138 self.last_end_line(),
2139 key_tok.col.saturating_sub(1) as usize,
2140 );
2141 let node = self.parse_schema_brace(&key_tok);
2142 parent.insert(key, node);
2143 return;
2144 }
2145 self.expect(TokenKind::LBrace);
2146 self.brace_tables += 1;
2147 let stmt_idx = self.statements.len();
2148 self.statements.push(StmtInfo {
2149 key: key.clone(),
2150 path: self.path_str(),
2151 start_line: key_tok.line,
2152 end_line: key_tok.line,
2153 level: key_tok.col.saturating_sub(1) as usize,
2154 file: self.current_file_name(),
2155 });
2156 self.current_path.push(key.clone());
2157 self.blocks.push(BlockInfo {
2158 path: self.path_str(),
2159 level: key_tok.col.saturating_sub(1) as usize,
2160 start_line: key_tok.line,
2161 file: self.current_file_name(),
2162 });
2163 self.ident_scopes.push(Vec::new());
2164 let mut fields = indexmap::IndexMap::new();
2165 let mut pending = Vec::new();
2166 self.parse_body(&mut fields, BodyEnd::Brace, &mut pending);
2167 self.splice_includes(&mut fields, pending, false);
2168 let idents = self.ident_scopes.pop().unwrap_or_default();
2169 let _ = self.resolve_sibling_refs(idents, &fields);
2170 let table_id = self.add_spanned(Node::Table { fields }, &key_tok);
2172 self.statements[stmt_idx].end_line = self.last_end_line();
2173 self.current_path.pop();
2174 self.extract_annotations(&key, table_id, key_tok.line);
2175 self.note_duplicate_key(&key, Some(span_of(&key_tok)), parent.contains_key(&key));
2176 parent.insert(key, table_id);
2177 }
2178
2179 fn error_at(&mut self, code: &str, message: impl Into<String>, tok: &Token) {
2181 self.errors.push(Diagnostic {
2182 severity: Severity::Error,
2183 code: code.into(),
2184 message: message.into(),
2185 path: None,
2186 span: Some(span_of(tok)),
2187 repair: None,
2188 confidence: 1.0,
2189 file: self.current_file_name(),
2190 });
2191 }
2192
2193 fn error_at_span(&mut self, code: &str, message: impl Into<String>, span: &Span) {
2196 self.errors.push(Diagnostic {
2197 severity: Severity::Error,
2198 code: code.into(),
2199 message: message.into(),
2200 path: None,
2201 span: Some(span.clone()),
2202 repair: None,
2203 confidence: 1.0,
2204 file: self.current_file_name(),
2205 });
2206 }
2207
2208 fn note_duplicate_key(&mut self, key: &str, span: Option<Span>, existed: bool) {
2216 if existed {
2217 self.module.healing.push(Diagnostic {
2218 severity: Severity::Warning,
2219 code: "W011".into(),
2220 message: format!("duplicate key '{key}'; last wins"),
2221 path: None,
2222 span,
2223 repair: None,
2224 confidence: 1.0,
2225 file: self.current_file_name(),
2226 });
2227 }
2228 }
2229
2230 fn extract_schemas(&mut self) {
2239 if self.in_include {
2240 return;
2241 }
2242 let Some(root) = self.module.root else {
2243 return;
2244 };
2245 if let Some(schema_id) = self.extract_table_schema(root) {
2246 self.module.schema = Some(schema_id);
2247 }
2248 }
2249
2250 fn extract_table_schema(&mut self, table_id: NodeId) -> Option<NodeId> {
2254 let fields = match self.module.get(table_id) {
2256 Some(Node::Table { fields }) => fields.clone(),
2257 _ => return None,
2258 };
2259 let mut schema_id = match fields.get("schema") {
2261 Some(&id) => match self.module.get(id) {
2262 Some(Node::SchemaTable { .. }) => {
2263 if let Some(Node::Table { fields }) = self.module.get_mut(table_id) {
2265 fields.shift_remove("schema");
2266 }
2267 Some(id)
2268 }
2269 _ => None,
2270 },
2271 None => None,
2272 };
2273 let mut nested: IndexMap<String, NodeId> = IndexMap::new();
2275 for (k, v) in &fields {
2276 if *k == "schema" {
2277 continue;
2278 }
2279 if matches!(self.module.get(*v), Some(Node::Table { .. })) {
2280 if let Some(sub) = self.extract_table_schema(*v) {
2281 nested.insert(k.clone(), sub);
2282 }
2283 }
2284 }
2285 if nested.is_empty() {
2286 return schema_id;
2287 }
2288 let parent = match schema_id {
2291 Some(id) => id,
2292 None => {
2293 let id = self.module.add_node(Node::SchemaTable {
2294 fields: IndexMap::new(),
2295 closed: false,
2296 });
2297 schema_id = Some(id);
2298 id
2299 }
2300 };
2301 if let Some(Node::SchemaTable { fields, .. }) = self.module.get_mut(parent) {
2302 for (k, v) in nested {
2303 fields.insert(k, v);
2304 }
2305 }
2306 schema_id
2307 }
2308
2309 fn extract_depends_on(&mut self) {
2318 if self.in_include {
2319 return;
2320 }
2321 let Some(root) = self.module.root else {
2322 return;
2323 };
2324 self.extract_table_depends_on(root);
2325 }
2326
2327 fn extract_table_depends_on(&mut self, table_id: NodeId) {
2329 let fields = match self.module.get(table_id) {
2330 Some(Node::Table { fields }) => fields.clone(),
2331 _ => return,
2332 };
2333 if let Some(&id) = fields.get("depends_on") {
2334 if let Some(Node::Array { elements }) = self.module.get(id).cloned() {
2335 let mut paths = Vec::new();
2336 let mut bad = false;
2337 for element in &elements {
2338 match self.module.get(*element) {
2339 Some(Node::Literal(Value::String(path))) => paths.push(path.clone()),
2340 _ => {
2341 bad = true;
2342 if let Some(span) = self.module.node_spans.get(element).cloned() {
2343 self.error_at_span(
2344 "E022",
2345 "depends_on entries must be string literals",
2346 &span,
2347 );
2348 }
2349 }
2350 }
2351 }
2352 if !bad {
2353 if let Some(Node::Table { fields }) = self.module.get_mut(table_id) {
2354 fields.shift_remove("depends_on");
2355 }
2356 let span =
2357 self.module
2358 .node_spans
2359 .get(&id)
2360 .cloned()
2361 .unwrap_or(crate::diag::Span {
2362 line: 1,
2363 column: 1,
2364 end_line: 1,
2365 end_column: 1,
2366 });
2367 self.module
2368 .depends_on
2369 .insert(table_id, DependsOn { paths, span });
2370 }
2371 }
2372 }
2373 let fields = match self.module.get(table_id) {
2377 Some(Node::Table { fields }) => fields.clone(),
2378 _ => return,
2379 };
2380 for (k, v) in &fields {
2381 if *k == "depends_on" {
2382 continue;
2383 }
2384 if matches!(self.module.get(*v), Some(Node::Table { .. })) {
2385 self.extract_table_depends_on(*v);
2386 }
2387 }
2388 }
2389
2390 fn current_file_name(&self) -> Option<String> {
2392 self.current_file
2393 .as_ref()
2394 .map(|p| p.to_string_lossy().into_owned())
2395 }
2396
2397 fn add_spanned(&mut self, node: Node, tok: &Token) -> NodeId {
2400 let id = self.module.add_node(node);
2401 self.module.record_span(id, span_of(tok));
2402 if let Some(file) = self.current_file_name() {
2403 self.module.record_file(id, file);
2404 }
2405 id
2406 }
2407
2408 fn heal(
2420 &mut self,
2421 code: &str,
2422 message: String,
2423 tok: &Token,
2424 repair: Repair,
2425 confidence: f64,
2426 ) -> bool {
2427 let applies = confidence >= thresholds::HIGH
2428 || (self.options.resilient && confidence >= thresholds::MEDIUM);
2429 if !applies {
2430 return false;
2431 }
2432 self.module.healing.push(Diagnostic {
2433 severity: if confidence >= thresholds::AUTO_APPLY {
2434 Severity::Info
2435 } else {
2436 Severity::Warning
2437 },
2438 code: code.into(),
2439 message,
2440 path: None,
2441 span: Some(span_of(tok)),
2442 repair: Some(repair),
2443 confidence,
2444 file: self.current_file_name(),
2445 });
2446 true
2447 }
2448
2449 fn expect_closing_healing(&mut self, expected: TokenKind, heal_code: &str) -> bool {
2453 let next = self.peek().clone();
2454 if next.kind == expected {
2455 self.bump();
2456 return true;
2457 }
2458 let healable = matches!(
2459 (&expected, &next.kind),
2460 (TokenKind::RBracket, TokenKind::RBrace) | (TokenKind::RBrace, TokenKind::RBracket)
2461 );
2462 if healable {
2463 let applied = self.heal(
2464 heal_code,
2465 format!(
2466 "mismatched closing delimiter; replaced {:?} with {:?}",
2467 next.kind, expected
2468 ),
2469 &next,
2470 Repair {
2471 original: format!("{:?}", next.kind),
2472 suggested: format!("{:?}", expected),
2473 kind: RepairKind::Other("bracket_mismatch".into()),
2474 safe: false,
2475 },
2476 thresholds::MEDIUM + 0.12, );
2478 if applied {
2479 self.bump();
2480 return true;
2481 }
2482 }
2483 self.error(
2486 "E001",
2487 format!("expected {:?}, found {:?}", expected, next.kind),
2488 );
2489 false
2490 }
2491
2492 fn parse_document(&mut self) {
2497 let mut root_fields = indexmap::IndexMap::new();
2498 self.ident_scopes.push(Vec::new());
2499
2500 let mut pending = Vec::new();
2501 self.parse_body(&mut root_fields, BodyEnd::Document, &mut pending);
2502 self.splice_includes(&mut root_fields, pending, false);
2503
2504 let root_idents = self.ident_scopes.pop().unwrap_or_default();
2505 let _ = self.resolve_sibling_refs(root_idents, &root_fields);
2506
2507 if !self.in_include {
2515 let profiles = std::mem::take(&mut self.pending_profiles);
2516 for (idents, prof_fields) in profiles {
2517 let leftover = self.resolve_sibling_refs(idents, &prof_fields);
2518 let unowned: Vec<(String, NodeId)> = leftover
2523 .into_iter()
2524 .map(|(_, id)| (String::new(), id))
2525 .collect();
2526 let _ = self.resolve_sibling_refs(unowned, &root_fields);
2527 }
2528 }
2529
2530 let root_id = self.module.add_node(Node::Table {
2531 fields: root_fields,
2532 });
2533 self.module.root = Some(root_id);
2534 }
2535
2536 fn parse_let(&mut self) {
2541 let let_tok = self.peek().clone();
2542 self.bump(); let name_tok = match self.expect(TokenKind::Ident) {
2544 Some(t) => t,
2545 None => return,
2546 };
2547 if self.expect(TokenKind::Assign).is_none() {
2548 return;
2549 }
2550 let expr = self.parse_expression();
2551 self.record_stmt(
2552 &name_tok.text,
2553 let_tok.line,
2554 self.last_end_line(),
2555 let_tok.col.saturating_sub(1) as usize,
2556 );
2557 self.extract_annotations(&name_tok.text, expr, let_tok.line);
2558 if self.in_include {
2559 self.module.bindings.entry(name_tok.text).or_insert(expr);
2562 } else {
2563 self.note_duplicate_key(
2564 &name_tok.text,
2565 Some(span_of(&name_tok)),
2566 self.module.bindings.contains_key(&name_tok.text),
2567 );
2568 self.module.bindings.insert(name_tok.text.clone(), expr);
2569 self.binding_spliced.remove(&name_tok.text);
2571 }
2572 }
2573
2574 fn parse_table_header(&mut self, parent: &mut indexmap::IndexMap<String, NodeId>) {
2579 let open = self.peek().clone();
2580 let header_level = open.col.saturating_sub(1) as usize;
2582 self.bump(); let name_tok = match self.expect(TokenKind::Ident) {
2584 Some(t) => t,
2585 None => return,
2586 };
2587 let mut segments = vec![name_tok.text.clone()];
2591 while matches!(self.peek_kind(), TokenKind::Dot) {
2592 self.bump(); match self.expect(TokenKind::Ident) {
2594 Some(t) => segments.push(t.text.clone()),
2595 None => return,
2596 }
2597 }
2598 let close = match self.expect(TokenKind::RBracket) {
2599 Some(t) => t,
2600 None => return,
2601 };
2602 self.brace_tables += 1;
2603 let mut scope: Option<NodeId> = None;
2609 for seg in &segments[..segments.len() - 1] {
2610 let found = match scope {
2611 None => parent.get(seg).copied(),
2612 Some(id) => match self.module.get(id) {
2613 Some(Node::Table { fields }) => fields.get(seg).copied(),
2614 _ => None,
2615 },
2616 };
2617 let next = match found {
2618 Some(id) => match self.module.get(id) {
2619 Some(Node::Table { .. }) => id,
2620 _ => {
2621 self.error(
2622 "E001",
2623 format!(
2624 "table path '{}' conflicts with a non-table value at '{}'",
2625 segments.join("."),
2626 seg
2627 ),
2628 );
2629 return;
2630 }
2631 },
2632 None => {
2633 let id = self.module.add_node(Node::Table {
2634 fields: indexmap::IndexMap::new(),
2635 });
2636 match scope {
2637 None => {
2638 parent.insert(seg.clone(), id);
2639 }
2640 Some(pid) => match self.module.get_mut(pid) {
2641 Some(Node::Table { fields }) => {
2642 fields.insert(seg.clone(), id);
2643 }
2644 _ => {
2645 self.error(
2646 "E001",
2647 format!(
2648 "table path '{}' conflicts with a non-table value",
2649 segments.join(".")
2650 ),
2651 );
2652 return;
2653 }
2654 },
2655 }
2656 id
2657 }
2658 };
2659 scope = Some(next);
2660 }
2661 let name = segments.join(".");
2662 let leaf = segments.last().cloned().unwrap_or_default();
2663 let stmt_idx = self.statements.len();
2664 self.statements.push(StmtInfo {
2665 key: name.clone(),
2666 path: self.path_str(),
2667 start_line: open.line,
2668 end_line: open.line,
2669 level: header_level,
2670 file: self.current_file_name(),
2671 });
2672 for seg in &segments {
2673 self.current_path.push(seg.clone());
2674 }
2675 self.blocks.push(BlockInfo {
2676 path: self.path_str(),
2677 level: header_level,
2678 start_line: open.line,
2679 file: self.current_file_name(),
2680 });
2681
2682 self.ident_scopes.push(Vec::new());
2683 let mut fields = indexmap::IndexMap::new();
2684 let mut pending = Vec::new();
2685 self.parse_body(&mut fields, BodyEnd::HeaderBody(header_level), &mut pending);
2686 self.splice_includes(&mut fields, pending, false);
2687 let idents = self.ident_scopes.pop().unwrap_or_default();
2688 let _ = self.resolve_sibling_refs(idents, &fields);
2689 let table_id = self.module.add_node(Node::Table { fields });
2693 self.module.record_span(
2694 table_id,
2695 Span {
2696 line: open.line,
2697 column: open.col,
2698 end_line: close.end_line,
2699 end_column: close.end_col,
2700 },
2701 );
2702 if let Some(file) = self.current_file_name() {
2703 self.module.record_file(table_id, file);
2704 }
2705 self.statements[stmt_idx].end_line = self.last_end_line();
2706 for _ in &segments {
2707 self.current_path.pop();
2708 }
2709 self.extract_annotations(&name, table_id, open.line);
2710 match scope {
2711 None => {
2712 self.note_duplicate_key(&leaf, Some(span_of(&open)), parent.contains_key(&leaf));
2713 parent.insert(leaf, table_id);
2714 }
2715 Some(pid) => {
2716 let existed = matches!(
2717 self.module.get(pid),
2718 Some(Node::Table { fields }) if fields.contains_key(&leaf)
2719 );
2720 self.note_duplicate_key(&leaf, Some(span_of(&open)), existed);
2721 match self.module.get_mut(pid) {
2722 Some(Node::Table { fields }) => {
2723 fields.insert(leaf, table_id);
2724 }
2725 _ => {
2726 self.error(
2727 "E001",
2728 format!(
2729 "table path '{}' conflicts with a non-table value",
2730 segments.join(".")
2731 ),
2732 );
2733 }
2734 }
2735 }
2736 }
2737 }
2738
2739 fn check_depends_on_statement(&mut self, key: &str, id: NodeId) -> bool {
2749 if key != "depends_on" || !matches!(self.module.get(id), Some(Node::Array { .. })) {
2750 return true;
2751 }
2752 if self.in_profile {
2753 if let Some(span) = self.module.node_spans.get(&id).cloned() {
2754 self.error_at_span("E022", "depends_on is not allowed inside profiles", &span);
2755 }
2756 return false;
2757 }
2758 true
2759 }
2760
2761 fn parse_key_value(&mut self) -> Option<(String, NodeId)> {
2762 let (key, id) = self.parse_key_value_sep(TokenKind::Assign)?;
2763 if key == "depends_on" && matches!(self.module.get(id), Some(Node::Array { .. })) {
2767 if let Some(span) = self.module.node_spans.get(&id).cloned() {
2768 self.error_at_span(
2769 "E022",
2770 "depends_on is only allowed as a direct table member",
2771 &span,
2772 );
2773 }
2774 }
2775 Some((key, id))
2776 }
2777
2778 fn parse_key_value_sep(&mut self, sep: TokenKind) -> Option<(String, NodeId)> {
2780 let key_tok = self.expect(TokenKind::Ident)?;
2781 let key = key_tok.text.clone();
2782 self.expect(sep)?;
2783 if key == "schema" && matches!(self.peek_kind(), TokenKind::LBrace) {
2786 if self.array_depth > 0 {
2787 self.error_at(
2788 "E020",
2789 "schemas may not appear inside arrays; describe element types with Array<T>",
2790 &key_tok,
2791 );
2792 return None;
2793 }
2794 if self.in_profile {
2795 self.error_at("E020", "schemas may not appear inside profiles", &key_tok);
2796 return None;
2797 }
2798 let node = self.parse_schema_inline(&key_tok);
2799 self.record_stmt(
2800 &key,
2801 key_tok.line,
2802 self.last_end_line(),
2803 key_tok.col.saturating_sub(1) as usize,
2804 );
2805 return Some((key, node));
2806 }
2807 let scope_start = self.ident_scopes.last().map_or(0, |s| s.len());
2810 let value = self.parse_expression();
2811 if let Some(scope) = self.ident_scopes.last_mut() {
2812 for entry in scope.iter_mut().skip(scope_start) {
2813 entry.0 = key.clone();
2814 }
2815 }
2816 Some((key, value))
2817 }
2818
2819 fn resolve_sibling_refs(
2825 &mut self,
2826 idents: Vec<(String, NodeId)>,
2827 fields: &indexmap::IndexMap<String, NodeId>,
2828 ) -> Vec<(String, NodeId)> {
2829 let mut leftover = Vec::new();
2830 for (owner_key, id) in idents {
2831 let name = match self.module.get(id) {
2832 Some(Node::Ident(n)) => n.clone(),
2833 _ => continue,
2834 };
2835 if name == owner_key {
2838 leftover.push((owner_key, id));
2839 continue;
2840 }
2841 if let Some(&target) = fields.get(&name) {
2842 if let Some(slot) = self.module.get_mut(id) {
2843 *slot = Node::Ref(target);
2844 }
2845 self.module.add_edge(target, id);
2846 } else {
2847 leftover.push((owner_key, id));
2848 }
2849 }
2850 leftover
2851 }
2852
2853 fn extract_annotations(&mut self, key: &str, node: NodeId, stmt_line: u32) {
2865 if matches!(self.module.get(node), Some(Node::SchemaTable { .. })) {
2866 return;
2867 }
2868 if self.in_profile {
2872 return;
2873 }
2874 let scope = self.path_str();
2875 let full_path = if scope.is_empty() {
2876 key.to_string()
2877 } else {
2878 format!("{scope}.{key}")
2879 };
2880 let here = self.current_file_name();
2885 let mut line = stmt_line.saturating_sub(1);
2886 let mut block: Vec<(String, Span)> = Vec::new();
2887 while line >= 1 {
2888 let comment = self
2889 .comments
2890 .iter()
2891 .zip(self.comment_files.iter())
2892 .find(|((_, s), f)| s.line == line && *f == &here)
2893 .map(|((text, span), _)| (text.clone(), span.clone()));
2894 let Some((text, span)) = comment else {
2895 break;
2896 };
2897 if self
2898 .statements
2899 .iter()
2900 .any(|st| st.end_line == line && st.file == here)
2901 {
2902 break;
2903 }
2904 block.push((text, span));
2905 if line == 1 {
2906 break;
2907 }
2908 line -= 1;
2909 }
2910 for (text, span) in block.into_iter().rev() {
2912 if let Some(kind) = self.parse_annotation_line(&text, &span, node) {
2913 self.module
2914 .annotations
2915 .entry(node)
2916 .or_default()
2917 .push(Annotation {
2918 kind,
2919 path: full_path.clone(),
2920 span,
2921 });
2922 }
2923 }
2924 }
2925
2926 fn parse_annotation_line(
2930 &mut self,
2931 text: &str,
2932 span: &Span,
2933 target: NodeId,
2934 ) -> Option<AnnotationKind> {
2935 let body = text.strip_prefix('#').unwrap_or(text).trim_start();
2936 let rest = body.strip_prefix('@')?;
2937 let kind_len = rest
2938 .chars()
2939 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
2940 .count();
2941 let (kind, after) = rest.split_at(kind_len);
2942 match kind {
2943 "intent" | "constraint" | "ai_immutable" => {}
2944 _ => return None,
2945 }
2946 if kind == "ai_immutable" && after.trim_start().is_empty() {
2951 return Some(AnnotationKind::AiImmutable(true));
2952 }
2953 let payload = match after.trim_start().strip_prefix(':') {
2954 Some(p) => p.trim_start(),
2955 None => {
2956 self.error_at_span(
2957 "E021",
2958 format!("invalid @{kind} annotation: expected ':' after the kind"),
2959 span,
2960 );
2961 return None;
2962 }
2963 };
2964 match kind {
2965 "intent" => {
2970 let wants_literal = payload.starts_with('"') || payload.starts_with('\'');
2971 match parse_annotation_string(payload) {
2972 Ok(documented) => Some(AnnotationKind::Intent(documented)),
2973 Err(_) if !wants_literal && !payload.is_empty() => {
2974 Some(AnnotationKind::Intent(payload.to_string()))
2975 }
2976 Err(detail) => {
2977 self.error_at_span(
2978 "E021",
2979 format!("invalid @intent annotation: {detail}"),
2980 span,
2981 );
2982 None
2983 }
2984 }
2985 }
2986 "ai_immutable" => match payload {
2987 "true" => Some(AnnotationKind::AiImmutable(true)),
2988 "false" => Some(AnnotationKind::AiImmutable(false)),
2989 _ => {
2990 self.error_at_span(
2991 "E021",
2992 "invalid @ai_immutable annotation: expected 'true' or 'false'",
2993 span,
2994 );
2995 None
2996 }
2997 },
2998 "constraint" => {
2999 if payload.is_empty() {
3000 self.error_at_span(
3001 "E021",
3002 "invalid @constraint annotation: empty predicate",
3003 span,
3004 );
3005 return None;
3006 }
3007 let payload_col =
3010 span.column + (text.chars().count() - payload.chars().count()) as u32;
3011 self.parse_constraint(payload, span, payload_col, target)
3012 .map(|expr| AnnotationKind::Constraint {
3013 source: payload.to_string(),
3014 expr,
3015 })
3016 }
3017 _ => None,
3018 }
3019 }
3020
3021 fn parse_constraint(
3029 &mut self,
3030 payload: &str,
3031 comment: &Span,
3032 payload_col: u32,
3033 target: NodeId,
3034 ) -> Option<NodeId> {
3035 let mut lexer = Lexer::new(payload);
3036 let lexed = lexer.tokenize();
3037 let module = std::mem::replace(&mut self.module, Module::new(""));
3038 let inherited_len = self.ident_scopes.last().map_or(0, |s| s.len());
3039 let mut sub = Parser {
3040 tokens: lexed.tokens,
3041 pos: 0,
3042 module,
3043 errors: Vec::new(),
3044 ident_scopes: self.ident_scopes.clone(),
3045 depth: self.depth,
3046 options: self.options,
3047 block_depth: 0,
3048 schema_depth: self.schema_depth,
3049 array_depth: self.array_depth,
3050 comments: Vec::new(),
3051 comment_files: Vec::new(),
3052 statements: Vec::new(),
3053 blocks: Vec::new(),
3054 current_path: self.current_path.clone(),
3055 brace_tables: 0,
3056 indent_tables: 0,
3057 current_file: self.current_file.clone(),
3058 include_stack: Vec::new(),
3059 in_include: self.in_include,
3060 in_profile: self.in_profile,
3061 binding_spliced: self.binding_spliced.clone(),
3062 pending_profiles: Vec::new(),
3063 trust: self.trust.clone(),
3064 };
3065 let node_base = sub.module.arena.len() as u32;
3066 let id = sub.parse_expression();
3067 while matches!(sub.peek_kind(), TokenKind::Newline) {
3068 sub.bump();
3069 }
3070 if !matches!(sub.peek_kind(), TokenKind::Eof) {
3071 let leftover = sub.peek().clone();
3072 sub.error_at(
3073 "E021",
3074 format!(
3075 "invalid @constraint annotation: unexpected {:?} after the predicate",
3076 leftover.kind
3077 ),
3078 &leftover,
3079 );
3080 }
3081 let mut rewritten = std::collections::HashSet::new();
3084 for i in node_base..sub.module.arena.len() as u32 {
3085 let nid = NodeId(i);
3086 if matches!(sub.module.get(nid), Some(Node::Ident(name)) if name == "value") {
3087 if let Some(slot) = sub.module.get_mut(nid) {
3088 *slot = Node::Ref(target);
3089 }
3090 sub.module.add_edge(target, nid);
3091 rewritten.insert(nid);
3092 }
3093 }
3094 let new_idents: Vec<(String, NodeId)> = sub
3095 .ident_scopes
3096 .last()
3097 .map(|s| {
3098 s[inherited_len.min(s.len())..]
3099 .iter()
3100 .filter(|(_, nid)| !rewritten.contains(nid))
3101 .cloned()
3102 .collect()
3103 })
3104 .unwrap_or_default();
3105 let lift = |line_1: u32, col_1: u32| -> (u32, u32) {
3109 let line = comment.line + line_1.saturating_sub(1);
3110 let col = if line_1 <= 1 {
3111 payload_col + col_1.saturating_sub(1)
3112 } else {
3113 col_1
3114 };
3115 (line, col)
3116 };
3117 for (node, span) in sub.module.node_spans.iter_mut() {
3118 if node.0 >= node_base {
3119 let (line, col) = lift(span.line, span.column);
3120 span.line = line;
3121 span.column = col;
3122 let (line, col) = lift(span.end_line, span.end_column);
3123 span.end_line = line;
3124 span.end_column = col;
3125 }
3126 }
3127 let mut sub_errors = std::mem::take(&mut sub.errors);
3128 let mut sub_lex_errors = lexed.errors;
3129 for d in sub_errors.iter_mut().chain(sub_lex_errors.iter_mut()) {
3130 if let Some(s) = &mut d.span {
3131 let (line, col) = lift(s.line, s.column);
3132 s.line = line;
3133 s.column = col;
3134 let (line, col) = lift(s.end_line, s.end_column);
3135 s.end_line = line;
3136 s.end_column = col;
3137 }
3138 if d.file.is_none() {
3139 d.file = self.current_file_name();
3140 }
3141 }
3142 self.errors.extend(sub_lex_errors);
3143 self.errors.extend(sub_errors);
3144 self.module = std::mem::replace(&mut sub.module, Module::new(""));
3145 if let Some(scope) = self.ident_scopes.last_mut() {
3146 scope.extend(new_idents);
3147 }
3148 Some(id)
3149 }
3150
3151 fn parse_expression(&mut self) -> NodeId {
3156 let guard_tok = self.peek().clone();
3157 self.depth += 1;
3158 if self.depth > MAX_EXPR_DEPTH {
3159 self.error(
3160 "E004",
3161 format!("maximum expression nesting depth ({MAX_EXPR_DEPTH}) exceeded"),
3162 );
3163 self.depth -= 1;
3164 self.bump(); return self.add_spanned(Node::Literal(Value::Null), &guard_tok);
3166 }
3167 let node = self.parse_or();
3168 self.depth -= 1;
3169 node
3170 }
3171
3172 fn parse_or(&mut self) -> NodeId {
3173 let mut left = self.parse_and();
3174 while matches!(self.peek_kind(), TokenKind::OrOr) {
3175 let op_tok = self.bump();
3176 let right = self.parse_and();
3177 left = self.add_spanned(
3178 Node::Binary {
3179 op: BinOp::Or,
3180 lhs: left,
3181 rhs: right,
3182 },
3183 &op_tok,
3184 );
3185 }
3186 left
3187 }
3188
3189 fn parse_and(&mut self) -> NodeId {
3190 let mut left = self.parse_equality();
3191 while matches!(self.peek_kind(), TokenKind::AndAnd) {
3192 let op_tok = self.bump();
3193 let right = self.parse_equality();
3194 left = self.add_spanned(
3195 Node::Binary {
3196 op: BinOp::And,
3197 lhs: left,
3198 rhs: right,
3199 },
3200 &op_tok,
3201 );
3202 }
3203 left
3204 }
3205
3206 fn parse_equality(&mut self) -> NodeId {
3207 let mut left = self.parse_comparison();
3208 loop {
3209 let op = match self.peek_kind() {
3210 TokenKind::EqEq => BinOp::Eq,
3211 TokenKind::NotEq => BinOp::Ne,
3212 _ => break,
3213 };
3214 let op_tok = self.bump();
3215 let right = self.parse_comparison();
3216 left = self.add_spanned(
3217 Node::Binary {
3218 op,
3219 lhs: left,
3220 rhs: right,
3221 },
3222 &op_tok,
3223 );
3224 }
3225 left
3226 }
3227
3228 fn parse_comparison(&mut self) -> NodeId {
3229 let mut left = self.parse_term();
3230 loop {
3231 let op = match self.peek_kind() {
3232 TokenKind::Lt => BinOp::Lt,
3233 TokenKind::LtEq => BinOp::Le,
3234 TokenKind::Gt => BinOp::Gt,
3235 TokenKind::GtEq => BinOp::Ge,
3236 _ => break,
3237 };
3238 let op_tok = self.bump();
3239 let right = self.parse_term();
3240 left = self.add_spanned(
3241 Node::Binary {
3242 op,
3243 lhs: left,
3244 rhs: right,
3245 },
3246 &op_tok,
3247 );
3248 }
3249 left
3250 }
3251
3252 fn parse_term(&mut self) -> NodeId {
3253 let mut left = self.parse_factor();
3254 loop {
3255 let op = match self.peek_kind() {
3256 TokenKind::Plus => BinOp::Add,
3257 TokenKind::Minus => BinOp::Sub,
3258 _ => break,
3259 };
3260 let op_tok = self.bump();
3261 let right = self.parse_factor();
3262 left = self.add_spanned(
3263 Node::Binary {
3264 op,
3265 lhs: left,
3266 rhs: right,
3267 },
3268 &op_tok,
3269 );
3270 }
3271 left
3272 }
3273
3274 fn parse_factor(&mut self) -> NodeId {
3275 let mut left = self.parse_unary();
3276 loop {
3277 let op = match self.peek_kind() {
3278 TokenKind::Star => BinOp::Mul,
3279 TokenKind::Slash => BinOp::Div,
3280 _ => break,
3281 };
3282 let op_tok = self.bump();
3283 let right = self.parse_unary();
3284 left = self.add_spanned(
3285 Node::Binary {
3286 op,
3287 lhs: left,
3288 rhs: right,
3289 },
3290 &op_tok,
3291 );
3292 }
3293 left
3294 }
3295
3296 fn parse_unary(&mut self) -> NodeId {
3297 match self.peek_kind() {
3298 TokenKind::Minus | TokenKind::Bang => {
3299 let op_tok = self.peek().clone();
3300 self.depth += 1;
3303 if self.depth > MAX_EXPR_DEPTH {
3304 self.error(
3305 "E004",
3306 format!("maximum expression nesting depth ({MAX_EXPR_DEPTH}) exceeded"),
3307 );
3308 self.depth -= 1;
3309 self.bump(); return self.add_spanned(Node::Literal(Value::Null), &op_tok);
3311 }
3312 let op = if matches!(self.peek_kind(), TokenKind::Minus) {
3313 UnOp::Neg
3314 } else {
3315 UnOp::Not
3316 };
3317 self.bump();
3318 let expr = self.parse_unary();
3319 self.depth -= 1;
3320 if op == UnOp::Neg {
3326 if let Some(folded) = self.fold_affine_negation(expr) {
3327 return folded;
3328 }
3329 }
3330 self.add_spanned(Node::Unary { op, expr }, &op_tok)
3331 }
3332 _ => self.parse_primary(),
3333 }
3334 }
3335
3336 fn fold_affine_negation(&mut self, expr: NodeId) -> Option<NodeId> {
3344 let value_id = match self.module.get(expr) {
3345 Some(Node::Quantity {
3346 value,
3347 unit: Unit::Celsius | Unit::Fahrenheit,
3348 }) => *value,
3349 _ => return None,
3350 };
3351 match self.module.get_mut(value_id) {
3352 Some(Node::Literal(Value::Int(n))) => {
3353 *n = n.saturating_neg();
3354 }
3355 Some(Node::Literal(Value::Float(f))) => {
3356 *f = -*f;
3357 }
3358 _ => return None,
3359 }
3360 Some(expr)
3361 }
3362
3363 fn parse_primary(&mut self) -> NodeId {
3368 let tok = self.peek().clone();
3369 match &tok.kind {
3370 TokenKind::String(src_pos) => {
3371 self.bump();
3372 let linemap = linemap_from_charmap(&tok.text, tok.line, src_pos);
3373 self.parse_string_literal(&tok.text, &tok, Some(&linemap))
3374 }
3375
3376 TokenKind::SingleString => {
3377 self.bump();
3380 let text = tok.text.clone();
3381 self.add_spanned(Node::Literal(Value::String(text)), &tok)
3382 }
3383
3384 TokenKind::MlString(src_pos) => {
3385 self.bump();
3391 let (stripped, linemap) =
3392 strip_common_indent_mapped(&tok.text, tok.line, tok.col, 3, Some(src_pos));
3393 self.parse_string_literal(&stripped, &tok, Some(&linemap))
3394 }
3395
3396 TokenKind::MlSingleString => {
3397 self.bump();
3400 let stripped = strip_common_indent(&tok.text);
3401 self.add_spanned(Node::Literal(Value::String(stripped)), &tok)
3402 }
3403
3404 TokenKind::At => {
3405 self.bump();
3406 if matches!(self.peek_kind(), TokenKind::Ident) {
3408 let tag_tok = self.peek().clone();
3409 let tag = tag_tok.text.clone();
3410 if (tag == "patch" || tag == "remove" || tag == "rename")
3411 && matches!(self.peek_kind_at(1), TokenKind::LParen)
3412 {
3413 self.error_at(
3414 "E019",
3415 "patch statements may only appear at document root, never as values",
3416 &tag_tok,
3417 );
3418 return self.add_spanned(Node::Literal(Value::Null), &tok);
3419 }
3420 }
3421 self.parse_semantic_block(&tok)
3422 }
3423
3424 TokenKind::Integer => {
3425 self.bump();
3426 let n: i64 = match tok.text.parse() {
3432 Ok(n) => n,
3433 Err(_) => {
3434 self.error_at(
3435 "E023",
3436 format!("integer literal out of i64 range: `{}`", tok.text),
3437 &tok,
3438 );
3439 i64::MAX
3440 }
3441 };
3442 let mut node = self.add_spanned(Node::Literal(Value::Int(n)), &tok);
3443 if let TokenKind::Unit(_) = self.peek_kind() {
3445 let unit_tok = self.bump();
3446 if let Some(unit) = Unit::from_suffix(&unit_tok.text) {
3447 node = self.add_spanned(Node::Quantity { value: node, unit }, &tok);
3448 }
3449 }
3450 node
3451 }
3452
3453 TokenKind::Float => {
3454 self.bump();
3455 let f: f64 = tok.text.parse().unwrap_or(0.0);
3456 let mut node = self.add_spanned(Node::Literal(Value::Float(f)), &tok);
3457 if let TokenKind::Unit(_) = self.peek_kind() {
3458 let unit_tok = self.bump();
3459 if let Some(unit) = Unit::from_suffix(&unit_tok.text) {
3460 node = self.add_spanned(Node::Quantity { value: node, unit }, &tok);
3461 }
3462 }
3463 node
3464 }
3465
3466 TokenKind::True => {
3467 self.bump();
3468 self.add_spanned(Node::Literal(Value::Bool(true)), &tok)
3469 }
3470 TokenKind::False => {
3471 self.bump();
3472 self.add_spanned(Node::Literal(Value::Bool(false)), &tok)
3473 }
3474 TokenKind::Null => {
3475 self.bump();
3476 self.add_spanned(Node::Literal(Value::Null), &tok)
3477 }
3478
3479 TokenKind::Env => {
3480 self.bump();
3481 self.expect(TokenKind::Dot);
3482 let var = self
3483 .expect(TokenKind::Ident)
3484 .map(|t| t.text)
3485 .unwrap_or_default();
3486 let default = if matches!(self.peek_kind(), TokenKind::QuestionColon) {
3487 self.bump();
3488 Some(self.parse_expression())
3489 } else {
3490 None
3491 };
3492 self.add_spanned(Node::Env { name: var, default }, &tok)
3493 }
3494
3495 TokenKind::Secret => {
3496 self.bump();
3497 let from_env = if matches!(self.peek_kind(), TokenKind::Dot) {
3501 self.bump();
3502 if !matches!(self.peek_kind(), TokenKind::Env) {
3503 self.error(
3504 "E003",
3505 "expected `env` after `secret.` (use secret.env(name))",
3506 );
3507 } else {
3508 self.bump();
3509 }
3510 true
3511 } else {
3512 false
3513 };
3514 self.expect(TokenKind::LParen);
3515 let arg = self.parse_expression();
3516 self.expect(TokenKind::RParen);
3517 let locator = if from_env {
3518 match self.module.get(arg) {
3519 Some(Node::Literal(Value::String(name))) => self.add_spanned(
3520 Node::Env {
3521 name: name.clone(),
3522 default: None,
3523 },
3524 &tok,
3525 ),
3526 _ => {
3527 self.error(
3528 "E003",
3529 "secret.env() requires a string literal env var name",
3530 );
3531 arg
3532 }
3533 }
3534 } else {
3535 arg
3536 };
3537 self.add_spanned(Node::Secret { locator }, &tok)
3538 }
3539
3540 TokenKind::Ident => {
3541 self.bump();
3542 let text = tok.text.clone();
3546 let id = self.add_spanned(Node::Ident(text), &tok);
3547 if let Some(scope) = self.ident_scopes.last_mut() {
3548 scope.push((String::new(), id));
3549 }
3550 id
3551 }
3552
3553 TokenKind::LBracket => self.parse_array(),
3554 TokenKind::LBrace => self.parse_inline_table(),
3555
3556 TokenKind::LParen => {
3557 self.bump();
3558 let expr = self.parse_expression();
3559 self.expect(TokenKind::RParen);
3560 expr
3561 }
3562
3563 _ => {
3564 self.error("E002", format!("unexpected token {:?}", tok.kind));
3565 self.bump();
3566 self.module.add_node(Node::Literal(Value::Null))
3567 }
3568 }
3569 }
3570
3571 fn parse_semantic_block(&mut self, at_tok: &Token) -> NodeId {
3575 let tag_tok = self.peek().clone();
3576 let tag = match &tag_tok.kind {
3577 TokenKind::Ident => {
3578 self.bump();
3579 tag_tok.text.clone()
3580 }
3581 _ => {
3582 self.error_at(
3583 "E014",
3584 "expected a semantic tag (prompt, policy, template) after `@`",
3585 &tag_tok,
3586 );
3587 String::new()
3588 }
3589 };
3590 let tag = match SemanticTag::parse_tag(&tag) {
3591 Some(t) => t,
3592 None => {
3593 self.error_at(
3594 "E014",
3595 format!("unknown semantic tag `@{tag}` (expected prompt, policy, or template)"),
3596 &tag_tok,
3597 );
3598 SemanticTag::Prompt
3599 }
3600 };
3601 let str_tok = self.peek().clone();
3602 let inner = match &str_tok.kind {
3603 TokenKind::MlString(src_pos) => {
3604 self.bump();
3605 let (stripped, linemap) = strip_common_indent_mapped(
3606 &str_tok.text,
3607 str_tok.line,
3608 str_tok.col,
3609 3,
3610 Some(src_pos),
3611 );
3612 self.parse_string_literal(&stripped, &str_tok, Some(&linemap))
3613 }
3614 TokenKind::MlSingleString => {
3615 self.bump();
3616 let stripped = strip_common_indent(&str_tok.text);
3617 self.add_spanned(Node::Literal(Value::String(stripped)), &str_tok)
3618 }
3619 _ => {
3620 self.error_at(
3621 "E014",
3622 "a semantic tag must be followed by a multi-line string (`\"\"\"…\"\"\"` or `'''…'''`)",
3623 &str_tok,
3624 );
3625 self.add_spanned(Node::Literal(Value::String(String::new())), at_tok)
3626 }
3627 };
3628 let id = self.add_spanned(Node::Semantic { tag, inner }, at_tok);
3629 id
3632 }
3633}
3634
3635fn strip_common_indent(raw: &str) -> String {
3642 strip_common_indent_mapped(raw, 1, 1, 0, None).0
3643}
3644
3645#[derive(Debug, Clone)]
3648struct MlLineMap {
3649 src_line: u32,
3650 src_cols: Vec<u32>,
3651}
3652
3653fn linemap_from_charmap(text: &str, tok_line: u32, charmap: &[(u32, u32)]) -> Vec<MlLineMap> {
3658 let chars: Vec<char> = text.chars().collect();
3659 let mut lines: Vec<(usize, usize)> = Vec::new();
3660 let mut start = 0usize;
3661 for (i, c) in chars.iter().enumerate() {
3662 if *c == '\n' {
3663 lines.push((start, i));
3664 start = i + 1;
3665 }
3666 }
3667 lines.push((start, chars.len()));
3668 lines
3669 .iter()
3670 .map(|&(s, e)| {
3671 let src_line = tok_line + charmap.get(s).map(|&(off, _)| off).unwrap_or(0);
3672 let src_cols = (s..e)
3673 .map(|k| charmap.get(k).map(|&(_, c)| c).unwrap_or(0))
3674 .collect();
3675 MlLineMap { src_line, src_cols }
3676 })
3677 .collect()
3678}
3679
3680fn strip_common_indent_mapped(
3686 raw: &str,
3687 tok_line: u32,
3688 tok_col: u32,
3689 delim_len: u32,
3690 src_pos: Option<&[(u32, u32)]>,
3691) -> (String, Vec<MlLineMap>) {
3692 let mut lines: Vec<&str> = raw.split('\n').collect();
3693 let mut kept: Vec<usize> = (0..lines.len()).collect();
3697 if !lines.is_empty() && lines[0].trim().is_empty() {
3698 kept.remove(0);
3699 lines.remove(0);
3700 }
3701 if !lines.is_empty() && lines.last().is_some_and(|l| l.trim().is_empty()) {
3702 kept.pop();
3703 lines.pop();
3704 }
3705 let mut prefix: Vec<char> = Vec::new();
3707 let mut seen_content = false;
3708 for line in lines.iter() {
3709 if line.trim().is_empty() {
3710 continue;
3711 }
3712 let indent: Vec<char> = line
3713 .chars()
3714 .take_while(|c| *c == ' ' || *c == '\t')
3715 .collect();
3716 if !seen_content {
3717 prefix = indent;
3718 seen_content = true;
3719 } else {
3720 let shared = prefix
3721 .iter()
3722 .zip(indent.iter())
3723 .take_while(|(a, b)| a == b)
3724 .count();
3725 prefix.truncate(shared);
3726 }
3727 if prefix.is_empty() {
3728 break;
3729 }
3730 }
3731 let mut out = Vec::with_capacity(lines.len());
3732 let mut map = Vec::with_capacity(lines.len());
3733 let all: Vec<&str> = raw.split('\n').collect();
3738 let mut line_start: Vec<usize> = Vec::with_capacity(all.len());
3739 let mut acc = 0usize;
3740 for l in &all {
3741 line_start.push(acc);
3742 acc += l.chars().count() + 1; }
3744 let line_start_for =
3745 |raw_idx: usize| -> usize { line_start.get(raw_idx).copied().unwrap_or(0) };
3746 for (j, l) in lines.iter().enumerate() {
3747 let raw_idx = kept[j];
3748 let decoded_start = line_start_for(raw_idx);
3749 let at = |k: usize| -> (u32, u32) {
3750 src_pos
3751 .and_then(|p| p.get(k).copied())
3752 .unwrap_or((raw_idx as u32, k as u32))
3753 };
3754 let shared = if l.trim().is_empty() {
3755 0
3756 } else {
3757 prefix
3758 .iter()
3759 .zip(l.chars())
3760 .take_while(|(a, b)| *a == b)
3761 .count()
3762 };
3763 if l.trim().is_empty() {
3765 let (line_off, _) = at(decoded_start);
3766 map.push(MlLineMap {
3767 src_line: tok_line + line_off,
3768 src_cols: Vec::new(),
3769 });
3770 out.push(String::new());
3771 continue;
3772 }
3773 let text: String = l.chars().skip(shared).collect();
3774 let first_kept = decoded_start + shared;
3776 let (line_off, _) = at(first_kept.min(decoded_start + l.chars().count()));
3777 let src_line = tok_line + line_off;
3778 let cols: Vec<u32> = if src_pos.is_some() {
3779 (0..text.chars().count())
3780 .map(|c| at(first_kept + c).1)
3781 .collect()
3782 } else if raw_idx == 0 {
3783 (0..text.chars().count())
3785 .map(|c| tok_col + delim_len + shared as u32 + c as u32)
3786 .collect()
3787 } else {
3788 (0..text.chars().count())
3789 .map(|c| shared as u32 + 1 + c as u32)
3790 .collect()
3791 };
3792 out.push(text);
3793 map.push(MlLineMap {
3794 src_line,
3795 src_cols: cols,
3796 });
3797 }
3798 (out.join("\n"), map)
3799}
3800
3801impl Parser {
3802 fn parse_patch_statement(&mut self, at_tok: &Token) {
3812 let nested = !self.current_path.is_empty();
3813 self.bump(); let tag_tok = self.bump(); let tag = tag_tok.text.clone();
3816 if self.expect(TokenKind::LParen).is_none() {
3817 return;
3818 }
3819 let path_tok = self.peek().clone();
3820 let path = match &path_tok.kind {
3821 TokenKind::String(_) | TokenKind::SingleString => {
3822 self.bump();
3823 path_tok.text.clone()
3824 }
3825 _ => {
3826 self.error_at("E019", "patch paths must be string literals", &path_tok);
3827 return;
3828 }
3829 };
3830 if path.contains("${") || path.contains(ESCAPED_DOLLAR) {
3831 self.error_at(
3832 "E019",
3833 "interpolation in patch paths is a hard error; paths must be static",
3834 &path_tok,
3835 );
3836 return;
3837 }
3838 if let Err(why) = validate_patch_path(&path) {
3839 self.error_at(
3840 "E019",
3841 format!("invalid patch path `{path}`: {why}"),
3842 &path_tok,
3843 );
3844 return;
3845 }
3846 let op = match tag.as_str() {
3847 "remove" => {
3848 if self.expect(TokenKind::RParen).is_none() {
3849 return;
3850 }
3851 PatchOp::Remove { path: path.clone() }
3852 }
3853 "rename" => {
3854 if self.expect(TokenKind::Comma).is_none() {
3855 return;
3856 }
3857 let new_tok = self.peek().clone();
3858 let new_name = match &new_tok.kind {
3859 TokenKind::String(_) | TokenKind::SingleString => {
3860 self.bump();
3861 new_tok.text.clone()
3862 }
3863 _ => {
3864 self.error_at(
3865 "E019",
3866 "rename needs a new key name as a string literal",
3867 &new_tok,
3868 );
3869 return;
3870 }
3871 };
3872 if new_name.contains('.') || new_name.contains('[') || new_name.is_empty() {
3873 self.error_at(
3874 "E019",
3875 "rename target must be a plain key name in the same parent",
3876 &new_tok,
3877 );
3878 return;
3879 }
3880 if self.expect(TokenKind::RParen).is_none() {
3881 return;
3882 }
3883 PatchOp::Rename {
3884 from: path.clone(),
3885 to: new_name,
3886 }
3887 }
3888 _ => {
3889 if self.expect(TokenKind::Comma).is_none() {
3891 return;
3892 }
3893 let value = self.parse_expression();
3894 if self.expect(TokenKind::RParen).is_none() {
3895 return;
3896 }
3897 PatchOp::Set {
3898 path: path.clone(),
3899 value,
3900 }
3901 }
3902 };
3903 let stmt_key = match &op {
3904 PatchOp::Set { path, .. } => format!("@patch({path})"),
3905 PatchOp::Remove { path } => format!("@remove({path})"),
3906 PatchOp::Rename { from, .. } => format!("@rename({from})"),
3907 };
3908 if nested {
3909 self.error_at(
3910 "E019",
3911 "patch statements may only appear at document root",
3912 at_tok,
3913 );
3914 return;
3915 }
3916 self.record_stmt(
3917 &stmt_key,
3918 at_tok.line,
3919 self.last_end_line(),
3920 at_tok.col.saturating_sub(1) as usize,
3921 );
3922 let id = self.add_spanned(Node::PatchDirective { op }, at_tok);
3923 if let Some(Node::PatchDirective {
3926 op: PatchOp::Set { value, .. },
3927 }) = self.module.get(id)
3928 {
3929 let value = *value;
3930 self.module.add_edge(value, id);
3931 }
3932 self.module.patches.push(id);
3933 }
3934
3935 fn parse_schema_inline(&mut self, key_tok: &Token) -> NodeId {
3942 let open = self
3943 .expect(TokenKind::LBrace)
3944 .unwrap_or_else(|| key_tok.clone());
3945 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
3946 self.add_spanned(Node::SchemaTable { fields, closed }, &open)
3947 }
3948
3949 fn parse_schema_brace(&mut self, key_tok: &Token) -> NodeId {
3951 let open = self
3952 .expect(TokenKind::LBrace)
3953 .unwrap_or_else(|| key_tok.clone());
3954 self.brace_tables += 1;
3955 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
3956 self.add_spanned(Node::SchemaTable { fields, closed }, &open)
3957 }
3958
3959 fn parse_schema_indent(&mut self, key_tok: &Token, _level: usize) -> NodeId {
3961 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Indent);
3962 self.add_spanned(Node::SchemaTable { fields, closed }, key_tok)
3963 }
3964
3965 fn parse_schema_entries(&mut self, end: SchemaEnd) -> (IndexMap<String, NodeId>, bool) {
3968 let mut fields = IndexMap::new();
3969 let mut closed = false;
3970 loop {
3971 self.skip_newlines();
3972 match self.peek_kind() {
3973 TokenKind::Eof => {
3974 if matches!(end, SchemaEnd::Brace) {
3978 self.error(
3979 "E001",
3980 "unclosed schema block: expected `}` before end of file",
3981 );
3982 }
3983 break;
3984 }
3985 TokenKind::RBrace if matches!(end, SchemaEnd::Brace) => {
3986 self.bump();
3987 break;
3988 }
3989 TokenKind::Dedent if matches!(end, SchemaEnd::Indent) => {
3990 self.bump();
3991 break;
3992 }
3993 TokenKind::RBrace | TokenKind::Dedent => {
3994 break;
3997 }
3998 TokenKind::Comma => {
3999 self.bump();
4000 continue;
4001 }
4002 TokenKind::Ident => {
4003 let name_tok = self.bump();
4004 let name = name_tok.text.clone();
4005 if self.expect(TokenKind::Assign).is_none() {
4006 continue;
4007 }
4008 if name == "closed" {
4009 match self.peek_kind() {
4010 TokenKind::True => {
4011 self.bump();
4012 closed = true;
4013 }
4014 TokenKind::False => {
4015 self.bump();
4016 closed = false;
4017 }
4018 _ => {
4019 let tok = self.peek().clone();
4020 self.error_at("E020", "closed must be true or false", &tok);
4021 }
4022 }
4023 self.skip_newlines();
4024 if matches!(self.peek_kind(), TokenKind::Comma) {
4025 self.bump();
4026 }
4027 continue;
4028 }
4029 let id = self.parse_schema_type(&name_tok);
4030 if self.schema_depth == 0 {
4041 self.record_stmt(
4042 &name,
4043 name_tok.line,
4044 self.last_end_line(),
4045 name_tok.col.saturating_sub(1) as usize,
4046 );
4047 }
4048 fields.insert(name, id);
4049 self.skip_newlines();
4050 if matches!(self.peek_kind(), TokenKind::Comma) {
4051 self.bump();
4052 }
4053 }
4054 _ => {
4055 let tok = self.peek().clone();
4056 self.error_at("E020", "expected a schema entry (`name = Type`)", &tok);
4057 self.bump();
4058 }
4059 }
4060 }
4061 (fields, closed)
4062 }
4063
4064 fn enter_schema_depth(&mut self, tok: &Token) -> bool {
4071 self.schema_depth += 1;
4072 if self.schema_depth > MAX_SCHEMA_DEPTH {
4073 self.schema_depth -= 1;
4074 self.error_at(
4075 "E004",
4076 format!("maximum schema nesting depth ({MAX_SCHEMA_DEPTH}) exceeded"),
4077 tok,
4078 );
4079 return true;
4080 }
4081 false
4082 }
4083
4084 fn exit_schema_depth(&mut self) {
4085 self.schema_depth = self.schema_depth.saturating_sub(1);
4086 }
4087
4088 fn parse_schema_type(&mut self, _name_tok: &Token) -> NodeId {
4092 let tok = self.peek().clone();
4093 let TokenKind::Ident = &tok.kind else {
4094 self.error_at("E020", "expected a schema type", &tok);
4095 self.bump();
4096 return self.fallback_schema_field(&tok);
4097 };
4098 let type_name = tok.text.clone();
4099 if type_name == "Array" {
4101 self.bump();
4102 if self.expect(TokenKind::Lt).is_none() {
4103 self.error_at("E020", "Array needs an element type: Array<T>", &tok);
4104 return self.fallback_schema_field(&tok);
4105 }
4106 let elem_tok = self.peek().clone();
4107 if self.enter_schema_depth(&tok) {
4108 return self.fallback_schema_field(&tok);
4109 }
4110 let element = self.parse_schema_type(&elem_tok);
4111 self.exit_schema_depth();
4112 if self.expect(TokenKind::Gt).is_none() {
4113 return self.fallback_schema_field(&tok);
4114 }
4115 return self.add_spanned(Node::SchemaArray { element }, &tok);
4116 }
4117 if type_name == "Enum" {
4119 self.bump();
4120 if self.expect(TokenKind::LParen).is_none() {
4121 self.error_at("E020", "Enum needs a value list: Enum(\"a\", ...)", &tok);
4122 return self.fallback_schema_field(&tok);
4123 }
4124 let mut values = Vec::new();
4125 let mut deprecated: Option<String> = None;
4126 loop {
4127 self.skip_newlines();
4128 match self.peek_kind() {
4129 TokenKind::RParen | TokenKind::Eof => break,
4130 TokenKind::Comma => {
4131 self.bump();
4132 continue;
4133 }
4134 TokenKind::String(_) | TokenKind::SingleString => {
4135 let v = self.bump();
4136 values.push(v.text.clone());
4137 }
4138 TokenKind::Ident if self.peek().text == "deprecated" => {
4139 self.bump();
4143 if self.expect(TokenKind::Assign).is_none() {
4144 continue;
4145 }
4146 deprecated = match self.peek_kind() {
4147 TokenKind::True => {
4148 self.bump();
4149 Some(String::new())
4150 }
4151 TokenKind::String(_) | TokenKind::SingleString => {
4152 Some(self.bump().text.clone())
4153 }
4154 _ => {
4155 let bad = self.peek().clone();
4156 self.error_at(
4157 "E020",
4158 "deprecated takes `true` or a string message",
4159 &bad,
4160 );
4161 None
4162 }
4163 };
4164 }
4165 _ => {
4166 let bad = self.peek().clone();
4167 self.error_at("E020", "Enum values must be string literals", &bad);
4168 self.bump();
4169 }
4170 }
4171 self.skip_newlines();
4172 if matches!(self.peek_kind(), TokenKind::Comma) {
4173 self.bump();
4174 }
4175 }
4176 self.expect(TokenKind::RParen);
4177 if values.is_empty() {
4178 self.error_at("E020", "Enum needs at least one value", &tok);
4179 }
4180 let field = SchemaField {
4181 typ: ActiveType::Enum(values),
4182 min: None,
4183 max: None,
4184 min_len: None,
4185 max_len: None,
4186 pattern: None,
4187 default: None,
4188 deprecated,
4189 span: Some(span_of(&tok)),
4190 };
4191 return self.add_spanned(Node::SchemaField { field }, &tok);
4192 }
4193 if type_name == "Table" && matches!(self.peek_kind_at(1), TokenKind::LBrace) {
4195 self.bump();
4196 let open = self
4197 .expect(TokenKind::LBrace)
4198 .unwrap_or_else(|| tok.clone());
4199 if self.enter_schema_depth(&tok) {
4200 return self.fallback_schema_field(&tok);
4201 }
4202 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
4203 self.exit_schema_depth();
4204 return self.add_spanned(Node::SchemaTable { fields, closed }, &open);
4205 }
4206 let Some(typ) = ActiveType::bare(&type_name) else {
4208 self.error_at("E020", format!("unknown schema type `{type_name}`"), &tok);
4209 self.bump();
4210 return self.fallback_schema_field(&tok);
4211 };
4212 self.bump();
4213 if !matches!(self.peek_kind(), TokenKind::LParen) {
4214 let field = SchemaField::bare(typ, Some(span_of(&tok)));
4215 return self.add_spanned(Node::SchemaField { field }, &tok);
4216 }
4217 if matches!(typ, ActiveType::Table { .. }) {
4221 self.error_at(
4222 "E020",
4223 "Table takes no call arguments; write Table { ... } for a nested schema",
4224 &tok,
4225 );
4226 self.bump(); let mut paren_depth = 1usize;
4228 while paren_depth > 0 {
4229 match self.peek_kind() {
4230 TokenKind::LParen => {
4231 self.bump();
4232 paren_depth += 1;
4233 }
4234 TokenKind::RParen => {
4235 self.bump();
4236 paren_depth -= 1;
4237 }
4238 TokenKind::Eof => break,
4239 _ => {
4240 self.bump();
4241 }
4242 }
4243 }
4244 return self.fallback_schema_field(&tok);
4245 }
4246 self.bump(); let mut field = SchemaField::bare(typ.clone(), Some(span_of(&tok)));
4248 loop {
4249 self.skip_newlines();
4250 match self.peek_kind() {
4251 TokenKind::RParen | TokenKind::Eof => break,
4252 TokenKind::Comma => {
4253 self.bump();
4254 continue;
4255 }
4256 TokenKind::Ident => {
4257 let arg_tok = self.bump();
4258 let arg = arg_tok.text.clone();
4259 if self.expect(TokenKind::Assign).is_none() {
4260 continue;
4261 }
4262 if arg == "deprecated" {
4266 let value = self.parse_expression();
4267 if field.deprecated.is_none() {
4269 match self.module.get(value) {
4270 Some(Node::Literal(Value::Bool(true))) => {
4271 field.deprecated = Some(String::new());
4272 }
4273 Some(Node::Literal(Value::String(msg))) => {
4274 field.deprecated = Some(msg.clone());
4275 }
4276 _ => {
4277 self.error_at(
4278 "E020",
4279 "deprecated takes `true` or a string message",
4280 &arg_tok,
4281 );
4282 }
4283 }
4284 }
4285 continue;
4286 }
4287 let value = self.parse_expression();
4288 if !Self::store_schema_arg(&mut field, &typ, &arg, value) {
4289 self.error_at(
4290 "E020",
4291 format!("unknown argument `{arg}` for {}", typ.name()),
4292 &arg_tok,
4293 );
4294 }
4295 }
4296 _ => {
4297 let bad = self.peek().clone();
4298 self.error_at("E020", "expected a named argument (`name = value`)", &bad);
4299 self.bump();
4300 }
4301 }
4302 self.skip_newlines();
4303 if matches!(self.peek_kind(), TokenKind::Comma) {
4304 self.bump();
4305 }
4306 }
4307 self.expect(TokenKind::RParen);
4308 self.add_spanned(Node::SchemaField { field }, &tok)
4309 }
4310
4311 fn fallback_schema_field(&mut self, tok: &Token) -> NodeId {
4313 let field = SchemaField::bare(ActiveType::Any, Some(span_of(tok)));
4314 self.add_spanned(Node::SchemaField { field }, tok)
4315 }
4316
4317 fn store_schema_arg(
4321 field: &mut SchemaField,
4322 typ: &ActiveType,
4323 arg: &str,
4324 value: NodeId,
4325 ) -> bool {
4326 use ActiveType::*;
4327 let slot = match (typ, arg) {
4328 (Int | Float | Duration | Bytes, "min") => &mut field.min,
4329 (Int | Float | Duration | Bytes, "max") => &mut field.max,
4330 (String, "min_len") => &mut field.min_len,
4331 (String, "max_len") => &mut field.max_len,
4332 (String, "pattern") => &mut field.pattern,
4333 (_, "default") => &mut field.default,
4334 _ => return false,
4335 };
4336 if slot.is_none() {
4337 *slot = Some(value);
4338 }
4339 true
4340 }
4341 fn parse_string_literal(
4342 &mut self,
4343 raw: &str,
4344 tok: &Token,
4345 linemap: Option<&[MlLineMap]>,
4346 ) -> NodeId {
4347 if !raw.contains("${") && !raw.contains(ESCAPED_DOLLAR) {
4349 let text = raw.replace(ESCAPED_DOLLAR, "$");
4350 return self.add_spanned(Node::Literal(Value::String(text)), tok);
4351 }
4352
4353 let chars: Vec<char> = raw.chars().collect();
4354 let mut parts: Vec<InterpPart> = Vec::new();
4355 let mut literal = String::new();
4356 let mut i = 0usize;
4357
4358 while i < chars.len() {
4359 if chars[i] == ESCAPED_DOLLAR {
4363 literal.push('$');
4364 i += 1;
4365 continue;
4366 }
4367 if chars[i] == '$' && i + 1 < chars.len() && chars[i + 1] == '{' {
4368 match self.find_interpolation_end(&chars, i + 2) {
4369 Some(end) => {
4370 if !literal.is_empty() {
4371 literal = literal.replace(ESCAPED_DOLLAR, "$");
4372 parts.push(InterpPart::Literal(std::mem::take(&mut literal)));
4373 }
4374 let inner: String = chars[i + 2..end].iter().collect();
4375 let offset = i + 2;
4378 let id = self.parse_embedded_expression(&inner, tok, raw, offset, linemap);
4379 parts.push(InterpPart::Expr(id));
4380 i = end + 1;
4381 continue;
4382 }
4383 None => {
4384 let span = self.interp_span(tok, raw, i, linemap);
4387 self.error_at(
4388 "E012",
4389 "unterminated interpolation: `${` has no matching `}`",
4390 &span,
4391 );
4392 literal.push_str(&chars[i..].iter().collect::<String>());
4393 break;
4394 }
4395 }
4396 }
4397 literal.push(chars[i]);
4398 i += 1;
4399 }
4400 if !literal.is_empty() {
4401 parts.push(InterpPart::Literal(literal.replace(ESCAPED_DOLLAR, "$")));
4402 }
4403
4404 self.add_spanned(Node::Interpolate { parts }, tok)
4405 }
4406
4407 fn find_interpolation_end(&self, chars: &[char], start: usize) -> Option<usize> {
4411 let mut i = start;
4412 let mut depth = 0usize;
4413 let mut in_string: Option<char> = None;
4414 let mut escaped = false;
4415 while i < chars.len() {
4416 let c = chars[i];
4417 if let Some(quote) = in_string {
4418 if escaped {
4419 escaped = false;
4420 } else if c == '\\' {
4421 escaped = true;
4422 } else if c == quote {
4423 in_string = None;
4424 }
4425 i += 1;
4426 continue;
4427 }
4428 match c {
4429 '"' | '\'' => in_string = Some(c),
4430 '$' if i + 1 < chars.len() && chars[i + 1] == '{' => {
4431 depth += 1;
4432 i += 2;
4433 continue;
4434 }
4435 '{' => depth += 1,
4436 '}' => {
4437 if depth == 0 {
4438 return Some(i);
4439 }
4440 depth -= 1;
4441 }
4442 _ => {}
4443 }
4444 i += 1;
4445 }
4446 None
4447 }
4448
4449 fn interp_span(
4456 &self,
4457 tok: &Token,
4458 stripped: &str,
4459 offset: usize,
4460 linemap: Option<&[MlLineMap]>,
4461 ) -> Token {
4462 let mut t = tok.clone();
4463 match linemap {
4464 Some(map) => {
4465 let mut line = 0usize;
4468 let mut col = 0usize;
4469 for c in stripped.chars().take(offset) {
4470 if c == '\n' {
4471 line += 1;
4472 col = 0;
4473 } else {
4474 col += 1;
4475 }
4476 }
4477 match map
4478 .get(line)
4479 .and_then(|m| m.src_cols.get(col).map(|&c| (m.src_line, c)))
4480 {
4481 Some((src_line, src_col)) => {
4482 t.line = src_line;
4483 t.col = src_col;
4484 t.end_line = src_line;
4485 t.end_col = src_col + 1;
4486 }
4487 None => {
4488 t.col = tok.col + 1 + offset as u32;
4489 t.end_col = t.col + 1;
4490 }
4491 }
4492 }
4493 None => {
4494 t.col = tok.col + 1 + offset as u32;
4495 t.end_col = t.col + 1;
4496 }
4497 }
4498 t
4499 }
4500
4501 fn parse_embedded_expression(
4509 &mut self,
4510 text: &str,
4511 tok: &Token,
4512 stripped: &str,
4513 offset: usize,
4514 linemap: Option<&[MlLineMap]>,
4515 ) -> NodeId {
4516 let leading_ws = text.chars().count() - text.trim_start().chars().count();
4521 let text = text.trim();
4522 let offset = offset + leading_ws;
4523 if text.is_empty() {
4524 let span = self.interp_span(tok, stripped, offset, linemap);
4525 self.error_at(
4526 "E013",
4527 "empty interpolation: `${}` has no expression",
4528 &span,
4529 );
4530 return self.add_spanned(Node::Literal(Value::Null), tok);
4531 }
4532 let mut lexer = Lexer::new(text);
4533 let lexed = lexer.tokenize();
4534
4535 let (anchor_line, anchor_col) = if linemap.is_some() {
4544 let mut al = 0u32;
4545 let mut ac = 0u32;
4546 for c in stripped.chars().take(offset) {
4547 if c == '\n' {
4548 al += 1;
4549 ac = 0;
4550 } else {
4551 ac += 1;
4552 }
4553 }
4554 (al, ac)
4555 } else {
4556 (0, 0)
4557 };
4558
4559 let module = std::mem::replace(&mut self.module, Module::new(""));
4562 let inherited_len = self.ident_scopes.last().map_or(0, |s| s.len());
4566 let mut sub = Parser {
4567 tokens: lexed.tokens,
4568 pos: 0,
4569 module,
4570 errors: Vec::new(),
4571 ident_scopes: self.ident_scopes.clone(),
4572 depth: self.depth,
4573 options: self.options,
4574 block_depth: 0,
4575 schema_depth: self.schema_depth,
4576 comments: Vec::new(),
4577 comment_files: Vec::new(),
4578 statements: Vec::new(),
4579 blocks: Vec::new(),
4580 current_path: self.current_path.clone(),
4581 brace_tables: 0,
4582 indent_tables: 0,
4583 current_file: self.current_file.clone(),
4584 include_stack: Vec::new(),
4585 in_include: self.in_include,
4586 in_profile: self.in_profile,
4587 array_depth: self.array_depth,
4588 binding_spliced: self.binding_spliced.clone(),
4589 pending_profiles: Vec::new(),
4590 trust: self.trust.clone(),
4591 };
4592 let node_base = sub.module.arena.len() as u32;
4596 let id = sub.parse_expression();
4597 while matches!(sub.peek_kind(), TokenKind::Newline) {
4599 sub.bump();
4600 }
4601 if !matches!(sub.peek_kind(), TokenKind::Eof) {
4602 let leftover = sub.peek().clone();
4603 sub.error_at(
4604 "E002",
4605 format!(
4606 "unexpected {:?} after the interpolated expression",
4607 leftover.kind
4608 ),
4609 &leftover,
4610 );
4611 }
4612
4613 let new_idents: Vec<(String, NodeId)> = sub
4614 .ident_scopes
4615 .last()
4616 .map(|s| s[inherited_len.min(s.len())..].to_vec())
4617 .unwrap_or_default();
4618 let mut sub_errors = std::mem::take(&mut sub.errors);
4619 let mut sub_repairs = std::mem::take(&mut sub.module.healing);
4620 let mut sub_lex_errors = lexed.errors;
4621 let mut sub_lex_repairs = lexed.repairs;
4622 let lift = |line_1: u32, col_1: u32| -> Option<(u32, u32)> {
4638 match linemap {
4639 Some(map) => {
4640 let sl = anchor_line as usize + (line_1 as usize).saturating_sub(1);
4641 let sc = if line_1 <= 1 {
4642 anchor_col as usize + (col_1 as usize).saturating_sub(1)
4643 } else {
4644 (col_1 as usize).saturating_sub(1)
4645 };
4646 let m = map.get(sl)?;
4647 let col = m.src_cols.get(sc).copied()?;
4648 Some((m.src_line, col))
4649 }
4650 None => {
4651 let line = tok.line + line_1.saturating_sub(1);
4652 let col = if line_1 <= 1 {
4653 tok.col + offset as u32 + col_1.saturating_sub(1)
4654 } else {
4655 col_1
4656 };
4657 Some((line, col))
4658 }
4659 }
4660 };
4661 for (node, span) in sub.module.node_spans.iter_mut() {
4662 if node.0 >= node_base {
4663 if let Some((line, col)) = lift(span.line, span.column) {
4664 span.line = line;
4665 span.column = col;
4666 }
4667 if let Some((line, col)) = lift(span.end_line, span.end_column) {
4668 span.end_line = line;
4669 span.end_column = col;
4670 }
4671 }
4672 }
4673 self.module = std::mem::replace(&mut sub.module, Module::new(""));
4674
4675 if let Some(scope) = self.ident_scopes.last_mut() {
4676 scope.extend(new_idents);
4677 }
4678 let rebase = |d: &mut Diagnostic| {
4681 if let Some(s) = &mut d.span {
4682 if let Some((line, col)) = lift(s.line, s.column) {
4683 s.line = line;
4684 s.column = col;
4685 }
4686 if let Some((line, col)) = lift(s.end_line, s.end_column) {
4687 s.end_line = line;
4688 s.end_column = col;
4689 }
4690 }
4691 };
4692 for d in sub_lex_errors
4693 .iter_mut()
4694 .chain(sub_lex_repairs.iter_mut())
4695 .chain(sub_errors.iter_mut())
4696 .chain(sub_repairs.iter_mut())
4697 {
4698 rebase(d);
4699 }
4700 let sub_file = self.current_file_name();
4703 for d in sub_lex_errors.iter_mut().chain(sub_lex_repairs.iter_mut()) {
4704 if d.file.is_none() {
4705 d.file = sub_file.clone();
4706 }
4707 }
4708 self.errors.extend(sub_lex_errors);
4709 self.errors.extend(sub_errors);
4710 self.module.healing.extend(sub_lex_repairs);
4711 self.module.healing.extend(sub_repairs);
4712 id
4713 }
4714
4715 fn parse_array(&mut self) -> NodeId {
4716 let open = self.peek().clone();
4717 self.bump(); self.array_depth += 1;
4721 let mut elements = Vec::new();
4722 loop {
4723 self.skip_newlines();
4724 if matches!(
4727 self.peek_kind(),
4728 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4729 ) {
4730 break;
4731 }
4732 if matches!(self.peek_kind(), TokenKind::Comma) {
4733 let comma = self.bump();
4735 if !self.heal(
4736 "H002",
4737 "removed stray comma".into(),
4738 &comma,
4739 Repair {
4740 original: ",".into(),
4741 suggested: String::new(),
4742 kind: RepairKind::RemoveTrailingComma,
4743 safe: true,
4744 },
4745 thresholds::AUTO_APPLY + 0.03,
4746 ) {
4747 self.error("E002", "unexpected comma");
4748 }
4749 continue;
4750 }
4751 elements.push(self.parse_expression());
4752 self.skip_newlines();
4753 if matches!(self.peek_kind(), TokenKind::Comma) {
4754 let comma = self.bump();
4755 self.skip_newlines();
4756 if matches!(
4757 self.peek_kind(),
4758 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4759 ) {
4760 self.heal(
4761 "H002",
4762 "removed trailing comma before closing bracket".into(),
4763 &comma,
4764 Repair {
4765 original: ",".into(),
4766 suggested: String::new(),
4767 kind: RepairKind::RemoveTrailingComma,
4768 safe: true,
4769 },
4770 thresholds::AUTO_APPLY + 0.03,
4771 );
4772 }
4773 } else if !matches!(
4774 self.peek_kind(),
4775 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4776 ) {
4777 let tok = self.peek().clone();
4779 if !self.heal(
4780 "H003",
4781 "inserted missing comma between array elements".into(),
4782 &tok,
4783 Repair {
4784 original: String::new(),
4785 suggested: ",".into(),
4786 kind: RepairKind::Other("insert_comma".into()),
4787 safe: false,
4788 },
4789 thresholds::HIGH + 0.05, ) {
4791 self.error(
4792 "E001",
4793 format!("expected `,` between array elements, found {:?}", tok.kind),
4794 );
4795 }
4796 }
4797 }
4798 self.expect_closing_healing(TokenKind::RBracket, "H004");
4799 self.array_depth = self.array_depth.saturating_sub(1);
4800 self.add_spanned(Node::Array { elements }, &open)
4801 }
4802
4803 fn parse_inline_table(&mut self) -> NodeId {
4804 let open = self.peek().clone();
4805 self.bump(); self.ident_scopes.push(Vec::new());
4807 let mut fields = indexmap::IndexMap::new();
4808 loop {
4809 self.skip_newlines();
4810 if matches!(
4811 self.peek_kind(),
4812 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4813 ) {
4814 break;
4815 }
4816 if matches!(self.peek_kind(), TokenKind::Comma) {
4817 let comma = self.bump();
4818 if !self.heal(
4819 "H002",
4820 "removed stray comma".into(),
4821 &comma,
4822 Repair {
4823 original: ",".into(),
4824 suggested: String::new(),
4825 kind: RepairKind::RemoveTrailingComma,
4826 safe: true,
4827 },
4828 thresholds::AUTO_APPLY + 0.03,
4829 ) {
4830 self.error("E002", "unexpected comma");
4831 }
4832 continue;
4833 }
4834 if let Some((k, id)) = self.parse_key_value() {
4835 let span = self.module.node_spans.get(&id).cloned();
4836 self.note_duplicate_key(&k, span, fields.contains_key(&k));
4837 fields.insert(k, id);
4838 } else {
4839 break;
4840 }
4841 self.skip_newlines();
4842 if matches!(self.peek_kind(), TokenKind::Comma) {
4843 let comma = self.bump();
4844 self.skip_newlines();
4845 if matches!(
4846 self.peek_kind(),
4847 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4848 ) {
4849 self.heal(
4850 "H002",
4851 "removed trailing comma before closing brace".into(),
4852 &comma,
4853 Repair {
4854 original: ",".into(),
4855 suggested: String::new(),
4856 kind: RepairKind::RemoveTrailingComma,
4857 safe: true,
4858 },
4859 thresholds::AUTO_APPLY + 0.03,
4860 );
4861 }
4862 } else if !matches!(
4863 self.peek_kind(),
4864 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4865 ) {
4866 let tok = self.peek().clone();
4867 if !self.heal(
4868 "H003",
4869 "inserted missing comma between table entries".into(),
4870 &tok,
4871 Repair {
4872 original: String::new(),
4873 suggested: ",".into(),
4874 kind: RepairKind::Other("insert_comma".into()),
4875 safe: false,
4876 },
4877 thresholds::HIGH + 0.05, ) {
4879 self.error(
4880 "E001",
4881 format!("expected `,` between table entries, found {:?}", tok.kind),
4882 );
4883 }
4884 }
4885 }
4886 self.expect_closing_healing(TokenKind::RBrace, "H004");
4887 let idents = self.ident_scopes.pop().unwrap_or_default();
4888 let _ = self.resolve_sibling_refs(idents, &fields);
4889 self.add_spanned(Node::Table { fields }, &open)
4890 }
4891
4892 fn build_graph(&mut self) {
4897 let bindings = self.module.bindings.clone();
4898 let declared: Vec<(NodeId, Vec<String>)> = self
4903 .module
4904 .depends_on
4905 .iter()
4906 .map(|(id, dep)| (*id, dep.paths.clone()))
4907 .collect();
4908 for (scope, paths) in &declared {
4909 for path in paths {
4910 if let Some(target) = self.module.resolve_value_path(path) {
4911 self.module.add_edge(target, *scope);
4912 }
4913 }
4914 }
4915
4916 for i in 0..self.module.arena.len() {
4917 let id = NodeId(i as u32);
4918 let node = self.module.get(id).cloned();
4919 match node {
4920 Some(Node::Ident(name)) => {
4921 if let Some(&target) = bindings.get(&name) {
4922 if let Some(slot) = self.module.get_mut(id) {
4923 *slot = Node::Ref(target);
4924 }
4925 self.module.add_edge(target, id);
4927 }
4928 }
4929 Some(Node::Env {
4930 default: Some(def), ..
4931 }) => {
4932 self.module.add_edge(def, id);
4933 }
4934 Some(Node::Interpolate { parts }) => {
4935 for part in &parts {
4936 if let InterpPart::Expr(expr_id) = part {
4937 self.module.add_edge(*expr_id, id);
4938 }
4939 }
4940 }
4941 Some(Node::Binary { lhs, rhs, .. }) => {
4942 self.module.add_edge(lhs, id);
4943 self.module.add_edge(rhs, id);
4944 }
4945 Some(Node::Unary { expr, .. }) => {
4946 self.module.add_edge(expr, id);
4947 }
4948 Some(Node::Quantity { value, .. }) => {
4949 self.module.add_edge(value, id);
4950 }
4951 Some(Node::Semantic { inner, .. }) => {
4952 self.module.add_edge(inner, id);
4953 }
4954 Some(Node::SchemaField { field }) => {
4955 for arg in [
4958 field.min,
4959 field.max,
4960 field.min_len,
4961 field.max_len,
4962 field.pattern,
4963 field.default,
4964 ]
4965 .into_iter()
4966 .flatten()
4967 {
4968 self.module.add_edge(arg, id);
4969 }
4970 }
4971 Some(Node::SchemaTable { fields, .. }) => {
4972 for child in fields.values() {
4973 self.module.add_edge(*child, id);
4974 }
4975 }
4976 Some(Node::SchemaArray { element }) => {
4977 self.module.add_edge(element, id);
4978 }
4979 Some(Node::Array { elements }) => {
4980 for e in &elements {
4981 self.module.add_edge(*e, id);
4982 }
4983 }
4984 Some(Node::Table { fields }) => {
4985 for (_, fid) in &fields {
4986 self.module.add_edge(*fid, id);
4987 }
4988 }
4989 Some(Node::Secret { locator }) => {
4990 self.module.add_edge(locator, id);
4991 }
4992 _ => {}
4993 }
4994 }
4995 }
4996}