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 self.add_spanned(Node::Unary { op, expr }, &op_tok)
3321 }
3322 _ => self.parse_primary(),
3323 }
3324 }
3325
3326 fn parse_primary(&mut self) -> NodeId {
3331 let tok = self.peek().clone();
3332 match &tok.kind {
3333 TokenKind::String(src_pos) => {
3334 self.bump();
3335 let linemap = linemap_from_charmap(&tok.text, tok.line, src_pos);
3336 self.parse_string_literal(&tok.text, &tok, Some(&linemap))
3337 }
3338
3339 TokenKind::SingleString => {
3340 self.bump();
3343 let text = tok.text.clone();
3344 self.add_spanned(Node::Literal(Value::String(text)), &tok)
3345 }
3346
3347 TokenKind::MlString(src_pos) => {
3348 self.bump();
3354 let (stripped, linemap) =
3355 strip_common_indent_mapped(&tok.text, tok.line, tok.col, 3, Some(src_pos));
3356 self.parse_string_literal(&stripped, &tok, Some(&linemap))
3357 }
3358
3359 TokenKind::MlSingleString => {
3360 self.bump();
3363 let stripped = strip_common_indent(&tok.text);
3364 self.add_spanned(Node::Literal(Value::String(stripped)), &tok)
3365 }
3366
3367 TokenKind::At => {
3368 self.bump();
3369 if matches!(self.peek_kind(), TokenKind::Ident) {
3371 let tag_tok = self.peek().clone();
3372 let tag = tag_tok.text.clone();
3373 if (tag == "patch" || tag == "remove" || tag == "rename")
3374 && matches!(self.peek_kind_at(1), TokenKind::LParen)
3375 {
3376 self.error_at(
3377 "E019",
3378 "patch statements may only appear at document root, never as values",
3379 &tag_tok,
3380 );
3381 return self.add_spanned(Node::Literal(Value::Null), &tok);
3382 }
3383 }
3384 self.parse_semantic_block(&tok)
3385 }
3386
3387 TokenKind::Integer => {
3388 self.bump();
3389 let n: i64 = match tok.text.parse() {
3395 Ok(n) => n,
3396 Err(_) => {
3397 self.error_at(
3398 "E023",
3399 format!("integer literal out of i64 range: `{}`", tok.text),
3400 &tok,
3401 );
3402 i64::MAX
3403 }
3404 };
3405 let mut node = self.add_spanned(Node::Literal(Value::Int(n)), &tok);
3406 if let TokenKind::Unit(_) = self.peek_kind() {
3408 let unit_tok = self.bump();
3409 if let Some(unit) = Unit::from_suffix(&unit_tok.text) {
3410 node = self.add_spanned(Node::Quantity { value: node, unit }, &tok);
3411 }
3412 }
3413 node
3414 }
3415
3416 TokenKind::Float => {
3417 self.bump();
3418 let f: f64 = tok.text.parse().unwrap_or(0.0);
3419 let mut node = self.add_spanned(Node::Literal(Value::Float(f)), &tok);
3420 if let TokenKind::Unit(_) = self.peek_kind() {
3421 let unit_tok = self.bump();
3422 if let Some(unit) = Unit::from_suffix(&unit_tok.text) {
3423 node = self.add_spanned(Node::Quantity { value: node, unit }, &tok);
3424 }
3425 }
3426 node
3427 }
3428
3429 TokenKind::True => {
3430 self.bump();
3431 self.add_spanned(Node::Literal(Value::Bool(true)), &tok)
3432 }
3433 TokenKind::False => {
3434 self.bump();
3435 self.add_spanned(Node::Literal(Value::Bool(false)), &tok)
3436 }
3437 TokenKind::Null => {
3438 self.bump();
3439 self.add_spanned(Node::Literal(Value::Null), &tok)
3440 }
3441
3442 TokenKind::Env => {
3443 self.bump();
3444 self.expect(TokenKind::Dot);
3445 let var = self
3446 .expect(TokenKind::Ident)
3447 .map(|t| t.text)
3448 .unwrap_or_default();
3449 let default = if matches!(self.peek_kind(), TokenKind::QuestionColon) {
3450 self.bump();
3451 Some(self.parse_expression())
3452 } else {
3453 None
3454 };
3455 self.add_spanned(Node::Env { name: var, default }, &tok)
3456 }
3457
3458 TokenKind::Secret => {
3459 self.bump();
3460 let from_env = if matches!(self.peek_kind(), TokenKind::Dot) {
3464 self.bump();
3465 if !matches!(self.peek_kind(), TokenKind::Env) {
3466 self.error(
3467 "E003",
3468 "expected `env` after `secret.` (use secret.env(name))",
3469 );
3470 } else {
3471 self.bump();
3472 }
3473 true
3474 } else {
3475 false
3476 };
3477 self.expect(TokenKind::LParen);
3478 let arg = self.parse_expression();
3479 self.expect(TokenKind::RParen);
3480 let locator = if from_env {
3481 match self.module.get(arg) {
3482 Some(Node::Literal(Value::String(name))) => self.add_spanned(
3483 Node::Env {
3484 name: name.clone(),
3485 default: None,
3486 },
3487 &tok,
3488 ),
3489 _ => {
3490 self.error(
3491 "E003",
3492 "secret.env() requires a string literal env var name",
3493 );
3494 arg
3495 }
3496 }
3497 } else {
3498 arg
3499 };
3500 self.add_spanned(Node::Secret { locator }, &tok)
3501 }
3502
3503 TokenKind::Ident => {
3504 self.bump();
3505 let text = tok.text.clone();
3509 let id = self.add_spanned(Node::Ident(text), &tok);
3510 if let Some(scope) = self.ident_scopes.last_mut() {
3511 scope.push((String::new(), id));
3512 }
3513 id
3514 }
3515
3516 TokenKind::LBracket => self.parse_array(),
3517 TokenKind::LBrace => self.parse_inline_table(),
3518
3519 TokenKind::LParen => {
3520 self.bump();
3521 let expr = self.parse_expression();
3522 self.expect(TokenKind::RParen);
3523 expr
3524 }
3525
3526 _ => {
3527 self.error("E002", format!("unexpected token {:?}", tok.kind));
3528 self.bump();
3529 self.module.add_node(Node::Literal(Value::Null))
3530 }
3531 }
3532 }
3533
3534 fn parse_semantic_block(&mut self, at_tok: &Token) -> NodeId {
3538 let tag_tok = self.peek().clone();
3539 let tag = match &tag_tok.kind {
3540 TokenKind::Ident => {
3541 self.bump();
3542 tag_tok.text.clone()
3543 }
3544 _ => {
3545 self.error_at(
3546 "E014",
3547 "expected a semantic tag (prompt, policy, template) after `@`",
3548 &tag_tok,
3549 );
3550 String::new()
3551 }
3552 };
3553 let tag = match SemanticTag::parse_tag(&tag) {
3554 Some(t) => t,
3555 None => {
3556 self.error_at(
3557 "E014",
3558 format!("unknown semantic tag `@{tag}` (expected prompt, policy, or template)"),
3559 &tag_tok,
3560 );
3561 SemanticTag::Prompt
3562 }
3563 };
3564 let str_tok = self.peek().clone();
3565 let inner = match &str_tok.kind {
3566 TokenKind::MlString(src_pos) => {
3567 self.bump();
3568 let (stripped, linemap) = strip_common_indent_mapped(
3569 &str_tok.text,
3570 str_tok.line,
3571 str_tok.col,
3572 3,
3573 Some(src_pos),
3574 );
3575 self.parse_string_literal(&stripped, &str_tok, Some(&linemap))
3576 }
3577 TokenKind::MlSingleString => {
3578 self.bump();
3579 let stripped = strip_common_indent(&str_tok.text);
3580 self.add_spanned(Node::Literal(Value::String(stripped)), &str_tok)
3581 }
3582 _ => {
3583 self.error_at(
3584 "E014",
3585 "a semantic tag must be followed by a multi-line string (`\"\"\"…\"\"\"` or `'''…'''`)",
3586 &str_tok,
3587 );
3588 self.add_spanned(Node::Literal(Value::String(String::new())), at_tok)
3589 }
3590 };
3591 let id = self.add_spanned(Node::Semantic { tag, inner }, at_tok);
3592 id
3595 }
3596}
3597
3598fn strip_common_indent(raw: &str) -> String {
3605 strip_common_indent_mapped(raw, 1, 1, 0, None).0
3606}
3607
3608#[derive(Debug, Clone)]
3611struct MlLineMap {
3612 src_line: u32,
3613 src_cols: Vec<u32>,
3614}
3615
3616fn linemap_from_charmap(text: &str, tok_line: u32, charmap: &[(u32, u32)]) -> Vec<MlLineMap> {
3621 let chars: Vec<char> = text.chars().collect();
3622 let mut lines: Vec<(usize, usize)> = Vec::new();
3623 let mut start = 0usize;
3624 for (i, c) in chars.iter().enumerate() {
3625 if *c == '\n' {
3626 lines.push((start, i));
3627 start = i + 1;
3628 }
3629 }
3630 lines.push((start, chars.len()));
3631 lines
3632 .iter()
3633 .map(|&(s, e)| {
3634 let src_line = tok_line + charmap.get(s).map(|&(off, _)| off).unwrap_or(0);
3635 let src_cols = (s..e)
3636 .map(|k| charmap.get(k).map(|&(_, c)| c).unwrap_or(0))
3637 .collect();
3638 MlLineMap { src_line, src_cols }
3639 })
3640 .collect()
3641}
3642
3643fn strip_common_indent_mapped(
3649 raw: &str,
3650 tok_line: u32,
3651 tok_col: u32,
3652 delim_len: u32,
3653 src_pos: Option<&[(u32, u32)]>,
3654) -> (String, Vec<MlLineMap>) {
3655 let mut lines: Vec<&str> = raw.split('\n').collect();
3656 let mut kept: Vec<usize> = (0..lines.len()).collect();
3660 if !lines.is_empty() && lines[0].trim().is_empty() {
3661 kept.remove(0);
3662 lines.remove(0);
3663 }
3664 if !lines.is_empty() && lines.last().is_some_and(|l| l.trim().is_empty()) {
3665 kept.pop();
3666 lines.pop();
3667 }
3668 let mut prefix: Vec<char> = Vec::new();
3670 let mut seen_content = false;
3671 for line in lines.iter() {
3672 if line.trim().is_empty() {
3673 continue;
3674 }
3675 let indent: Vec<char> = line
3676 .chars()
3677 .take_while(|c| *c == ' ' || *c == '\t')
3678 .collect();
3679 if !seen_content {
3680 prefix = indent;
3681 seen_content = true;
3682 } else {
3683 let shared = prefix
3684 .iter()
3685 .zip(indent.iter())
3686 .take_while(|(a, b)| a == b)
3687 .count();
3688 prefix.truncate(shared);
3689 }
3690 if prefix.is_empty() {
3691 break;
3692 }
3693 }
3694 let mut out = Vec::with_capacity(lines.len());
3695 let mut map = Vec::with_capacity(lines.len());
3696 let all: Vec<&str> = raw.split('\n').collect();
3701 let mut line_start: Vec<usize> = Vec::with_capacity(all.len());
3702 let mut acc = 0usize;
3703 for l in &all {
3704 line_start.push(acc);
3705 acc += l.chars().count() + 1; }
3707 let line_start_for =
3708 |raw_idx: usize| -> usize { line_start.get(raw_idx).copied().unwrap_or(0) };
3709 for (j, l) in lines.iter().enumerate() {
3710 let raw_idx = kept[j];
3711 let decoded_start = line_start_for(raw_idx);
3712 let at = |k: usize| -> (u32, u32) {
3713 src_pos
3714 .and_then(|p| p.get(k).copied())
3715 .unwrap_or((raw_idx as u32, k as u32))
3716 };
3717 let shared = if l.trim().is_empty() {
3718 0
3719 } else {
3720 prefix
3721 .iter()
3722 .zip(l.chars())
3723 .take_while(|(a, b)| *a == b)
3724 .count()
3725 };
3726 if l.trim().is_empty() {
3728 let (line_off, _) = at(decoded_start);
3729 map.push(MlLineMap {
3730 src_line: tok_line + line_off,
3731 src_cols: Vec::new(),
3732 });
3733 out.push(String::new());
3734 continue;
3735 }
3736 let text: String = l.chars().skip(shared).collect();
3737 let first_kept = decoded_start + shared;
3739 let (line_off, _) = at(first_kept.min(decoded_start + l.chars().count()));
3740 let src_line = tok_line + line_off;
3741 let cols: Vec<u32> = if src_pos.is_some() {
3742 (0..text.chars().count())
3743 .map(|c| at(first_kept + c).1)
3744 .collect()
3745 } else if raw_idx == 0 {
3746 (0..text.chars().count())
3748 .map(|c| tok_col + delim_len + shared as u32 + c as u32)
3749 .collect()
3750 } else {
3751 (0..text.chars().count())
3752 .map(|c| shared as u32 + 1 + c as u32)
3753 .collect()
3754 };
3755 out.push(text);
3756 map.push(MlLineMap {
3757 src_line,
3758 src_cols: cols,
3759 });
3760 }
3761 (out.join("\n"), map)
3762}
3763
3764impl Parser {
3765 fn parse_patch_statement(&mut self, at_tok: &Token) {
3775 let nested = !self.current_path.is_empty();
3776 self.bump(); let tag_tok = self.bump(); let tag = tag_tok.text.clone();
3779 if self.expect(TokenKind::LParen).is_none() {
3780 return;
3781 }
3782 let path_tok = self.peek().clone();
3783 let path = match &path_tok.kind {
3784 TokenKind::String(_) | TokenKind::SingleString => {
3785 self.bump();
3786 path_tok.text.clone()
3787 }
3788 _ => {
3789 self.error_at("E019", "patch paths must be string literals", &path_tok);
3790 return;
3791 }
3792 };
3793 if path.contains("${") || path.contains(ESCAPED_DOLLAR) {
3794 self.error_at(
3795 "E019",
3796 "interpolation in patch paths is a hard error; paths must be static",
3797 &path_tok,
3798 );
3799 return;
3800 }
3801 if let Err(why) = validate_patch_path(&path) {
3802 self.error_at(
3803 "E019",
3804 format!("invalid patch path `{path}`: {why}"),
3805 &path_tok,
3806 );
3807 return;
3808 }
3809 let op = match tag.as_str() {
3810 "remove" => {
3811 if self.expect(TokenKind::RParen).is_none() {
3812 return;
3813 }
3814 PatchOp::Remove { path: path.clone() }
3815 }
3816 "rename" => {
3817 if self.expect(TokenKind::Comma).is_none() {
3818 return;
3819 }
3820 let new_tok = self.peek().clone();
3821 let new_name = match &new_tok.kind {
3822 TokenKind::String(_) | TokenKind::SingleString => {
3823 self.bump();
3824 new_tok.text.clone()
3825 }
3826 _ => {
3827 self.error_at(
3828 "E019",
3829 "rename needs a new key name as a string literal",
3830 &new_tok,
3831 );
3832 return;
3833 }
3834 };
3835 if new_name.contains('.') || new_name.contains('[') || new_name.is_empty() {
3836 self.error_at(
3837 "E019",
3838 "rename target must be a plain key name in the same parent",
3839 &new_tok,
3840 );
3841 return;
3842 }
3843 if self.expect(TokenKind::RParen).is_none() {
3844 return;
3845 }
3846 PatchOp::Rename {
3847 from: path.clone(),
3848 to: new_name,
3849 }
3850 }
3851 _ => {
3852 if self.expect(TokenKind::Comma).is_none() {
3854 return;
3855 }
3856 let value = self.parse_expression();
3857 if self.expect(TokenKind::RParen).is_none() {
3858 return;
3859 }
3860 PatchOp::Set {
3861 path: path.clone(),
3862 value,
3863 }
3864 }
3865 };
3866 let stmt_key = match &op {
3867 PatchOp::Set { path, .. } => format!("@patch({path})"),
3868 PatchOp::Remove { path } => format!("@remove({path})"),
3869 PatchOp::Rename { from, .. } => format!("@rename({from})"),
3870 };
3871 if nested {
3872 self.error_at(
3873 "E019",
3874 "patch statements may only appear at document root",
3875 at_tok,
3876 );
3877 return;
3878 }
3879 self.record_stmt(
3880 &stmt_key,
3881 at_tok.line,
3882 self.last_end_line(),
3883 at_tok.col.saturating_sub(1) as usize,
3884 );
3885 let id = self.add_spanned(Node::PatchDirective { op }, at_tok);
3886 if let Some(Node::PatchDirective {
3889 op: PatchOp::Set { value, .. },
3890 }) = self.module.get(id)
3891 {
3892 let value = *value;
3893 self.module.add_edge(value, id);
3894 }
3895 self.module.patches.push(id);
3896 }
3897
3898 fn parse_schema_inline(&mut self, key_tok: &Token) -> NodeId {
3905 let open = self
3906 .expect(TokenKind::LBrace)
3907 .unwrap_or_else(|| key_tok.clone());
3908 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
3909 self.add_spanned(Node::SchemaTable { fields, closed }, &open)
3910 }
3911
3912 fn parse_schema_brace(&mut self, key_tok: &Token) -> NodeId {
3914 let open = self
3915 .expect(TokenKind::LBrace)
3916 .unwrap_or_else(|| key_tok.clone());
3917 self.brace_tables += 1;
3918 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
3919 self.add_spanned(Node::SchemaTable { fields, closed }, &open)
3920 }
3921
3922 fn parse_schema_indent(&mut self, key_tok: &Token, _level: usize) -> NodeId {
3924 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Indent);
3925 self.add_spanned(Node::SchemaTable { fields, closed }, key_tok)
3926 }
3927
3928 fn parse_schema_entries(&mut self, end: SchemaEnd) -> (IndexMap<String, NodeId>, bool) {
3931 let mut fields = IndexMap::new();
3932 let mut closed = false;
3933 loop {
3934 self.skip_newlines();
3935 match self.peek_kind() {
3936 TokenKind::Eof => {
3937 if matches!(end, SchemaEnd::Brace) {
3941 self.error(
3942 "E001",
3943 "unclosed schema block: expected `}` before end of file",
3944 );
3945 }
3946 break;
3947 }
3948 TokenKind::RBrace if matches!(end, SchemaEnd::Brace) => {
3949 self.bump();
3950 break;
3951 }
3952 TokenKind::Dedent if matches!(end, SchemaEnd::Indent) => {
3953 self.bump();
3954 break;
3955 }
3956 TokenKind::RBrace | TokenKind::Dedent => {
3957 break;
3960 }
3961 TokenKind::Comma => {
3962 self.bump();
3963 continue;
3964 }
3965 TokenKind::Ident => {
3966 let name_tok = self.bump();
3967 let name = name_tok.text.clone();
3968 if self.expect(TokenKind::Assign).is_none() {
3969 continue;
3970 }
3971 if name == "closed" {
3972 match self.peek_kind() {
3973 TokenKind::True => {
3974 self.bump();
3975 closed = true;
3976 }
3977 TokenKind::False => {
3978 self.bump();
3979 closed = false;
3980 }
3981 _ => {
3982 let tok = self.peek().clone();
3983 self.error_at("E020", "closed must be true or false", &tok);
3984 }
3985 }
3986 self.skip_newlines();
3987 if matches!(self.peek_kind(), TokenKind::Comma) {
3988 self.bump();
3989 }
3990 continue;
3991 }
3992 let id = self.parse_schema_type(&name_tok);
3993 if self.schema_depth == 0 {
4004 self.record_stmt(
4005 &name,
4006 name_tok.line,
4007 self.last_end_line(),
4008 name_tok.col.saturating_sub(1) as usize,
4009 );
4010 }
4011 fields.insert(name, id);
4012 self.skip_newlines();
4013 if matches!(self.peek_kind(), TokenKind::Comma) {
4014 self.bump();
4015 }
4016 }
4017 _ => {
4018 let tok = self.peek().clone();
4019 self.error_at("E020", "expected a schema entry (`name = Type`)", &tok);
4020 self.bump();
4021 }
4022 }
4023 }
4024 (fields, closed)
4025 }
4026
4027 fn enter_schema_depth(&mut self, tok: &Token) -> bool {
4034 self.schema_depth += 1;
4035 if self.schema_depth > MAX_SCHEMA_DEPTH {
4036 self.schema_depth -= 1;
4037 self.error_at(
4038 "E004",
4039 format!("maximum schema nesting depth ({MAX_SCHEMA_DEPTH}) exceeded"),
4040 tok,
4041 );
4042 return true;
4043 }
4044 false
4045 }
4046
4047 fn exit_schema_depth(&mut self) {
4048 self.schema_depth = self.schema_depth.saturating_sub(1);
4049 }
4050
4051 fn parse_schema_type(&mut self, _name_tok: &Token) -> NodeId {
4055 let tok = self.peek().clone();
4056 let TokenKind::Ident = &tok.kind else {
4057 self.error_at("E020", "expected a schema type", &tok);
4058 self.bump();
4059 return self.fallback_schema_field(&tok);
4060 };
4061 let type_name = tok.text.clone();
4062 if type_name == "Array" {
4064 self.bump();
4065 if self.expect(TokenKind::Lt).is_none() {
4066 self.error_at("E020", "Array needs an element type: Array<T>", &tok);
4067 return self.fallback_schema_field(&tok);
4068 }
4069 let elem_tok = self.peek().clone();
4070 if self.enter_schema_depth(&tok) {
4071 return self.fallback_schema_field(&tok);
4072 }
4073 let element = self.parse_schema_type(&elem_tok);
4074 self.exit_schema_depth();
4075 if self.expect(TokenKind::Gt).is_none() {
4076 return self.fallback_schema_field(&tok);
4077 }
4078 return self.add_spanned(Node::SchemaArray { element }, &tok);
4079 }
4080 if type_name == "Enum" {
4082 self.bump();
4083 if self.expect(TokenKind::LParen).is_none() {
4084 self.error_at("E020", "Enum needs a value list: Enum(\"a\", ...)", &tok);
4085 return self.fallback_schema_field(&tok);
4086 }
4087 let mut values = Vec::new();
4088 let mut deprecated: Option<String> = None;
4089 loop {
4090 self.skip_newlines();
4091 match self.peek_kind() {
4092 TokenKind::RParen | TokenKind::Eof => break,
4093 TokenKind::Comma => {
4094 self.bump();
4095 continue;
4096 }
4097 TokenKind::String(_) | TokenKind::SingleString => {
4098 let v = self.bump();
4099 values.push(v.text.clone());
4100 }
4101 TokenKind::Ident if self.peek().text == "deprecated" => {
4102 self.bump();
4106 if self.expect(TokenKind::Assign).is_none() {
4107 continue;
4108 }
4109 deprecated = match self.peek_kind() {
4110 TokenKind::True => {
4111 self.bump();
4112 Some(String::new())
4113 }
4114 TokenKind::String(_) | TokenKind::SingleString => {
4115 Some(self.bump().text.clone())
4116 }
4117 _ => {
4118 let bad = self.peek().clone();
4119 self.error_at(
4120 "E020",
4121 "deprecated takes `true` or a string message",
4122 &bad,
4123 );
4124 None
4125 }
4126 };
4127 }
4128 _ => {
4129 let bad = self.peek().clone();
4130 self.error_at("E020", "Enum values must be string literals", &bad);
4131 self.bump();
4132 }
4133 }
4134 self.skip_newlines();
4135 if matches!(self.peek_kind(), TokenKind::Comma) {
4136 self.bump();
4137 }
4138 }
4139 self.expect(TokenKind::RParen);
4140 if values.is_empty() {
4141 self.error_at("E020", "Enum needs at least one value", &tok);
4142 }
4143 let field = SchemaField {
4144 typ: ActiveType::Enum(values),
4145 min: None,
4146 max: None,
4147 min_len: None,
4148 max_len: None,
4149 pattern: None,
4150 default: None,
4151 deprecated,
4152 span: Some(span_of(&tok)),
4153 };
4154 return self.add_spanned(Node::SchemaField { field }, &tok);
4155 }
4156 if type_name == "Table" && matches!(self.peek_kind_at(1), TokenKind::LBrace) {
4158 self.bump();
4159 let open = self
4160 .expect(TokenKind::LBrace)
4161 .unwrap_or_else(|| tok.clone());
4162 if self.enter_schema_depth(&tok) {
4163 return self.fallback_schema_field(&tok);
4164 }
4165 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
4166 self.exit_schema_depth();
4167 return self.add_spanned(Node::SchemaTable { fields, closed }, &open);
4168 }
4169 let Some(typ) = ActiveType::bare(&type_name) else {
4171 self.error_at("E020", format!("unknown schema type `{type_name}`"), &tok);
4172 self.bump();
4173 return self.fallback_schema_field(&tok);
4174 };
4175 self.bump();
4176 if !matches!(self.peek_kind(), TokenKind::LParen) {
4177 let field = SchemaField::bare(typ, Some(span_of(&tok)));
4178 return self.add_spanned(Node::SchemaField { field }, &tok);
4179 }
4180 if matches!(typ, ActiveType::Table { .. }) {
4184 self.error_at(
4185 "E020",
4186 "Table takes no call arguments; write Table { ... } for a nested schema",
4187 &tok,
4188 );
4189 self.bump(); let mut paren_depth = 1usize;
4191 while paren_depth > 0 {
4192 match self.peek_kind() {
4193 TokenKind::LParen => {
4194 self.bump();
4195 paren_depth += 1;
4196 }
4197 TokenKind::RParen => {
4198 self.bump();
4199 paren_depth -= 1;
4200 }
4201 TokenKind::Eof => break,
4202 _ => {
4203 self.bump();
4204 }
4205 }
4206 }
4207 return self.fallback_schema_field(&tok);
4208 }
4209 self.bump(); let mut field = SchemaField::bare(typ.clone(), Some(span_of(&tok)));
4211 loop {
4212 self.skip_newlines();
4213 match self.peek_kind() {
4214 TokenKind::RParen | TokenKind::Eof => break,
4215 TokenKind::Comma => {
4216 self.bump();
4217 continue;
4218 }
4219 TokenKind::Ident => {
4220 let arg_tok = self.bump();
4221 let arg = arg_tok.text.clone();
4222 if self.expect(TokenKind::Assign).is_none() {
4223 continue;
4224 }
4225 if arg == "deprecated" {
4229 let value = self.parse_expression();
4230 if field.deprecated.is_none() {
4232 match self.module.get(value) {
4233 Some(Node::Literal(Value::Bool(true))) => {
4234 field.deprecated = Some(String::new());
4235 }
4236 Some(Node::Literal(Value::String(msg))) => {
4237 field.deprecated = Some(msg.clone());
4238 }
4239 _ => {
4240 self.error_at(
4241 "E020",
4242 "deprecated takes `true` or a string message",
4243 &arg_tok,
4244 );
4245 }
4246 }
4247 }
4248 continue;
4249 }
4250 let value = self.parse_expression();
4251 if !Self::store_schema_arg(&mut field, &typ, &arg, value) {
4252 self.error_at(
4253 "E020",
4254 format!("unknown argument `{arg}` for {}", typ.name()),
4255 &arg_tok,
4256 );
4257 }
4258 }
4259 _ => {
4260 let bad = self.peek().clone();
4261 self.error_at("E020", "expected a named argument (`name = value`)", &bad);
4262 self.bump();
4263 }
4264 }
4265 self.skip_newlines();
4266 if matches!(self.peek_kind(), TokenKind::Comma) {
4267 self.bump();
4268 }
4269 }
4270 self.expect(TokenKind::RParen);
4271 self.add_spanned(Node::SchemaField { field }, &tok)
4272 }
4273
4274 fn fallback_schema_field(&mut self, tok: &Token) -> NodeId {
4276 let field = SchemaField::bare(ActiveType::Any, Some(span_of(tok)));
4277 self.add_spanned(Node::SchemaField { field }, tok)
4278 }
4279
4280 fn store_schema_arg(
4284 field: &mut SchemaField,
4285 typ: &ActiveType,
4286 arg: &str,
4287 value: NodeId,
4288 ) -> bool {
4289 use ActiveType::*;
4290 let slot = match (typ, arg) {
4291 (Int | Float | Duration | Bytes, "min") => &mut field.min,
4292 (Int | Float | Duration | Bytes, "max") => &mut field.max,
4293 (String, "min_len") => &mut field.min_len,
4294 (String, "max_len") => &mut field.max_len,
4295 (String, "pattern") => &mut field.pattern,
4296 (_, "default") => &mut field.default,
4297 _ => return false,
4298 };
4299 if slot.is_none() {
4300 *slot = Some(value);
4301 }
4302 true
4303 }
4304 fn parse_string_literal(
4305 &mut self,
4306 raw: &str,
4307 tok: &Token,
4308 linemap: Option<&[MlLineMap]>,
4309 ) -> NodeId {
4310 if !raw.contains("${") && !raw.contains(ESCAPED_DOLLAR) {
4312 let text = raw.replace(ESCAPED_DOLLAR, "$");
4313 return self.add_spanned(Node::Literal(Value::String(text)), tok);
4314 }
4315
4316 let chars: Vec<char> = raw.chars().collect();
4317 let mut parts: Vec<InterpPart> = Vec::new();
4318 let mut literal = String::new();
4319 let mut i = 0usize;
4320
4321 while i < chars.len() {
4322 if chars[i] == ESCAPED_DOLLAR {
4326 literal.push('$');
4327 i += 1;
4328 continue;
4329 }
4330 if chars[i] == '$' && i + 1 < chars.len() && chars[i + 1] == '{' {
4331 match self.find_interpolation_end(&chars, i + 2) {
4332 Some(end) => {
4333 if !literal.is_empty() {
4334 literal = literal.replace(ESCAPED_DOLLAR, "$");
4335 parts.push(InterpPart::Literal(std::mem::take(&mut literal)));
4336 }
4337 let inner: String = chars[i + 2..end].iter().collect();
4338 let offset = i + 2;
4341 let id = self.parse_embedded_expression(&inner, tok, raw, offset, linemap);
4342 parts.push(InterpPart::Expr(id));
4343 i = end + 1;
4344 continue;
4345 }
4346 None => {
4347 let span = self.interp_span(tok, raw, i, linemap);
4350 self.error_at(
4351 "E012",
4352 "unterminated interpolation: `${` has no matching `}`",
4353 &span,
4354 );
4355 literal.push_str(&chars[i..].iter().collect::<String>());
4356 break;
4357 }
4358 }
4359 }
4360 literal.push(chars[i]);
4361 i += 1;
4362 }
4363 if !literal.is_empty() {
4364 parts.push(InterpPart::Literal(literal.replace(ESCAPED_DOLLAR, "$")));
4365 }
4366
4367 self.add_spanned(Node::Interpolate { parts }, tok)
4368 }
4369
4370 fn find_interpolation_end(&self, chars: &[char], start: usize) -> Option<usize> {
4374 let mut i = start;
4375 let mut depth = 0usize;
4376 let mut in_string: Option<char> = None;
4377 let mut escaped = false;
4378 while i < chars.len() {
4379 let c = chars[i];
4380 if let Some(quote) = in_string {
4381 if escaped {
4382 escaped = false;
4383 } else if c == '\\' {
4384 escaped = true;
4385 } else if c == quote {
4386 in_string = None;
4387 }
4388 i += 1;
4389 continue;
4390 }
4391 match c {
4392 '"' | '\'' => in_string = Some(c),
4393 '$' if i + 1 < chars.len() && chars[i + 1] == '{' => {
4394 depth += 1;
4395 i += 2;
4396 continue;
4397 }
4398 '{' => depth += 1,
4399 '}' => {
4400 if depth == 0 {
4401 return Some(i);
4402 }
4403 depth -= 1;
4404 }
4405 _ => {}
4406 }
4407 i += 1;
4408 }
4409 None
4410 }
4411
4412 fn interp_span(
4419 &self,
4420 tok: &Token,
4421 stripped: &str,
4422 offset: usize,
4423 linemap: Option<&[MlLineMap]>,
4424 ) -> Token {
4425 let mut t = tok.clone();
4426 match linemap {
4427 Some(map) => {
4428 let mut line = 0usize;
4431 let mut col = 0usize;
4432 for c in stripped.chars().take(offset) {
4433 if c == '\n' {
4434 line += 1;
4435 col = 0;
4436 } else {
4437 col += 1;
4438 }
4439 }
4440 match map
4441 .get(line)
4442 .and_then(|m| m.src_cols.get(col).map(|&c| (m.src_line, c)))
4443 {
4444 Some((src_line, src_col)) => {
4445 t.line = src_line;
4446 t.col = src_col;
4447 t.end_line = src_line;
4448 t.end_col = src_col + 1;
4449 }
4450 None => {
4451 t.col = tok.col + 1 + offset as u32;
4452 t.end_col = t.col + 1;
4453 }
4454 }
4455 }
4456 None => {
4457 t.col = tok.col + 1 + offset as u32;
4458 t.end_col = t.col + 1;
4459 }
4460 }
4461 t
4462 }
4463
4464 fn parse_embedded_expression(
4472 &mut self,
4473 text: &str,
4474 tok: &Token,
4475 stripped: &str,
4476 offset: usize,
4477 linemap: Option<&[MlLineMap]>,
4478 ) -> NodeId {
4479 let leading_ws = text.chars().count() - text.trim_start().chars().count();
4484 let text = text.trim();
4485 let offset = offset + leading_ws;
4486 if text.is_empty() {
4487 let span = self.interp_span(tok, stripped, offset, linemap);
4488 self.error_at(
4489 "E013",
4490 "empty interpolation: `${}` has no expression",
4491 &span,
4492 );
4493 return self.add_spanned(Node::Literal(Value::Null), tok);
4494 }
4495 let mut lexer = Lexer::new(text);
4496 let lexed = lexer.tokenize();
4497
4498 let (anchor_line, anchor_col) = if linemap.is_some() {
4507 let mut al = 0u32;
4508 let mut ac = 0u32;
4509 for c in stripped.chars().take(offset) {
4510 if c == '\n' {
4511 al += 1;
4512 ac = 0;
4513 } else {
4514 ac += 1;
4515 }
4516 }
4517 (al, ac)
4518 } else {
4519 (0, 0)
4520 };
4521
4522 let module = std::mem::replace(&mut self.module, Module::new(""));
4525 let inherited_len = self.ident_scopes.last().map_or(0, |s| s.len());
4529 let mut sub = Parser {
4530 tokens: lexed.tokens,
4531 pos: 0,
4532 module,
4533 errors: Vec::new(),
4534 ident_scopes: self.ident_scopes.clone(),
4535 depth: self.depth,
4536 options: self.options,
4537 block_depth: 0,
4538 schema_depth: self.schema_depth,
4539 comments: Vec::new(),
4540 comment_files: Vec::new(),
4541 statements: Vec::new(),
4542 blocks: Vec::new(),
4543 current_path: self.current_path.clone(),
4544 brace_tables: 0,
4545 indent_tables: 0,
4546 current_file: self.current_file.clone(),
4547 include_stack: Vec::new(),
4548 in_include: self.in_include,
4549 in_profile: self.in_profile,
4550 array_depth: self.array_depth,
4551 binding_spliced: self.binding_spliced.clone(),
4552 pending_profiles: Vec::new(),
4553 trust: self.trust.clone(),
4554 };
4555 let node_base = sub.module.arena.len() as u32;
4559 let id = sub.parse_expression();
4560 while matches!(sub.peek_kind(), TokenKind::Newline) {
4562 sub.bump();
4563 }
4564 if !matches!(sub.peek_kind(), TokenKind::Eof) {
4565 let leftover = sub.peek().clone();
4566 sub.error_at(
4567 "E002",
4568 format!(
4569 "unexpected {:?} after the interpolated expression",
4570 leftover.kind
4571 ),
4572 &leftover,
4573 );
4574 }
4575
4576 let new_idents: Vec<(String, NodeId)> = sub
4577 .ident_scopes
4578 .last()
4579 .map(|s| s[inherited_len.min(s.len())..].to_vec())
4580 .unwrap_or_default();
4581 let mut sub_errors = std::mem::take(&mut sub.errors);
4582 let mut sub_repairs = std::mem::take(&mut sub.module.healing);
4583 let mut sub_lex_errors = lexed.errors;
4584 let mut sub_lex_repairs = lexed.repairs;
4585 let lift = |line_1: u32, col_1: u32| -> Option<(u32, u32)> {
4601 match linemap {
4602 Some(map) => {
4603 let sl = anchor_line as usize + (line_1 as usize).saturating_sub(1);
4604 let sc = if line_1 <= 1 {
4605 anchor_col as usize + (col_1 as usize).saturating_sub(1)
4606 } else {
4607 (col_1 as usize).saturating_sub(1)
4608 };
4609 let m = map.get(sl)?;
4610 let col = m.src_cols.get(sc).copied()?;
4611 Some((m.src_line, col))
4612 }
4613 None => {
4614 let line = tok.line + line_1.saturating_sub(1);
4615 let col = if line_1 <= 1 {
4616 tok.col + offset as u32 + col_1.saturating_sub(1)
4617 } else {
4618 col_1
4619 };
4620 Some((line, col))
4621 }
4622 }
4623 };
4624 for (node, span) in sub.module.node_spans.iter_mut() {
4625 if node.0 >= node_base {
4626 if let Some((line, col)) = lift(span.line, span.column) {
4627 span.line = line;
4628 span.column = col;
4629 }
4630 if let Some((line, col)) = lift(span.end_line, span.end_column) {
4631 span.end_line = line;
4632 span.end_column = col;
4633 }
4634 }
4635 }
4636 self.module = std::mem::replace(&mut sub.module, Module::new(""));
4637
4638 if let Some(scope) = self.ident_scopes.last_mut() {
4639 scope.extend(new_idents);
4640 }
4641 let rebase = |d: &mut Diagnostic| {
4644 if let Some(s) = &mut d.span {
4645 if let Some((line, col)) = lift(s.line, s.column) {
4646 s.line = line;
4647 s.column = col;
4648 }
4649 if let Some((line, col)) = lift(s.end_line, s.end_column) {
4650 s.end_line = line;
4651 s.end_column = col;
4652 }
4653 }
4654 };
4655 for d in sub_lex_errors
4656 .iter_mut()
4657 .chain(sub_lex_repairs.iter_mut())
4658 .chain(sub_errors.iter_mut())
4659 .chain(sub_repairs.iter_mut())
4660 {
4661 rebase(d);
4662 }
4663 let sub_file = self.current_file_name();
4666 for d in sub_lex_errors.iter_mut().chain(sub_lex_repairs.iter_mut()) {
4667 if d.file.is_none() {
4668 d.file = sub_file.clone();
4669 }
4670 }
4671 self.errors.extend(sub_lex_errors);
4672 self.errors.extend(sub_errors);
4673 self.module.healing.extend(sub_lex_repairs);
4674 self.module.healing.extend(sub_repairs);
4675 id
4676 }
4677
4678 fn parse_array(&mut self) -> NodeId {
4679 let open = self.peek().clone();
4680 self.bump(); self.array_depth += 1;
4684 let mut elements = Vec::new();
4685 loop {
4686 self.skip_newlines();
4687 if matches!(
4690 self.peek_kind(),
4691 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4692 ) {
4693 break;
4694 }
4695 if matches!(self.peek_kind(), TokenKind::Comma) {
4696 let comma = self.bump();
4698 if !self.heal(
4699 "H002",
4700 "removed stray comma".into(),
4701 &comma,
4702 Repair {
4703 original: ",".into(),
4704 suggested: String::new(),
4705 kind: RepairKind::RemoveTrailingComma,
4706 safe: true,
4707 },
4708 thresholds::AUTO_APPLY + 0.03,
4709 ) {
4710 self.error("E002", "unexpected comma");
4711 }
4712 continue;
4713 }
4714 elements.push(self.parse_expression());
4715 self.skip_newlines();
4716 if matches!(self.peek_kind(), TokenKind::Comma) {
4717 let comma = self.bump();
4718 self.skip_newlines();
4719 if matches!(
4720 self.peek_kind(),
4721 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4722 ) {
4723 self.heal(
4724 "H002",
4725 "removed trailing comma before closing bracket".into(),
4726 &comma,
4727 Repair {
4728 original: ",".into(),
4729 suggested: String::new(),
4730 kind: RepairKind::RemoveTrailingComma,
4731 safe: true,
4732 },
4733 thresholds::AUTO_APPLY + 0.03,
4734 );
4735 }
4736 } else if !matches!(
4737 self.peek_kind(),
4738 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4739 ) {
4740 let tok = self.peek().clone();
4742 if !self.heal(
4743 "H003",
4744 "inserted missing comma between array elements".into(),
4745 &tok,
4746 Repair {
4747 original: String::new(),
4748 suggested: ",".into(),
4749 kind: RepairKind::Other("insert_comma".into()),
4750 safe: false,
4751 },
4752 thresholds::HIGH + 0.05, ) {
4754 self.error(
4755 "E001",
4756 format!("expected `,` between array elements, found {:?}", tok.kind),
4757 );
4758 }
4759 }
4760 }
4761 self.expect_closing_healing(TokenKind::RBracket, "H004");
4762 self.array_depth = self.array_depth.saturating_sub(1);
4763 self.add_spanned(Node::Array { elements }, &open)
4764 }
4765
4766 fn parse_inline_table(&mut self) -> NodeId {
4767 let open = self.peek().clone();
4768 self.bump(); self.ident_scopes.push(Vec::new());
4770 let mut fields = indexmap::IndexMap::new();
4771 loop {
4772 self.skip_newlines();
4773 if matches!(
4774 self.peek_kind(),
4775 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4776 ) {
4777 break;
4778 }
4779 if matches!(self.peek_kind(), TokenKind::Comma) {
4780 let comma = self.bump();
4781 if !self.heal(
4782 "H002",
4783 "removed stray comma".into(),
4784 &comma,
4785 Repair {
4786 original: ",".into(),
4787 suggested: String::new(),
4788 kind: RepairKind::RemoveTrailingComma,
4789 safe: true,
4790 },
4791 thresholds::AUTO_APPLY + 0.03,
4792 ) {
4793 self.error("E002", "unexpected comma");
4794 }
4795 continue;
4796 }
4797 if let Some((k, id)) = self.parse_key_value() {
4798 let span = self.module.node_spans.get(&id).cloned();
4799 self.note_duplicate_key(&k, span, fields.contains_key(&k));
4800 fields.insert(k, id);
4801 } else {
4802 break;
4803 }
4804 self.skip_newlines();
4805 if matches!(self.peek_kind(), TokenKind::Comma) {
4806 let comma = self.bump();
4807 self.skip_newlines();
4808 if matches!(
4809 self.peek_kind(),
4810 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4811 ) {
4812 self.heal(
4813 "H002",
4814 "removed trailing comma before closing brace".into(),
4815 &comma,
4816 Repair {
4817 original: ",".into(),
4818 suggested: String::new(),
4819 kind: RepairKind::RemoveTrailingComma,
4820 safe: true,
4821 },
4822 thresholds::AUTO_APPLY + 0.03,
4823 );
4824 }
4825 } else if !matches!(
4826 self.peek_kind(),
4827 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4828 ) {
4829 let tok = self.peek().clone();
4830 if !self.heal(
4831 "H003",
4832 "inserted missing comma between table entries".into(),
4833 &tok,
4834 Repair {
4835 original: String::new(),
4836 suggested: ",".into(),
4837 kind: RepairKind::Other("insert_comma".into()),
4838 safe: false,
4839 },
4840 thresholds::HIGH + 0.05, ) {
4842 self.error(
4843 "E001",
4844 format!("expected `,` between table entries, found {:?}", tok.kind),
4845 );
4846 }
4847 }
4848 }
4849 self.expect_closing_healing(TokenKind::RBrace, "H004");
4850 let idents = self.ident_scopes.pop().unwrap_or_default();
4851 let _ = self.resolve_sibling_refs(idents, &fields);
4852 self.add_spanned(Node::Table { fields }, &open)
4853 }
4854
4855 fn build_graph(&mut self) {
4860 let bindings = self.module.bindings.clone();
4861 let declared: Vec<(NodeId, Vec<String>)> = self
4866 .module
4867 .depends_on
4868 .iter()
4869 .map(|(id, dep)| (*id, dep.paths.clone()))
4870 .collect();
4871 for (scope, paths) in &declared {
4872 for path in paths {
4873 if let Some(target) = self.module.resolve_value_path(path) {
4874 self.module.add_edge(target, *scope);
4875 }
4876 }
4877 }
4878
4879 for i in 0..self.module.arena.len() {
4880 let id = NodeId(i as u32);
4881 let node = self.module.get(id).cloned();
4882 match node {
4883 Some(Node::Ident(name)) => {
4884 if let Some(&target) = bindings.get(&name) {
4885 if let Some(slot) = self.module.get_mut(id) {
4886 *slot = Node::Ref(target);
4887 }
4888 self.module.add_edge(target, id);
4890 }
4891 }
4892 Some(Node::Env {
4893 default: Some(def), ..
4894 }) => {
4895 self.module.add_edge(def, id);
4896 }
4897 Some(Node::Interpolate { parts }) => {
4898 for part in &parts {
4899 if let InterpPart::Expr(expr_id) = part {
4900 self.module.add_edge(*expr_id, id);
4901 }
4902 }
4903 }
4904 Some(Node::Binary { lhs, rhs, .. }) => {
4905 self.module.add_edge(lhs, id);
4906 self.module.add_edge(rhs, id);
4907 }
4908 Some(Node::Unary { expr, .. }) => {
4909 self.module.add_edge(expr, id);
4910 }
4911 Some(Node::Quantity { value, .. }) => {
4912 self.module.add_edge(value, id);
4913 }
4914 Some(Node::Semantic { inner, .. }) => {
4915 self.module.add_edge(inner, id);
4916 }
4917 Some(Node::SchemaField { field }) => {
4918 for arg in [
4921 field.min,
4922 field.max,
4923 field.min_len,
4924 field.max_len,
4925 field.pattern,
4926 field.default,
4927 ]
4928 .into_iter()
4929 .flatten()
4930 {
4931 self.module.add_edge(arg, id);
4932 }
4933 }
4934 Some(Node::SchemaTable { fields, .. }) => {
4935 for child in fields.values() {
4936 self.module.add_edge(*child, id);
4937 }
4938 }
4939 Some(Node::SchemaArray { element }) => {
4940 self.module.add_edge(element, id);
4941 }
4942 Some(Node::Array { elements }) => {
4943 for e in &elements {
4944 self.module.add_edge(*e, id);
4945 }
4946 }
4947 Some(Node::Table { fields }) => {
4948 for (_, fid) in &fields {
4949 self.module.add_edge(*fid, id);
4950 }
4951 }
4952 Some(Node::Secret { locator }) => {
4953 self.module.add_edge(locator, id);
4954 }
4955 _ => {}
4956 }
4957 }
4958 }
4959}