1mod lexer;
23
24use lexer::ESCAPED_DOLLAR;
25use lexer::SENTINEL_POS;
26pub use lexer::{Lexed, Lexer, Token, TokenKind};
27
28fn span_of(tok: &Token) -> Span {
30 Span {
31 line: tok.line,
32 column: tok.col,
33 end_line: tok.end_line,
34 end_column: tok.end_col,
35 }
36}
37
38fn parse_annotation_string(payload: &str) -> Result<String, String> {
44 let mut chars = payload.chars().peekable();
45 let quote = chars.next().ok_or("expected a string literal")?;
46 if quote != '"' && quote != '\'' {
47 return Err("expected a string literal".to_string());
48 }
49 let mut out = String::new();
50 let mut escaped = false;
51 for c in chars.by_ref() {
52 if escaped {
53 match (quote, c) {
54 ('"', 'n') => out.push('\n'),
55 ('"', 't') => out.push('\t'),
56 ('"', 'r') => out.push('\r'),
57 ('"', '0') => out.push('\0'),
58 ('"', '$') => out.push('$'),
59 (_, '\\') => out.push('\\'),
60 ('"', '"') => out.push('"'),
61 ('\'', '\'') => out.push('\''),
62 _ => {
63 out.push('\\');
64 out.push(c);
65 }
66 }
67 escaped = false;
68 } else if c == '\\' {
69 escaped = true;
70 } else if c == quote {
71 for rest in chars.by_ref() {
73 if !rest.is_whitespace() {
74 return Err("unexpected text after the string literal".to_string());
75 }
76 }
77 return Ok(out);
78 } else {
79 out.push(c);
80 }
81 }
82 Err("unterminated string literal".to_string())
83}
84
85const MAX_EXPR_DEPTH: usize = 64;
90
91const MAX_BLOCK_DEPTH: usize = 64;
94
95const MAX_SCHEMA_DEPTH: usize = 64;
99
100use crate::diag::{thresholds, Diagnostic, Repair, RepairKind, Severity, Span};
101use crate::ir::{NodeId, SchemaField, SemanticTag, *};
102use crate::types::{ActiveType, Unit};
103use indexmap::IndexMap;
104
105#[allow(clippy::arc_with_non_send_sync)]
116pub type IncludeLoader = std::sync::Arc<dyn Fn(&str) -> Result<String, String>>;
117
118#[derive(Clone)]
120pub struct ParseOptions {
121 pub resilient: bool,
125 pub enable_includes: bool,
129 pub resolve_includes: bool,
133 pub allow_parent_escapes: bool,
136 pub include_loader: Option<IncludeLoader>,
139}
140
141impl Default for ParseOptions {
142 fn default() -> Self {
143 Self {
144 resilient: false,
145 enable_includes: false,
146 resolve_includes: true,
147 allow_parent_escapes: false,
148 include_loader: None,
149 }
150 }
151}
152
153impl ParseOptions {
154 pub const fn strict() -> Self {
156 Self {
157 resilient: false,
158 enable_includes: false,
159 resolve_includes: true,
160 allow_parent_escapes: false,
161 include_loader: None,
162 }
163 }
164
165 pub const fn resilient_mode() -> Self {
167 Self {
168 resilient: true,
169 enable_includes: false,
170 resolve_includes: true,
171 allow_parent_escapes: false,
172 include_loader: None,
173 }
174 }
175}
176
177pub fn parse(source: &str, name: &str) -> Result<Module, Vec<Diagnostic>> {
178 parse_with(source, name, ParseOptions::default())
179}
180
181pub fn parse_with(
182 source: &str,
183 name: &str,
184 options: ParseOptions,
185) -> Result<Module, Vec<Diagnostic>> {
186 let (module, errors) = parse_recovering(source, name, options);
187 if errors.is_empty() {
188 Ok(module)
189 } else {
190 Err(errors)
191 }
192}
193
194pub fn parse_text_with_file(
202 source: &str,
203 name: &str,
204 options: ParseOptions,
205 path: &std::path::Path,
206) -> Result<Module, Vec<Diagnostic>> {
207 let mut options = options;
208 options.enable_includes = true;
209 let (module, errors) = parse_recovering_impl(source, name, options, Some(path), None);
210 if errors.is_empty() {
211 Ok(module)
212 } else {
213 Err(errors)
214 }
215}
216
217pub fn parse_text_recovering_with_file(
223 source: &str,
224 name: &str,
225 options: ParseOptions,
226 path: &std::path::Path,
227) -> (Module, Vec<Diagnostic>) {
228 let mut options = options;
229 options.enable_includes = true;
230 parse_recovering_impl(source, name, options, Some(path), None)
231}
232
233pub fn parse_file(
238 path: &std::path::Path,
239 options: ParseOptions,
240) -> Result<Module, Vec<Diagnostic>> {
241 let src = std::fs::read_to_string(path).map_err(|e| {
242 vec![Diagnostic {
243 severity: Severity::Error,
244 code: "E017".into(),
245 message: format!("cannot read file {}: {e}", path.display()),
246 path: None,
247 span: None,
248 repair: None,
249 confidence: 1.0,
250 file: Some(path.to_string_lossy().into_owned()),
251 }]
252 })?;
253 let mut options = options;
254 options.enable_includes = true;
255 let (module, errors) =
256 parse_recovering_impl(&src, &path.to_string_lossy(), options, Some(path), None);
257 if errors.is_empty() {
258 Ok(module)
259 } else {
260 Err(errors)
261 }
262}
263
264pub fn parse_file_with_trust(
270 path: &std::path::Path,
271 options: ParseOptions,
272 trusted: &crate::sign::TrustedKeys,
273) -> Result<Module, Vec<Diagnostic>> {
274 let src = std::fs::read_to_string(path).map_err(|e| {
275 vec![Diagnostic {
276 severity: Severity::Error,
277 code: "E017".into(),
278 message: format!("cannot read file {}: {e}", path.display()),
279 path: None,
280 span: None,
281 repair: None,
282 confidence: 1.0,
283 file: Some(path.to_string_lossy().into_owned()),
284 }]
285 })?;
286 let mut options = options;
287 options.enable_includes = true;
288 let (module, errors) = parse_recovering_impl(
289 &src,
290 &path.to_string_lossy(),
291 options,
292 Some(path),
293 Some(trusted.clone()),
294 );
295 if errors.is_empty() {
296 Ok(module)
297 } else {
298 Err(errors)
299 }
300}
301
302pub fn parse_recovering(
308 source: &str,
309 name: &str,
310 options: ParseOptions,
311) -> (Module, Vec<Diagnostic>) {
312 parse_recovering_impl(source, name, options, None, None)
313}
314
315fn parse_recovering_impl(
316 source: &str,
317 name: &str,
318 options: ParseOptions,
319 root_file: Option<&std::path::Path>,
320 trust: Option<crate::sign::TrustedKeys>,
321) -> (Module, Vec<Diagnostic>) {
322 let mut lexer = Lexer::new(source);
323 let mut lexed = lexer.tokenize();
324 let n_comments = lexed.comments.len();
325 let root_stamp = root_file.map(|p| p.to_string_lossy().into_owned());
329 let mut parser = Parser {
330 tokens: lexed.tokens,
331 pos: 0,
332 module: Module::new(name),
333 errors: Vec::new(),
334 ident_scopes: Vec::new(),
335 depth: 0,
336 options,
337 block_depth: 0,
338 schema_depth: 0,
339 comments: lexed.comments,
340 comment_files: vec![root_stamp; n_comments],
341 statements: Vec::new(),
342 blocks: Vec::new(),
343 current_path: Vec::new(),
344 brace_tables: 0,
345 indent_tables: 0,
346 current_file: root_file.map(|p| p.to_path_buf()),
347 include_stack: Vec::new(),
348 in_include: false,
349 in_profile: false,
350 array_depth: 0,
351 binding_spliced: std::collections::HashSet::new(),
352 pending_profiles: Vec::new(),
353 trust,
354 };
355 let lex_file = parser.current_file_name();
358 for d in lexed.errors.iter_mut().chain(lexed.repairs.iter_mut()) {
359 if d.file.is_none() {
360 d.file = lex_file.clone();
361 }
362 }
363 parser.module.healing.extend(lexed.repairs);
364 parser.errors.extend(lexed.errors);
365 parser.parse_document();
366 parser.extract_schemas();
370 parser.extract_depends_on();
372 parser.attach_trivia();
373 parser.module.source_style = if parser.indent_tables > parser.brace_tables {
374 Style::Indent
375 } else {
376 Style::Brace
377 };
378 parser.build_graph();
379 (parser.module, parser.errors)
380}
381
382fn pos_entry(pos: &[(u32, u32)], k: usize, fallback: (u32, u32)) -> (u32, u32) {
386 let mut j = k;
387 loop {
388 match pos.get(j) {
389 Some(&e) if e != SENTINEL_POS => return e,
390 Some(_) => {
391 if j == 0 {
392 return fallback;
393 }
394 j -= 1;
395 }
396 None => return fallback,
397 }
398 }
399}
400
401impl Parser {
404 fn attach_trivia(&mut self) {
405 let comments = std::mem::take(&mut self.comments);
406 let comment_files = std::mem::take(&mut self.comment_files);
409 let statements = std::mem::take(&mut self.statements);
410 let blocks = std::mem::take(&mut self.blocks);
411
412 let mut trivia = Vec::with_capacity(comments.len());
413 for ((text, span), file) in comments.into_iter().zip(comment_files) {
414 let attachment = self.attach_one(&span, &file, &statements, &blocks);
415 trivia.push(TriviaComment {
416 text,
417 span,
418 attachment,
419 file,
420 });
421 }
422 self.module.trivia = trivia;
423 }
424
425 fn attach_one(
426 &self,
427 span: &Span,
428 file: &Option<String>,
429 stmts: &[StmtInfo],
430 blocks: &[BlockInfo],
431 ) -> Attachment {
432 let line = span.line;
433 let level = span.column.saturating_sub(1) as usize;
434
435 if let Some(stmt) = stmts
440 .iter()
441 .filter(|s| &s.file == file && (s.start_line == line || s.end_line == line))
442 .max_by_key(|s| s.start_line)
443 {
444 return Attachment::Trailing(qualified(&stmt.path, &stmt.key));
445 }
446
447 let owner = self.owning_block(line, level, file, stmts, blocks);
451 if let Some(b) = &owner {
452 if let Some(stmt) = stmts
455 .iter()
456 .filter(|s| &s.file == file && s.start_line > line && s.path == b.path)
457 .min_by_key(|s| s.start_line)
458 {
459 return Attachment::Leading(qualified(&stmt.path, &stmt.key));
460 }
461 return Attachment::TrailingBlock(b.path.clone());
463 }
464
465 if let Some(stmt) = stmts
467 .iter()
468 .filter(|s| &s.file == file && s.start_line > line && s.path.is_empty())
469 .min_by_key(|s| s.start_line)
470 {
471 return Attachment::Leading(qualified(&stmt.path, &stmt.key));
472 }
473 Attachment::TrailingDocument
474 }
475
476 fn owning_block(
478 &self,
479 line: u32,
480 level: usize,
481 file: &Option<String>,
482 stmts: &[StmtInfo],
483 blocks: &[BlockInfo],
484 ) -> Option<BlockInfo> {
485 fn score(b: &BlockInfo, level: usize) -> (u8, usize, u32) {
488 (if b.level == level { 1 } else { 0 }, b.level, b.start_line)
489 }
490 let mut best: Option<&BlockInfo> = None;
491 for b in blocks {
492 if &b.file != file || b.start_line > line {
493 continue;
494 }
495 let next_sibling = stmts
498 .iter()
499 .filter(|s| &s.file == file && s.start_line > line && s.level <= b.level)
500 .min_by_key(|s| s.start_line)
501 .map(|s| s.start_line)
502 .unwrap_or(u32::MAX);
503 if line >= next_sibling {
504 continue;
505 }
506 let better = match best {
507 None => true,
508 Some(prev) => score(b, level) >= score(prev, level),
509 };
510 if better {
511 best = Some(b);
512 }
513 }
514 best.cloned()
515 }
516}
517
518fn qualified(path: &str, key: &str) -> String {
521 if path.is_empty() {
522 key.to_string()
523 } else {
524 format!("{path}.{key}")
525 }
526}
527
528pub fn validate_patch_path(path: &str) -> Result<Vec<PathSeg>, String> {
535 fn is_key(s: &str) -> bool {
536 let mut chars = s.chars();
537 match chars.next() {
538 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
539 _ => return false,
540 }
541 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
542 }
543 if path.is_empty() {
544 return Err("empty path".into());
545 }
546 let mut segs = Vec::new();
547 for part in path.split('.') {
548 if part.is_empty() {
549 return Err("empty segment".into());
550 }
551 let mut rest = part;
554 let key_end = rest.find('[').unwrap_or(rest.len());
555 let key = &rest[..key_end];
556 if !is_key(key) {
557 return Err(format!("invalid key `{key}` in segment `{part}`"));
558 }
559 segs.push(PathSeg::Key(key.to_string()));
560 rest = &rest[key_end..];
561 while !rest.is_empty() {
562 if !rest.starts_with('[') {
563 return Err(format!("unexpected text in segment `{part}`"));
564 }
565 let close = rest
566 .find(']')
567 .ok_or_else(|| format!("unclosed bracket in segment `{part}`"))?;
568 let idx: usize = rest[1..close]
569 .parse()
570 .map_err(|_| format!("array index must be a non-negative integer in `{part}`"))?;
571 segs.push(PathSeg::Index(idx));
572 rest = &rest[close + 1..];
573 }
574 }
575 Ok(segs)
576}
577
578#[derive(Debug, Clone, PartialEq, Eq)]
581pub enum PathSeg {
582 Key(String),
583 Index(usize),
584}
585struct Parser {
586 tokens: Vec<Token>,
587 pos: usize,
588 module: Module,
589 errors: Vec<Diagnostic>,
590 ident_scopes: Vec<Vec<(String, NodeId)>>,
595 depth: usize,
597 options: ParseOptions,
599 block_depth: usize,
601 schema_depth: usize,
605 comments: Vec<(String, Span)>,
607 comment_files: Vec<Option<String>>,
612 statements: Vec<StmtInfo>,
614 blocks: Vec<BlockInfo>,
616 current_path: Vec<String>,
618 brace_tables: usize,
620 indent_tables: usize,
621 current_file: Option<std::path::PathBuf>,
624 include_stack: Vec<std::path::PathBuf>,
626 in_include: bool,
629 in_profile: bool,
632 array_depth: usize,
637 binding_spliced: std::collections::HashSet<String>,
641 pending_profiles: Vec<ProfileScope>,
645 trust: Option<crate::sign::TrustedKeys>,
649}
650
651impl Parser {
652 fn path_str(&self) -> String {
654 self.current_path.join(".")
655 }
656
657 fn last_end_line(&self) -> u32 {
659 self.tokens
660 .get(self.pos.saturating_sub(1))
661 .map(|t| t.end_line)
662 .unwrap_or(1)
663 }
664
665 fn record_stmt(&mut self, key: &str, start_line: u32, end_line: u32, level: usize) {
667 self.statements.push(StmtInfo {
668 key: key.to_string(),
669 path: self.path_str(),
670 start_line,
671 end_line,
672 level,
673 file: self.current_file_name(),
674 });
675 }
676}
677
678#[derive(Debug, Clone)]
680struct StmtInfo {
681 key: String,
682 path: String,
683 start_line: u32,
684 end_line: u32,
685 level: usize,
686 file: Option<String>,
689}
690
691#[derive(Debug, Clone)]
693struct BlockInfo {
694 path: String,
695 level: usize,
696 start_line: u32,
697 file: Option<String>,
699}
700
701#[derive(Debug, Clone)]
704struct PendingInclude {
705 path: String,
706 alias: Option<String>,
707 private: bool,
708 tok: Token,
709 patch_mark: usize,
713}
714
715#[derive(Debug, Clone, Copy, PartialEq, Eq)]
717enum SchemaEnd {
718 Brace,
720 Indent,
722}
723
724type ProfileScope = (Vec<(String, NodeId)>, indexmap::IndexMap<String, NodeId>);
727
728#[derive(Debug, Clone, Copy, PartialEq, Eq)]
730enum BodyEnd {
731 Document,
733 Block(usize),
736 Brace,
738 HeaderBody(usize),
741}
742
743fn indent_level(tok: &Token) -> usize {
745 tok.text.parse().unwrap_or(0)
746}
747
748impl Parser {
749 fn peek(&self) -> &Token {
754 self.tokens.get(self.pos).unwrap_or_else(|| {
758 self.tokens
759 .last()
760 .expect("token stream is never empty; the lexer appends Eof")
761 })
762 }
763
764 fn peek_kind(&self) -> &TokenKind {
765 &self.peek().kind
766 }
767
768 fn bump(&mut self) -> Token {
769 let t = self.peek().clone();
770 if self.pos < self.tokens.len() {
771 self.pos += 1;
772 }
773 t
774 }
775
776 fn expect(&mut self, kind: TokenKind) -> Option<Token> {
777 if std::mem::discriminant(self.peek_kind()) == std::mem::discriminant(&kind) {
779 Some(self.bump())
780 } else {
781 self.error(
782 "E001",
783 format!("expected {:?}, found {:?}", kind, self.peek_kind()),
784 );
785 None
786 }
787 }
788
789 fn error(&mut self, code: &str, message: impl Into<String>) {
790 let t = self.peek();
791 self.errors.push(Diagnostic {
792 severity: Severity::Error,
793 code: code.into(),
794 message: message.into(),
795 path: None,
796 span: Some(Span {
797 line: t.line,
798 column: t.col,
799 end_line: t.end_line,
800 end_column: t.end_col,
801 }),
802 repair: None,
803 confidence: 1.0,
804 file: self.current_file_name(),
805 });
806 }
807
808 fn skip_newlines(&mut self) {
809 while matches!(self.peek_kind(), TokenKind::Newline) {
810 self.bump();
811 }
812 }
813
814 fn peek_kind_at(&self, n: usize) -> TokenKind {
817 self.peek_at(n)
818 .map(|t| t.kind.clone())
819 .unwrap_or(TokenKind::Eof)
820 }
821
822 fn peek_at(&self, n: usize) -> Option<Token> {
823 self.tokens.get(self.pos + n).cloned()
824 }
825
826 fn parse_body(
832 &mut self,
833 fields: &mut indexmap::IndexMap<String, NodeId>,
834 end: BodyEnd,
835 pending: &mut Vec<PendingInclude>,
836 ) {
837 self.block_depth += 1;
838 if self.block_depth > MAX_BLOCK_DEPTH {
839 self.error(
840 "E011",
841 format!("maximum block nesting depth ({MAX_BLOCK_DEPTH}) exceeded"),
842 );
843 self.block_depth -= 1;
844 self.recover_block(end);
845 return;
846 }
847 self.parse_body_inner(fields, end, pending);
848 self.block_depth -= 1;
849 }
850
851 fn recover_block(&mut self, end: BodyEnd) {
856 let mut open_braces = 0usize;
857 loop {
858 match self.peek_kind() {
859 TokenKind::Eof => return,
860 TokenKind::LBrace => {
861 open_braces += 1;
862 self.bump();
863 }
864 TokenKind::RBrace => {
865 if open_braces > 0 {
866 open_braces -= 1;
867 self.bump();
868 } else if end == BodyEnd::Brace {
869 self.bump();
870 return;
871 } else {
872 self.bump();
873 }
874 }
875 TokenKind::Dedent => {
876 let tok = self.peek().clone();
877 let target = indent_level(&tok);
878 match end {
879 BodyEnd::Block(level) | BodyEnd::HeaderBody(level) => {
880 self.bump();
881 if target < level {
882 return;
883 }
884 }
885 _ => {
886 self.bump();
887 }
888 }
889 }
890 TokenKind::LBracket | TokenKind::Let if matches!(end, BodyEnd::HeaderBody(_)) => {
891 return
892 }
893 _ => {
894 self.bump();
895 }
896 }
897 }
898 }
899
900 fn parse_body_inner(
901 &mut self,
902 fields: &mut indexmap::IndexMap<String, NodeId>,
903 end: BodyEnd,
904 pending: &mut Vec<PendingInclude>,
905 ) {
906 loop {
907 self.skip_newlines();
908 match self.peek_kind() {
909 TokenKind::Eof => {
910 if end == BodyEnd::Brace {
911 self.error("E001", "expected `}`, found end of input");
912 }
913 return;
914 }
915 TokenKind::Dedent => {
916 let tok = self.peek().clone();
917 let target = indent_level(&tok);
918 match end {
919 BodyEnd::Document | BodyEnd::Brace => {
922 self.bump();
923 }
924 BodyEnd::Block(level) => {
925 self.bump();
926 if target >= level {
927 continue;
930 }
931 return;
932 }
933 BodyEnd::HeaderBody(level) => {
934 if target >= level {
935 self.bump();
936 continue;
937 }
938 return;
939 }
940 }
941 }
942 TokenKind::RBrace => match end {
943 BodyEnd::Brace => {
944 self.bump();
945 return;
946 }
947 BodyEnd::Block(_) => {
949 self.error("E001", "expected Dedent to close this block, found `}`");
950 self.bump();
951 return;
952 }
953 BodyEnd::HeaderBody(_) => return,
954 BodyEnd::Document => {
955 self.error("E002", "unexpected `}`");
956 self.bump();
957 }
958 },
959 TokenKind::LBracket | TokenKind::Let if matches!(end, BodyEnd::HeaderBody(_)) => {
967 return
968 }
969 TokenKind::Ident => {
970 let t = self.peek().clone();
971 if let BodyEnd::HeaderBody(level) = end {
972 if (t.text == "profile" || t.text == "include")
973 && t.col.saturating_sub(1) as usize <= level
974 && !matches!(
975 self.peek_kind_at(1),
976 TokenKind::Assign | TokenKind::Colon | TokenKind::LBrace
977 )
978 {
979 return;
980 }
981 }
982 self.parse_ident_statement(fields, pending);
983 }
984 TokenKind::Let => self.parse_let(),
985 TokenKind::LBracket => self.parse_table_header(fields),
986 TokenKind::At => {
987 if let BodyEnd::HeaderBody(level) = end {
994 let is_patch = matches!(self.peek_kind_at(1), TokenKind::Ident) && {
995 let tag = self.peek_at(1).map(|t| t.text.clone()).unwrap_or_default();
996 (tag == "patch" || tag == "remove" || tag == "rename")
997 && matches!(self.peek_kind_at(2), TokenKind::LParen)
998 };
999 let t = self.peek().clone();
1000 if is_patch && t.col.saturating_sub(1) as usize <= level {
1001 return;
1002 }
1003 }
1004 let at_tok = self.peek().clone();
1005 if matches!(self.peek_kind_at(1), TokenKind::Ident) {
1006 let tag = self.peek_at(1).map(|t| t.text.clone()).unwrap_or_default();
1007 if tag == "patch" || tag == "remove" || tag == "rename" {
1008 if !matches!(self.peek_kind_at(2), TokenKind::LParen) {
1012 let tag_tok = self.peek_at(1).unwrap_or_else(|| at_tok.clone());
1013 self.error_at(
1014 "E019",
1015 format!("expected `(` after `@{tag}`"),
1016 &tag_tok,
1017 );
1018 self.bump();
1019 self.bump();
1020 continue;
1021 }
1022 self.parse_patch_statement(&at_tok);
1023 continue;
1024 }
1025 }
1026 self.bump();
1027 let id = self.parse_semantic_block(&at_tok);
1028 if let Some(Node::Semantic { tag, .. }) = self.module.get(id) {
1029 let key = tag.as_str().to_string();
1030 self.record_stmt(
1031 &key,
1032 at_tok.line,
1033 self.last_end_line(),
1034 at_tok.col.saturating_sub(1) as usize,
1035 );
1036 fields.insert(key, id);
1037 }
1038 }
1039 TokenKind::Indent => {
1040 self.error("E010", "unexpected indentation");
1045 self.bump();
1046 }
1047 TokenKind::String(_)
1048 | TokenKind::SingleString
1049 | TokenKind::MlString(_)
1050 | TokenKind::MlSingleString => {
1051 let tok = self.peek().clone();
1056 self.error_at(
1057 "E002",
1058 "unexpected string literal at statement position (keys must be identifiers)",
1059 &tok,
1060 );
1061 self.bump();
1062 }
1063 _ => {
1064 let tok = self.peek().clone();
1078 let stmt_col = match end {
1079 BodyEnd::Block(l) | BodyEnd::HeaderBody(l) => (l + 1) as u32,
1080 _ => 1,
1081 };
1082 match &tok.kind {
1083 TokenKind::Integer
1084 | TokenKind::Float
1085 | TokenKind::True
1086 | TokenKind::False
1087 | TokenKind::Null
1088 | TokenKind::Env
1089 | TokenKind::Secret
1090 | TokenKind::Minus
1091 | TokenKind::Bang
1092 | TokenKind::LParen
1093 if tok.col == stmt_col =>
1094 {
1095 self.error_at(
1096 "E002",
1097 "unexpected value at statement position (values need a key: `name = …`)",
1098 &tok,
1099 );
1100 }
1101 _ => {}
1102 }
1103 self.bump();
1104 }
1105 }
1106 }
1107 }
1108
1109 fn parse_ident_statement(
1114 &mut self,
1115 fields: &mut indexmap::IndexMap<String, NodeId>,
1116 pending: &mut Vec<PendingInclude>,
1117 ) {
1118 let start = self.peek().clone();
1119 let level = start.col.saturating_sub(1) as usize;
1120 if start.text == "include"
1121 && !matches!(
1122 self.peek_kind_at(1),
1123 TokenKind::Assign | TokenKind::Colon | TokenKind::LBrace
1124 )
1125 {
1126 self.parse_include(pending);
1127 return;
1128 }
1129 if start.text == "profile"
1130 && !matches!(
1131 self.peek_kind_at(1),
1132 TokenKind::Assign | TokenKind::Colon | TokenKind::LBrace
1133 )
1134 {
1135 self.parse_profile();
1136 return;
1137 }
1138 match self.peek_kind_at(1) {
1139 TokenKind::Assign => {
1140 if let Some((k, id)) = self.parse_key_value_sep(TokenKind::Assign) {
1141 let end = self.last_end_line();
1142 self.record_stmt(&k, start.line, end, level);
1143 self.extract_annotations(&k, id, start.line);
1144 if self.check_depends_on_statement(&k, id) {
1145 self.note_duplicate_key(&k, Some(span_of(&start)), fields.contains_key(&k));
1146 fields.insert(k, id);
1147 }
1148 }
1149 }
1150 TokenKind::Colon => {
1151 if matches!(self.peek_kind_at(2), TokenKind::Newline | TokenKind::Eof) {
1152 self.parse_block_header(fields);
1153 } else if let Some((k, id)) = self.parse_key_value_sep(TokenKind::Colon) {
1154 let end = self.last_end_line();
1155 self.record_stmt(&k, start.line, end, level);
1156 self.extract_annotations(&k, id, start.line);
1157 if self.check_depends_on_statement(&k, id) {
1158 self.note_duplicate_key(&k, Some(span_of(&start)), fields.contains_key(&k));
1159 fields.insert(k, id);
1160 }
1161 }
1162 }
1163 TokenKind::LBrace => self.parse_table_block(fields),
1164 _ => {
1165 let tok = self.peek().clone();
1167 self.error_at("E002", "unexpected token after identifier", &tok);
1168 self.bump();
1169 }
1170 }
1171 }
1172
1173 fn parse_profile(&mut self) {
1182 let kw = self.bump(); if !self.current_path.is_empty() {
1184 self.error_at(
1185 "E018",
1186 "profile blocks may only appear at document root",
1187 &kw,
1188 );
1189 return;
1190 }
1191 let name_tok = self.peek().clone();
1192 let name = match &name_tok.kind {
1193 TokenKind::String(_)
1194 | TokenKind::SingleString
1195 | TokenKind::MlString(_)
1196 | TokenKind::MlSingleString => {
1197 self.bump();
1198 if name_tok.text.contains("${") {
1201 self.error_at(
1202 "E018",
1203 "profile names must be plain string literals",
1204 &name_tok,
1205 );
1206 return;
1207 }
1208 name_tok.text.clone()
1209 }
1210 _ => {
1211 self.error_at("E018", "profile blocks need a string name", &name_tok);
1212 return;
1213 }
1214 };
1215 let name = Self::render_marked(&name, &Self::sentinel_marks(&name, &name_tok.kind));
1221 if self.module.profiles.contains_key(&name) {
1222 self.error_at("E018", format!("duplicate profile \"{name}\""), &name_tok);
1223 return;
1224 }
1225 enum ProfileBody {
1228 Brace,
1229 Block(usize),
1230 }
1231 let body = match self.peek_kind() {
1232 TokenKind::LBrace => {
1233 self.bump();
1234 ProfileBody::Brace
1235 }
1236 TokenKind::Colon => {
1237 let colon = self.bump();
1238 self.skip_newlines();
1239 match self.peek_kind() {
1240 TokenKind::Indent => {
1241 let indent = self.bump();
1242 ProfileBody::Block(indent_level(&indent))
1243 }
1244 _ => {
1245 self.error_at("E018", "profile block has no indented body", &colon);
1246 return;
1247 }
1248 }
1249 }
1250 _ => {
1251 let tok = self.peek().clone();
1252 self.error_at("E018", "expected `{` or `:` after the profile name", &tok);
1253 return;
1254 }
1255 };
1256 self.record_stmt(
1257 &name,
1258 kw.line,
1259 self.last_end_line(),
1260 kw.col.saturating_sub(1) as usize,
1261 );
1262 let profile_stmt = self.statements.len() - 1;
1263 self.current_path.push(name.clone());
1264 self.ident_scopes.push(Vec::new());
1265 let mut fields = indexmap::IndexMap::new();
1266 let mut pending = Vec::new();
1267 let saved_in_profile = self.in_profile;
1268 self.in_profile = true;
1269 match body {
1270 ProfileBody::Brace => self.parse_body(&mut fields, BodyEnd::Brace, &mut pending),
1271 ProfileBody::Block(level) => {
1272 self.parse_body(&mut fields, BodyEnd::Block(level), &mut pending)
1273 }
1274 }
1275 self.in_profile = saved_in_profile;
1276 self.splice_includes(&mut fields, pending, true);
1277 let idents = self.ident_scopes.pop().unwrap_or_default();
1278 self.pending_profiles.push((idents, fields.clone()));
1282 let table_id = self.module.add_node(Node::Table { fields });
1283 self.statements[profile_stmt].end_line = self.last_end_line();
1284 self.current_path.pop();
1285 self.module.profiles.insert(name, table_id);
1286 }
1287
1288 const MAX_INCLUDE_DEPTH: usize = 32;
1294
1295 fn parse_include(&mut self, pending: &mut Vec<PendingInclude>) {
1300 let kw = self.bump(); let path_tok = self.peek().clone();
1302 let path = match &path_tok.kind {
1303 TokenKind::String(_)
1304 | TokenKind::SingleString
1305 | TokenKind::MlString(_)
1306 | TokenKind::MlSingleString => {
1307 self.bump();
1308 path_tok.text.clone()
1309 }
1310 _ => {
1311 self.error_at("E015", "include paths must be string literals", &path_tok);
1312 self.bump(); return;
1314 }
1315 };
1316 if path.contains("${")
1317 || Self::has_marked(&path, &Self::sentinel_marks(&path, &path_tok.kind))
1318 {
1319 self.error_at(
1320 "E015",
1321 "interpolation in include paths is a hard error; paths must be static string literals",
1322 &path_tok,
1323 );
1324 return;
1325 }
1326 let mut alias: Option<String> = None;
1328 let mut private = false;
1329 loop {
1330 let t = self.peek().clone();
1331 match &t.kind {
1332 TokenKind::Ident if t.text == "as" && alias.is_none() => {
1333 self.bump();
1334 match self.expect(TokenKind::Ident) {
1335 Some(name) => alias = Some(name.text),
1336 None => return,
1337 }
1338 }
1339 TokenKind::Ident if t.text == "private" && !private => {
1340 self.bump();
1341 private = true;
1342 }
1343 _ => break,
1344 }
1345 }
1346 let key = alias.clone().unwrap_or_else(|| path.clone());
1347 self.record_stmt(
1348 &key,
1349 kw.line,
1350 self.last_end_line(),
1351 kw.col.saturating_sub(1) as usize,
1352 );
1353 self.module.includes.push(IncludeStmt {
1354 path: path.clone(),
1355 alias: alias.clone(),
1356 private,
1357 scope_path: self.current_path.clone(),
1358 line: kw.line,
1359 });
1360 pending.push(PendingInclude {
1361 path,
1362 alias,
1363 private,
1364 tok: path_tok,
1365 patch_mark: self.module.patches.len(),
1366 });
1367 }
1368
1369 fn load_include(&mut self, inc: &PendingInclude) -> Option<NodeId> {
1375 use std::path::{Component, PathBuf};
1376 let fail = |me: &mut Parser, code: &str, msg: String| {
1377 me.error_at(code, msg, &inc.tok);
1378 None
1379 };
1380 let raw = std::path::Path::new(&inc.path);
1382 if !self.options.allow_parent_escapes {
1383 if raw.is_absolute() {
1384 return fail(
1385 self,
1386 "E015",
1387 format!(
1388 "include paths must be relative (got `{}`); pass --allow-parent-escapes to override",
1389 inc.path
1390 ),
1391 );
1392 }
1393 if raw.components().any(|c| matches!(c, Component::ParentDir)) {
1394 return fail(
1395 self,
1396 "E015",
1397 format!(
1398 "include paths must not escape the including directory via `..` (got `{}`); pass --allow-parent-escapes to override",
1399 inc.path
1400 ),
1401 );
1402 }
1403 }
1404 let base_dir: PathBuf = self
1405 .current_file
1406 .as_ref()
1407 .and_then(|f| f.parent().map(|p| p.to_path_buf()))
1408 .unwrap_or_else(|| PathBuf::from("."));
1409 let joined = base_dir.join(raw);
1410 fn lexical_join(base: &std::path::Path, raw: &std::path::Path) -> PathBuf {
1418 let mut out = base.to_path_buf();
1419 for c in raw.components() {
1420 match c {
1421 Component::CurDir => {}
1422 Component::ParentDir => {
1423 out.pop();
1424 }
1425 Component::Normal(s) => out.push(s),
1426 Component::RootDir | Component::Prefix(_) => {
1427 out = PathBuf::from(c.as_os_str());
1428 }
1429 }
1430 }
1431 out
1432 }
1433 let has_loader = self.options.include_loader.is_some();
1434 let canonical = if has_loader {
1435 lexical_join(&base_dir, raw)
1436 } else {
1437 match joined.canonicalize() {
1438 Ok(p) => p,
1439 Err(e) => {
1440 return fail(
1441 self,
1442 "E017",
1443 format!("cannot read include file `{}`: {e}", joined.display()),
1444 );
1445 }
1446 }
1447 };
1448 if !self.options.allow_parent_escapes {
1449 let contained = if has_loader {
1450 canonical.starts_with(lexical_join(&base_dir, std::path::Path::new("")))
1451 } else if let Ok(base_canon) = base_dir.canonicalize() {
1452 canonical.starts_with(&base_canon)
1453 } else {
1454 true
1455 };
1456 if !contained {
1457 return fail(
1458 self,
1459 "E015",
1460 format!(
1461 "include target `{}` escapes the including directory; pass --allow-parent-escapes to override",
1462 canonical.display()
1463 ),
1464 );
1465 }
1466 }
1467 if self.include_stack.contains(&canonical) {
1468 return fail(
1469 self,
1470 "E016",
1471 format!(
1472 "circular include detected: `{}` is already being parsed ({})",
1473 canonical.display(),
1474 self.include_stack
1475 .iter()
1476 .map(|p| p.display().to_string())
1477 .collect::<Vec<_>>()
1478 .join(" -> ")
1479 ),
1480 );
1481 }
1482 if self.include_stack.len() >= Self::MAX_INCLUDE_DEPTH {
1483 return fail(
1484 self,
1485 "E016",
1486 format!(
1487 "maximum include depth ({}) exceeded",
1488 Self::MAX_INCLUDE_DEPTH
1489 ),
1490 );
1491 }
1492 let trust = self.trust.clone();
1500 let src = match self.options.include_loader.clone() {
1504 Some(loader) => match loader(&canonical.to_string_lossy()) {
1505 Ok(s) => s,
1506 Err(e) => {
1507 return fail(
1508 self,
1509 "E017",
1510 format!("include loader refused `{}`: {e}", canonical.display()),
1511 );
1512 }
1513 },
1514 None => match std::fs::read_to_string(&canonical) {
1515 Ok(s) => s,
1516 Err(e) => {
1517 return fail(
1518 self,
1519 "E017",
1520 format!("cannot read include file `{}`: {e}", canonical.display()),
1521 );
1522 }
1523 },
1524 };
1525 if let Some(keys) = &trust {
1526 let label = canonical.to_string_lossy().into_owned();
1527 match crate::sign::read_sidecar(&canonical, &label) {
1528 Err(reason) => return fail(self, "E032", reason),
1529 Ok(sidecar) => {
1530 if let Err(reason) =
1531 crate::sign::verify_bytes(&label, src.as_bytes(), &sidecar, keys)
1532 {
1533 return fail(self, "E032", reason);
1534 }
1535 }
1536 }
1537 }
1538 let saved_file = self.current_file.clone();
1541 let saved_root = self.module.root;
1542 let saved_scopes = std::mem::take(&mut self.ident_scopes);
1543 let saved_path = self.current_path.clone();
1544 let saved_brace = self.brace_tables;
1545 let saved_indent = self.indent_tables;
1546 let saved_in_include = self.in_include;
1547 let saved_in_profile = self.in_profile;
1548 self.current_file = Some(canonical.clone());
1549 self.include_stack.push(canonical);
1550 self.in_include = true;
1551 self.in_profile = false;
1554 let mut lexer = Lexer::new(&src);
1556 let lexed = lexer.tokenize();
1557 let lex_file = self.current_file_name();
1558 let n_lex_comments = lexed.comments.len();
1562 self.comments.extend(lexed.comments);
1563 self.comment_files
1564 .extend(std::iter::repeat_with(|| lex_file.clone()).take(n_lex_comments));
1565 if let Some(alias) = inc.alias.clone() {
1568 self.current_path.push(alias);
1569 }
1570 let mut sub_errors = lexed.errors;
1571 let mut sub_repairs = lexed.repairs;
1572 for d in sub_errors.iter_mut().chain(sub_repairs.iter_mut()) {
1573 if d.file.is_none() {
1574 d.file = lex_file.clone();
1575 }
1576 }
1577 self.errors.extend(sub_errors);
1578 self.module.healing.extend(sub_repairs);
1579 let saved_tokens = std::mem::take(&mut self.tokens);
1582 let saved_pos = self.pos;
1583 self.tokens = lexed.tokens;
1584 self.pos = 0;
1585 self.parse_document();
1586 self.tokens = saved_tokens;
1587 self.pos = saved_pos;
1588 let sub_root = self.module.root;
1591 self.module.root = saved_root;
1592 self.current_file = saved_file;
1593 self.include_stack.pop();
1594 self.current_path = saved_path;
1595 self.brace_tables = saved_brace;
1596 self.indent_tables = saved_indent;
1597 self.in_include = saved_in_include;
1598 self.in_profile = saved_in_profile;
1599 self.ident_scopes = saved_scopes;
1600 sub_root
1601 }
1602
1603 fn splice_includes(
1613 &mut self,
1614 fields: &mut indexmap::IndexMap<String, NodeId>,
1615 mut pending: Vec<PendingInclude>,
1616 profile_scope: bool,
1617 ) {
1618 if pending.is_empty() {
1619 return;
1620 }
1621 if !self.options.enable_includes {
1622 for inc in &pending {
1623 self.error_at(
1624 "E015",
1625 "include statements require file-based parsing (parse_file); string input stays hermetic",
1626 &inc.tok,
1627 );
1628 }
1629 return;
1630 }
1631 if !self.options.resolve_includes {
1632 return;
1634 }
1635 let mut spliced: std::collections::HashSet<String> = std::collections::HashSet::new();
1640 let mut pi = 0usize;
1641 while pi < pending.len() {
1642 let inc = pending[pi].clone();
1643 let patches_before = self.module.patches.len();
1644 let loaded = self.load_include(&inc);
1645 let mark = inc.patch_mark.min(patches_before);
1653 let old_tail_len = patches_before - mark;
1654 let mut inserted = 0usize;
1655 if self.module.patches.len() > patches_before {
1656 let mut tail: Vec<NodeId> = self.module.patches.drain(mark..).collect();
1657 let new_part: Vec<NodeId> = tail.split_off(old_tail_len.min(tail.len()));
1658 inserted = new_part.len();
1659 self.module.patches.extend(new_part);
1660 self.module.patches.extend(tail);
1661 }
1662 for later in pending.iter_mut().skip(pi + 1) {
1663 if later.patch_mark >= mark {
1664 later.patch_mark += inserted;
1665 }
1666 }
1667 pi += 1;
1668 let Some(sub_root) = loaded else {
1669 continue;
1670 };
1671 let sub_fields = match self.module.get(sub_root) {
1672 Some(Node::Table { fields }) => fields.clone(),
1673 _ => continue,
1674 };
1675 if inc.private {
1676 match inc.alias {
1677 Some(name) => {
1678 if !self.module.bindings.contains_key(&name)
1682 || self.binding_spliced.contains(&name)
1683 {
1684 self.module.bindings.insert(name.clone(), sub_root);
1685 self.binding_spliced.insert(name);
1686 }
1687 }
1688 None => {
1689 for (k, v) in sub_fields {
1690 if !self.module.bindings.contains_key(&k)
1691 || self.binding_spliced.contains(&k)
1692 {
1693 self.module.bindings.insert(k.clone(), v);
1694 self.binding_spliced.insert(k);
1695 }
1696 }
1697 }
1698 }
1699 continue;
1700 }
1701 match inc.alias {
1702 Some(name) => {
1703 self.merge_key(fields, name, sub_root, &mut spliced);
1706 }
1707 None => {
1708 for (k, v) in sub_fields {
1709 self.merge_key(fields, k, v, &mut spliced);
1710 }
1711 }
1712 }
1713 }
1714 if profile_scope {
1715 self.reject_profile_schemas(fields);
1716 }
1717 }
1718
1719 fn reject_profile_schemas(&mut self, fields: &mut IndexMap<String, NodeId>) {
1729 let keys: Vec<(String, NodeId)> = fields.iter().map(|(k, v)| (k.clone(), *v)).collect();
1730 for (key, id) in keys {
1731 if matches!(self.module.get(id), Some(Node::SchemaTable { .. })) {
1732 self.report_profile_schema(id);
1733 fields.shift_remove(&key);
1734 continue;
1735 }
1736 let mut hits = Vec::new();
1737 self.strip_schema_tables(id, &mut hits);
1738 for hit in hits {
1739 self.report_profile_schema(hit);
1740 }
1741 }
1742 }
1743
1744 fn report_profile_schema(&mut self, schema_id: NodeId) {
1746 let span = self.module.node_spans.get(&schema_id).cloned();
1747 let file = self.module.node_files.get(&schema_id).cloned();
1748 self.errors.push(Diagnostic {
1749 severity: Severity::Error,
1750 code: "E020".into(),
1751 message: "schemas may not appear inside profiles, including through includes; move the schema to the document root".into(),
1752 path: None,
1753 span,
1754 repair: None,
1755 confidence: 1.0,
1756 file,
1757 });
1758 }
1759
1760 fn strip_schema_tables(&mut self, id: NodeId, out: &mut Vec<NodeId>) {
1766 let children: Vec<(String, NodeId)> = match self.module.get(id) {
1767 Some(Node::Table { fields }) => fields.clone().into_iter().collect(),
1768 Some(Node::SchemaTable { .. }) => {
1771 out.push(id);
1772 return;
1773 }
1774 _ => return,
1775 };
1776 for (k, v) in &children {
1777 if matches!(self.module.get(*v), Some(Node::SchemaTable { .. })) {
1778 if let Some(Node::Table { fields }) = self.module.get_mut(id) {
1779 fields.shift_remove(k);
1780 }
1781 out.push(*v);
1782 } else {
1783 self.strip_schema_tables(*v, out);
1784 }
1785 }
1786 }
1787
1788 fn merge_key(
1794 &mut self,
1795 fields: &mut indexmap::IndexMap<String, NodeId>,
1796 key: String,
1797 inc_id: NodeId,
1798 spliced: &mut std::collections::HashSet<String>,
1799 ) {
1800 match fields.get(&key) {
1801 None => {
1802 fields.insert(key.clone(), inc_id);
1803 self.mark_subtree(&key, inc_id, spliced);
1804 }
1805 Some(_) if spliced.contains(&key) => {
1806 self.merge_tables_at(fields, &key, inc_id, spliced, true);
1807 }
1808 Some(_) => {
1809 self.merge_tables_at(fields, &key, inc_id, spliced, false);
1810 }
1811 }
1812 }
1813
1814 fn merge_tables_at(
1820 &mut self,
1821 fields: &mut indexmap::IndexMap<String, NodeId>,
1822 key: &str,
1823 inc_id: NodeId,
1824 spliced: &mut std::collections::HashSet<String>,
1825 overwrite: bool,
1826 ) {
1827 if matches!(self.module.get(inc_id), Some(Node::SchemaTable { .. })) {
1832 if let Some(&local_id) = fields.get(key) {
1833 if local_id != inc_id
1834 && matches!(self.module.get(local_id), Some(Node::SchemaTable { .. }))
1835 {
1836 if self.merge_schemas_inner(local_id, inc_id, key, spliced, overwrite) {
1837 spliced.insert(key.to_string());
1838 }
1839 return;
1840 }
1841 }
1842 }
1843 let Some(Node::Table { fields: inc_fields }) = self.module.get(inc_id) else {
1844 if overwrite {
1848 if let Some(&local_id) = fields.get(key) {
1849 if !self.has_unmarked(local_id, key, spliced) {
1850 fields.insert(key.to_string(), inc_id);
1851 self.mark_subtree(key, inc_id, spliced);
1852 }
1853 }
1854 }
1855 return;
1856 };
1857 let inc_fields = inc_fields.clone();
1858 let Some(&local_id) = fields.get(key) else {
1859 return;
1860 };
1861 if local_id == inc_id {
1862 return;
1863 }
1864 let Some(Node::Table { .. }) = self.module.get(local_id) else {
1865 if overwrite && spliced.contains(key) {
1868 fields.insert(key.to_string(), inc_id);
1869 self.mark_subtree(key, inc_id, spliced);
1870 }
1871 return;
1872 };
1873 let mut merged = match self.module.get(local_id) {
1874 Some(Node::Table { fields: lf }) => lf.clone(),
1875 _ => return,
1876 };
1877 let mut changed = false;
1878 for (k, v) in &inc_fields {
1879 let child = format!("{key}.{k}");
1880 match merged.get(k) {
1881 None => {
1882 merged.insert(k.clone(), *v);
1883 self.module.add_edge(*v, local_id);
1884 self.mark_subtree(&child, *v, spliced);
1885 changed = true;
1886 }
1887 Some(&sub_local) => {
1888 if sub_local == *v {
1889 continue;
1890 }
1891 let local_kind = self.module.get(sub_local).cloned();
1892 let inc_kind = self.module.get(*v).cloned();
1893 if matches!(
1895 (&local_kind, &inc_kind),
1896 (
1897 Some(Node::SchemaTable { .. }),
1898 Some(Node::SchemaTable { .. })
1899 )
1900 ) {
1901 let sub_overwrite = overwrite && spliced.contains(&child);
1902 if self.merge_schemas_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1903 changed = true;
1904 }
1905 continue;
1906 }
1907 let both_tables = matches!(
1908 (&local_kind, &inc_kind),
1909 (Some(Node::Table { .. }), Some(Node::Table { .. }))
1910 );
1911 if !both_tables {
1912 if overwrite && spliced.contains(&child) {
1913 merged.insert(k.clone(), *v);
1914 self.module.add_edge(*v, local_id);
1915 changed = true;
1916 }
1917 continue;
1918 }
1919 let sub_overwrite = overwrite && spliced.contains(&child);
1923 if self.merge_tables_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1924 changed = true;
1925 }
1926 }
1927 }
1928 }
1929 if changed {
1930 if let Some(Node::Table { fields: slot }) = self.module.get_mut(local_id) {
1931 *slot = merged;
1932 }
1933 spliced.insert(key.to_string());
1934 }
1935 }
1936
1937 fn merge_tables_inner(
1941 &mut self,
1942 local_id: NodeId,
1943 inc_id: NodeId,
1944 path: &str,
1945 spliced: &mut std::collections::HashSet<String>,
1946 overwrite: bool,
1947 ) -> bool {
1948 let Some(Node::Table { fields: inc_fields }) = self.module.get(inc_id) else {
1949 return false;
1950 };
1951 let inc_fields = inc_fields.clone();
1952 let mut merged = match self.module.get(local_id) {
1953 Some(Node::Table { fields: lf }) => lf.clone(),
1954 _ => return false,
1955 };
1956 let mut changed = false;
1957 for (k, v) in &inc_fields {
1958 let child = format!("{path}.{k}");
1959 match merged.get(k) {
1960 None => {
1961 merged.insert(k.clone(), *v);
1962 self.module.add_edge(*v, local_id);
1963 self.mark_subtree(&child, *v, spliced);
1964 changed = true;
1965 }
1966 Some(&sub_local) => {
1967 if sub_local == *v {
1968 continue;
1969 }
1970 let local_kind = self.module.get(sub_local).cloned();
1971 let inc_kind = self.module.get(*v).cloned();
1972 if matches!(
1974 (&local_kind, &inc_kind),
1975 (
1976 Some(Node::SchemaTable { .. }),
1977 Some(Node::SchemaTable { .. })
1978 )
1979 ) {
1980 let sub_overwrite = overwrite && spliced.contains(&child);
1981 if self.merge_schemas_inner(sub_local, *v, &child, spliced, sub_overwrite) {
1982 changed = true;
1983 }
1984 continue;
1985 }
1986 let both_tables = matches!(
1987 (&local_kind, &inc_kind),
1988 (Some(Node::Table { .. }), Some(Node::Table { .. }))
1989 );
1990 if !both_tables {
1991 if overwrite && spliced.contains(&child) {
1992 merged.insert(k.clone(), *v);
1993 self.module.add_edge(*v, local_id);
1994 changed = true;
1995 }
1996 continue;
1997 }
1998 let sub_overwrite = overwrite && spliced.contains(&child);
1999 if self.merge_tables_inner(sub_local, *v, &child, spliced, sub_overwrite) {
2000 changed = true;
2001 }
2002 }
2003 }
2004 }
2005 if changed {
2006 if let Some(Node::Table { fields: slot }) = self.module.get_mut(local_id) {
2007 *slot = merged;
2008 }
2009 }
2010 changed
2011 }
2012
2013 fn merge_schemas_inner(
2019 &mut self,
2020 local_id: NodeId,
2021 inc_id: NodeId,
2022 path: &str,
2023 spliced: &mut std::collections::HashSet<String>,
2024 overwrite: bool,
2025 ) -> bool {
2026 let (local_fields, local_closed) = match self.module.get(local_id) {
2027 Some(Node::SchemaTable { fields, closed }) => (fields.clone(), *closed),
2028 _ => return false,
2029 };
2030 let (inc_fields, inc_closed) = match self.module.get(inc_id) {
2031 Some(Node::SchemaTable { fields, closed }) => (fields.clone(), *closed),
2032 _ => return false,
2033 };
2034 let mut merged = local_fields;
2035 let mut merged_closed = local_closed;
2036 let mut changed = false;
2037 if overwrite && merged_closed != inc_closed {
2038 merged_closed = inc_closed;
2039 changed = true;
2040 }
2041 for (k, v) in &inc_fields {
2042 let child = format!("{path}.{k}");
2043 match merged.get(k) {
2044 None => {
2045 merged.insert(k.clone(), *v);
2046 self.module.add_edge(*v, local_id);
2047 self.mark_subtree(&child, *v, spliced);
2048 changed = true;
2049 }
2050 Some(&sub_local) => {
2051 if sub_local == *v {
2052 continue;
2053 }
2054 let both_schema = matches!(
2055 (self.module.get(sub_local), self.module.get(*v)),
2056 (
2057 Some(Node::SchemaTable { .. }),
2058 Some(Node::SchemaTable { .. })
2059 )
2060 );
2061 if both_schema {
2062 let sub_overwrite = overwrite && spliced.contains(&child);
2063 if self.merge_schemas_inner(sub_local, *v, &child, spliced, sub_overwrite) {
2064 changed = true;
2065 }
2066 continue;
2067 }
2068 if overwrite && spliced.contains(&child) {
2069 merged.insert(k.clone(), *v);
2070 self.module.add_edge(*v, local_id);
2071 changed = true;
2072 }
2073 }
2074 }
2075 }
2076 if changed {
2077 if let Some(Node::SchemaTable {
2078 fields: slot,
2079 closed,
2080 }) = self.module.get_mut(local_id)
2081 {
2082 *slot = merged;
2083 *closed = merged_closed;
2084 }
2085 }
2086 changed
2087 }
2088
2089 fn has_unmarked(
2093 &self,
2094 id: NodeId,
2095 path: &str,
2096 spliced: &std::collections::HashSet<String>,
2097 ) -> bool {
2098 if !spliced.contains(path) {
2099 return true;
2100 }
2101 match self.module.get(id) {
2102 Some(Node::Table { fields }) => {
2103 let fields = fields.clone();
2104 fields.iter().any(|(k, v)| {
2105 let child = format!("{path}.{k}");
2106 self.has_unmarked(*v, &child, spliced)
2107 })
2108 }
2109 _ => false,
2110 }
2111 }
2112
2113 fn mark_subtree(
2118 &self,
2119 path: &str,
2120 id: NodeId,
2121 spliced: &mut std::collections::HashSet<String>,
2122 ) {
2123 spliced.insert(path.to_string());
2124 let children: Vec<(String, NodeId)> = match self.module.get(id) {
2125 Some(Node::Table { fields }) | Some(Node::SchemaTable { fields, .. }) => {
2126 fields.clone().into_iter().collect()
2127 }
2128 _ => return,
2129 };
2130 for (k, v) in &children {
2131 let child = format!("{path}.{k}");
2132 self.mark_subtree(&child, *v, spliced);
2133 }
2134 }
2135
2136 fn parse_block_header(&mut self, parent: &mut indexmap::IndexMap<String, NodeId>) {
2138 let key_tok = self.peek().clone();
2139 let key = self
2140 .expect(TokenKind::Ident)
2141 .map(|t| t.text)
2142 .unwrap_or_default();
2143 let colon = self.expect(TokenKind::Colon);
2144 self.skip_newlines();
2145 let level = if matches!(self.peek_kind(), TokenKind::Indent) {
2146 let indent = self.bump();
2147 indent_level(&indent)
2148 } else {
2149 let tok = colon.unwrap_or_else(|| self.peek().clone());
2151 self.error_at(
2152 "E008",
2153 format!("block header `{key}:` has no indented body"),
2154 &tok,
2155 );
2156 return;
2157 };
2158 if key == "schema" {
2161 if self.in_profile {
2162 self.error_at("E020", "schemas may not appear inside profiles", &key_tok);
2163 return;
2164 }
2165 self.indent_tables += 1;
2166 self.record_stmt(
2167 &key,
2168 key_tok.line,
2169 self.last_end_line(),
2170 key_tok.col.saturating_sub(1) as usize,
2171 );
2172 let node = self.parse_schema_indent(&key_tok, level);
2173 parent.insert(key, node);
2174 return;
2175 }
2176 self.indent_tables += 1;
2177 let stmt_idx = self.statements.len();
2178 self.statements.push(StmtInfo {
2179 key: key.clone(),
2180 path: self.path_str(),
2181 start_line: key_tok.line,
2182 end_line: key_tok.line,
2183 level: key_tok.col.saturating_sub(1) as usize,
2184 file: self.current_file_name(),
2185 });
2186 self.current_path.push(key.clone());
2187 self.blocks.push(BlockInfo {
2188 path: self.path_str(),
2189 level,
2190 start_line: key_tok.line,
2191 file: self.current_file_name(),
2192 });
2193 self.ident_scopes.push(Vec::new());
2194 let mut fields = indexmap::IndexMap::new();
2195 let mut pending = Vec::new();
2196 self.parse_body(&mut fields, BodyEnd::Block(level), &mut pending);
2197 self.splice_includes(&mut fields, pending, false);
2198 let idents = self.ident_scopes.pop().unwrap_or_default();
2199 let _ = self.resolve_sibling_refs(idents, &fields);
2200 let table_id = self.add_spanned(Node::Table { fields }, &key_tok);
2203 self.statements[stmt_idx].end_line = self.last_end_line();
2204 self.current_path.pop();
2205 self.extract_annotations(&key, table_id, key_tok.line);
2206 self.note_duplicate_key(&key, Some(span_of(&key_tok)), parent.contains_key(&key));
2207 parent.insert(key, table_id);
2208 }
2209
2210 fn parse_table_block(&mut self, parent: &mut indexmap::IndexMap<String, NodeId>) {
2212 let key_tok = self.peek().clone();
2213 let key = self
2214 .expect(TokenKind::Ident)
2215 .map(|t| t.text)
2216 .unwrap_or_default();
2217 if key == "schema" {
2220 if self.in_profile {
2221 self.error_at("E020", "schemas may not appear inside profiles", &key_tok);
2222 return;
2223 }
2224 self.brace_tables += 1;
2225 self.record_stmt(
2226 &key,
2227 key_tok.line,
2228 self.last_end_line(),
2229 key_tok.col.saturating_sub(1) as usize,
2230 );
2231 let node = self.parse_schema_brace(&key_tok);
2232 parent.insert(key, node);
2233 return;
2234 }
2235 self.expect(TokenKind::LBrace);
2236 self.brace_tables += 1;
2237 let stmt_idx = self.statements.len();
2238 self.statements.push(StmtInfo {
2239 key: key.clone(),
2240 path: self.path_str(),
2241 start_line: key_tok.line,
2242 end_line: key_tok.line,
2243 level: key_tok.col.saturating_sub(1) as usize,
2244 file: self.current_file_name(),
2245 });
2246 self.current_path.push(key.clone());
2247 self.blocks.push(BlockInfo {
2248 path: self.path_str(),
2249 level: key_tok.col.saturating_sub(1) as usize,
2250 start_line: key_tok.line,
2251 file: self.current_file_name(),
2252 });
2253 self.ident_scopes.push(Vec::new());
2254 let mut fields = indexmap::IndexMap::new();
2255 let mut pending = Vec::new();
2256 self.parse_body(&mut fields, BodyEnd::Brace, &mut pending);
2257 self.splice_includes(&mut fields, pending, false);
2258 let idents = self.ident_scopes.pop().unwrap_or_default();
2259 let _ = self.resolve_sibling_refs(idents, &fields);
2260 let table_id = self.add_spanned(Node::Table { fields }, &key_tok);
2262 self.statements[stmt_idx].end_line = self.last_end_line();
2263 self.current_path.pop();
2264 self.extract_annotations(&key, table_id, key_tok.line);
2265 self.note_duplicate_key(&key, Some(span_of(&key_tok)), parent.contains_key(&key));
2266 parent.insert(key, table_id);
2267 }
2268
2269 fn error_at(&mut self, code: &str, message: impl Into<String>, tok: &Token) {
2271 self.errors.push(Diagnostic {
2272 severity: Severity::Error,
2273 code: code.into(),
2274 message: message.into(),
2275 path: None,
2276 span: Some(span_of(tok)),
2277 repair: None,
2278 confidence: 1.0,
2279 file: self.current_file_name(),
2280 });
2281 }
2282
2283 fn error_at_span(&mut self, code: &str, message: impl Into<String>, span: &Span) {
2286 self.errors.push(Diagnostic {
2287 severity: Severity::Error,
2288 code: code.into(),
2289 message: message.into(),
2290 path: None,
2291 span: Some(span.clone()),
2292 repair: None,
2293 confidence: 1.0,
2294 file: self.current_file_name(),
2295 });
2296 }
2297
2298 fn note_duplicate_key(&mut self, key: &str, span: Option<Span>, existed: bool) {
2306 if existed {
2307 self.module.healing.push(Diagnostic {
2308 severity: Severity::Warning,
2309 code: "W011".into(),
2310 message: format!("duplicate key '{key}'; last wins"),
2311 path: None,
2312 span,
2313 repair: None,
2314 confidence: 1.0,
2315 file: self.current_file_name(),
2316 });
2317 }
2318 }
2319
2320 fn extract_schemas(&mut self) {
2329 if self.in_include {
2330 return;
2331 }
2332 let Some(root) = self.module.root else {
2333 return;
2334 };
2335 if let Some(schema_id) = self.extract_table_schema(root) {
2336 self.module.schema = Some(schema_id);
2337 }
2338 }
2339
2340 fn extract_table_schema(&mut self, table_id: NodeId) -> Option<NodeId> {
2344 let fields = match self.module.get(table_id) {
2346 Some(Node::Table { fields }) => fields.clone(),
2347 _ => return None,
2348 };
2349 let mut schema_id = match fields.get("schema") {
2351 Some(&id) => match self.module.get(id) {
2352 Some(Node::SchemaTable { .. }) => {
2353 if let Some(Node::Table { fields }) = self.module.get_mut(table_id) {
2355 fields.shift_remove("schema");
2356 }
2357 Some(id)
2358 }
2359 _ => None,
2360 },
2361 None => None,
2362 };
2363 let mut nested: IndexMap<String, NodeId> = IndexMap::new();
2365 for (k, v) in &fields {
2366 if *k == "schema" {
2367 continue;
2368 }
2369 if matches!(self.module.get(*v), Some(Node::Table { .. })) {
2370 if let Some(sub) = self.extract_table_schema(*v) {
2371 nested.insert(k.clone(), sub);
2372 }
2373 }
2374 }
2375 if nested.is_empty() {
2376 return schema_id;
2377 }
2378 let parent = match schema_id {
2381 Some(id) => id,
2382 None => {
2383 let id = self.module.add_node(Node::SchemaTable {
2384 fields: IndexMap::new(),
2385 closed: false,
2386 });
2387 schema_id = Some(id);
2388 id
2389 }
2390 };
2391 if let Some(Node::SchemaTable { fields, .. }) = self.module.get_mut(parent) {
2392 for (k, v) in nested {
2393 fields.insert(k, v);
2394 }
2395 }
2396 schema_id
2397 }
2398
2399 fn extract_depends_on(&mut self) {
2408 if self.in_include {
2409 return;
2410 }
2411 let Some(root) = self.module.root else {
2412 return;
2413 };
2414 self.extract_table_depends_on(root);
2415 }
2416
2417 fn extract_table_depends_on(&mut self, table_id: NodeId) {
2419 let fields = match self.module.get(table_id) {
2420 Some(Node::Table { fields }) => fields.clone(),
2421 _ => return,
2422 };
2423 if let Some(&id) = fields.get("depends_on") {
2424 if let Some(Node::Array { elements }) = self.module.get(id).cloned() {
2425 let mut paths = Vec::new();
2426 let mut bad = false;
2427 for element in &elements {
2428 match self.module.get(*element) {
2429 Some(Node::Literal(Value::String(path))) => paths.push(path.clone()),
2430 _ => {
2431 bad = true;
2432 if let Some(span) = self.module.node_spans.get(element).cloned() {
2433 self.error_at_span(
2434 "E022",
2435 "depends_on entries must be string literals",
2436 &span,
2437 );
2438 }
2439 }
2440 }
2441 }
2442 if !bad {
2443 if let Some(Node::Table { fields }) = self.module.get_mut(table_id) {
2444 fields.shift_remove("depends_on");
2445 }
2446 let span =
2447 self.module
2448 .node_spans
2449 .get(&id)
2450 .cloned()
2451 .unwrap_or(crate::diag::Span {
2452 line: 1,
2453 column: 1,
2454 end_line: 1,
2455 end_column: 1,
2456 });
2457 self.module
2458 .depends_on
2459 .insert(table_id, DependsOn { paths, span });
2460 }
2461 }
2462 }
2463 let fields = match self.module.get(table_id) {
2467 Some(Node::Table { fields }) => fields.clone(),
2468 _ => return,
2469 };
2470 for (k, v) in &fields {
2471 if *k == "depends_on" {
2472 continue;
2473 }
2474 if matches!(self.module.get(*v), Some(Node::Table { .. })) {
2475 self.extract_table_depends_on(*v);
2476 }
2477 }
2478 }
2479
2480 fn current_file_name(&self) -> Option<String> {
2482 self.current_file
2483 .as_ref()
2484 .map(|p| p.to_string_lossy().into_owned())
2485 }
2486
2487 fn add_spanned(&mut self, node: Node, tok: &Token) -> NodeId {
2490 let id = self.module.add_node(node);
2491 self.module.record_span(id, span_of(tok));
2492 if let Some(file) = self.current_file_name() {
2493 self.module.record_file(id, file);
2494 }
2495 id
2496 }
2497
2498 fn heal(
2510 &mut self,
2511 code: &str,
2512 message: String,
2513 tok: &Token,
2514 repair: Repair,
2515 confidence: f64,
2516 ) -> bool {
2517 let applies = confidence >= thresholds::HIGH
2518 || (self.options.resilient && confidence >= thresholds::MEDIUM);
2519 if !applies {
2520 return false;
2521 }
2522 self.module.healing.push(Diagnostic {
2523 severity: if confidence >= thresholds::AUTO_APPLY {
2524 Severity::Info
2525 } else {
2526 Severity::Warning
2527 },
2528 code: code.into(),
2529 message,
2530 path: None,
2531 span: Some(span_of(tok)),
2532 repair: Some(repair),
2533 confidence,
2534 file: self.current_file_name(),
2535 });
2536 true
2537 }
2538
2539 fn expect_closing_healing(&mut self, expected: TokenKind, heal_code: &str) -> bool {
2543 let next = self.peek().clone();
2544 if next.kind == expected {
2545 self.bump();
2546 return true;
2547 }
2548 let healable = matches!(
2549 (&expected, &next.kind),
2550 (TokenKind::RBracket, TokenKind::RBrace) | (TokenKind::RBrace, TokenKind::RBracket)
2551 );
2552 if healable {
2553 let applied = self.heal(
2554 heal_code,
2555 format!(
2556 "mismatched closing delimiter; replaced {:?} with {:?}",
2557 next.kind, expected
2558 ),
2559 &next,
2560 Repair {
2561 original: format!("{:?}", next.kind),
2562 suggested: format!("{:?}", expected),
2563 kind: RepairKind::Other("bracket_mismatch".into()),
2564 safe: false,
2565 },
2566 thresholds::MEDIUM + 0.12, );
2568 if applied {
2569 self.bump();
2570 return true;
2571 }
2572 }
2573 self.error(
2576 "E001",
2577 format!("expected {:?}, found {:?}", expected, next.kind),
2578 );
2579 false
2580 }
2581
2582 fn parse_document(&mut self) {
2587 let mut root_fields = indexmap::IndexMap::new();
2588 self.ident_scopes.push(Vec::new());
2589
2590 let mut pending = Vec::new();
2591 self.parse_body(&mut root_fields, BodyEnd::Document, &mut pending);
2592 self.splice_includes(&mut root_fields, pending, false);
2593
2594 let root_idents = self.ident_scopes.pop().unwrap_or_default();
2595 let _ = self.resolve_sibling_refs(root_idents, &root_fields);
2596
2597 if !self.in_include {
2605 let profiles = std::mem::take(&mut self.pending_profiles);
2606 for (idents, prof_fields) in profiles {
2607 let leftover = self.resolve_sibling_refs(idents, &prof_fields);
2608 let unowned: Vec<(String, NodeId)> = leftover
2613 .into_iter()
2614 .map(|(_, id)| (String::new(), id))
2615 .collect();
2616 let _ = self.resolve_sibling_refs(unowned, &root_fields);
2617 }
2618 }
2619
2620 let root_id = self.module.add_node(Node::Table {
2621 fields: root_fields,
2622 });
2623 self.module.root = Some(root_id);
2624 }
2625
2626 fn parse_let(&mut self) {
2631 let let_tok = self.peek().clone();
2632 self.bump(); let name_tok = match self.expect(TokenKind::Ident) {
2634 Some(t) => t,
2635 None => return,
2636 };
2637 if self.expect(TokenKind::Assign).is_none() {
2638 return;
2639 }
2640 let expr = self.parse_expression();
2641 self.record_stmt(
2642 &name_tok.text,
2643 let_tok.line,
2644 self.last_end_line(),
2645 let_tok.col.saturating_sub(1) as usize,
2646 );
2647 self.extract_annotations(&name_tok.text, expr, let_tok.line);
2648 if self.in_include {
2649 self.module.bindings.entry(name_tok.text).or_insert(expr);
2652 } else {
2653 self.note_duplicate_key(
2654 &name_tok.text,
2655 Some(span_of(&name_tok)),
2656 self.module.bindings.contains_key(&name_tok.text),
2657 );
2658 self.module.bindings.insert(name_tok.text.clone(), expr);
2659 self.binding_spliced.remove(&name_tok.text);
2661 }
2662 }
2663
2664 fn parse_table_header(&mut self, parent: &mut indexmap::IndexMap<String, NodeId>) {
2669 let open = self.peek().clone();
2670 let header_level = open.col.saturating_sub(1) as usize;
2672 self.bump(); let name_tok = match self.expect(TokenKind::Ident) {
2674 Some(t) => t,
2675 None => return,
2676 };
2677 let mut segments = vec![name_tok.text.clone()];
2681 while matches!(self.peek_kind(), TokenKind::Dot) {
2682 self.bump(); match self.expect(TokenKind::Ident) {
2684 Some(t) => segments.push(t.text.clone()),
2685 None => return,
2686 }
2687 }
2688 let close = match self.expect(TokenKind::RBracket) {
2689 Some(t) => t,
2690 None => return,
2691 };
2692 self.brace_tables += 1;
2693 let mut scope: Option<NodeId> = None;
2699 for seg in &segments[..segments.len() - 1] {
2700 let found = match scope {
2701 None => parent.get(seg).copied(),
2702 Some(id) => match self.module.get(id) {
2703 Some(Node::Table { fields }) => fields.get(seg).copied(),
2704 _ => None,
2705 },
2706 };
2707 let next = match found {
2708 Some(id) => match self.module.get(id) {
2709 Some(Node::Table { .. }) => id,
2710 _ => {
2711 self.error(
2712 "E001",
2713 format!(
2714 "table path '{}' conflicts with a non-table value at '{}'",
2715 segments.join("."),
2716 seg
2717 ),
2718 );
2719 return;
2720 }
2721 },
2722 None => {
2723 let id = self.module.add_node(Node::Table {
2724 fields: indexmap::IndexMap::new(),
2725 });
2726 match scope {
2727 None => {
2728 parent.insert(seg.clone(), id);
2729 }
2730 Some(pid) => match self.module.get_mut(pid) {
2731 Some(Node::Table { fields }) => {
2732 fields.insert(seg.clone(), id);
2733 }
2734 _ => {
2735 self.error(
2736 "E001",
2737 format!(
2738 "table path '{}' conflicts with a non-table value",
2739 segments.join(".")
2740 ),
2741 );
2742 return;
2743 }
2744 },
2745 }
2746 id
2747 }
2748 };
2749 scope = Some(next);
2750 }
2751 let name = segments.join(".");
2752 let leaf = segments.last().cloned().unwrap_or_default();
2753 let stmt_idx = self.statements.len();
2754 self.statements.push(StmtInfo {
2755 key: name.clone(),
2756 path: self.path_str(),
2757 start_line: open.line,
2758 end_line: open.line,
2759 level: header_level,
2760 file: self.current_file_name(),
2761 });
2762 for seg in &segments {
2763 self.current_path.push(seg.clone());
2764 }
2765 self.blocks.push(BlockInfo {
2766 path: self.path_str(),
2767 level: header_level,
2768 start_line: open.line,
2769 file: self.current_file_name(),
2770 });
2771
2772 self.ident_scopes.push(Vec::new());
2773 let mut fields = indexmap::IndexMap::new();
2774 let mut pending = Vec::new();
2775 self.parse_body(&mut fields, BodyEnd::HeaderBody(header_level), &mut pending);
2776 self.splice_includes(&mut fields, pending, false);
2777 let idents = self.ident_scopes.pop().unwrap_or_default();
2778 let _ = self.resolve_sibling_refs(idents, &fields);
2779 let table_id = self.module.add_node(Node::Table { fields });
2783 self.module.record_span(
2784 table_id,
2785 Span {
2786 line: open.line,
2787 column: open.col,
2788 end_line: close.end_line,
2789 end_column: close.end_col,
2790 },
2791 );
2792 if let Some(file) = self.current_file_name() {
2793 self.module.record_file(table_id, file);
2794 }
2795 self.statements[stmt_idx].end_line = self.last_end_line();
2796 for _ in &segments {
2797 self.current_path.pop();
2798 }
2799 self.extract_annotations(&name, table_id, open.line);
2800 match scope {
2801 None => {
2802 self.note_duplicate_key(&leaf, Some(span_of(&open)), parent.contains_key(&leaf));
2803 parent.insert(leaf, table_id);
2804 }
2805 Some(pid) => {
2806 let existed = matches!(
2807 self.module.get(pid),
2808 Some(Node::Table { fields }) if fields.contains_key(&leaf)
2809 );
2810 self.note_duplicate_key(&leaf, Some(span_of(&open)), existed);
2811 match self.module.get_mut(pid) {
2812 Some(Node::Table { fields }) => {
2813 fields.insert(leaf, table_id);
2814 }
2815 _ => {
2816 self.error(
2817 "E001",
2818 format!(
2819 "table path '{}' conflicts with a non-table value",
2820 segments.join(".")
2821 ),
2822 );
2823 }
2824 }
2825 }
2826 }
2827 }
2828
2829 fn check_depends_on_statement(&mut self, key: &str, id: NodeId) -> bool {
2839 if key != "depends_on" || !matches!(self.module.get(id), Some(Node::Array { .. })) {
2840 return true;
2841 }
2842 if self.in_profile {
2843 if let Some(span) = self.module.node_spans.get(&id).cloned() {
2844 self.error_at_span("E022", "depends_on is not allowed inside profiles", &span);
2845 }
2846 return false;
2847 }
2848 true
2849 }
2850
2851 fn parse_key_value(&mut self) -> Option<(String, NodeId)> {
2852 let (key, id) = self.parse_key_value_sep(TokenKind::Assign)?;
2853 if key == "depends_on" && matches!(self.module.get(id), Some(Node::Array { .. })) {
2857 if let Some(span) = self.module.node_spans.get(&id).cloned() {
2858 self.error_at_span(
2859 "E022",
2860 "depends_on is only allowed as a direct table member",
2861 &span,
2862 );
2863 }
2864 }
2865 Some((key, id))
2866 }
2867
2868 fn parse_key_value_sep(&mut self, sep: TokenKind) -> Option<(String, NodeId)> {
2870 let key_tok = self.expect(TokenKind::Ident)?;
2871 let key = key_tok.text.clone();
2872 self.expect(sep)?;
2873 if key == "schema" && matches!(self.peek_kind(), TokenKind::LBrace) {
2876 if self.array_depth > 0 {
2877 self.error_at(
2878 "E020",
2879 "schemas may not appear inside arrays; describe element types with Array<T>",
2880 &key_tok,
2881 );
2882 return None;
2883 }
2884 if self.in_profile {
2885 self.error_at("E020", "schemas may not appear inside profiles", &key_tok);
2886 return None;
2887 }
2888 let node = self.parse_schema_inline(&key_tok);
2889 self.record_stmt(
2890 &key,
2891 key_tok.line,
2892 self.last_end_line(),
2893 key_tok.col.saturating_sub(1) as usize,
2894 );
2895 return Some((key, node));
2896 }
2897 let scope_start = self.ident_scopes.last().map_or(0, |s| s.len());
2900 let value = self.parse_expression();
2901 if let Some(scope) = self.ident_scopes.last_mut() {
2902 for entry in scope.iter_mut().skip(scope_start) {
2903 entry.0 = key.clone();
2904 }
2905 }
2906 Some((key, value))
2907 }
2908
2909 fn resolve_sibling_refs(
2915 &mut self,
2916 idents: Vec<(String, NodeId)>,
2917 fields: &indexmap::IndexMap<String, NodeId>,
2918 ) -> Vec<(String, NodeId)> {
2919 let mut leftover = Vec::new();
2920 for (owner_key, id) in idents {
2921 let name = match self.module.get(id) {
2922 Some(Node::Ident(n)) => n.clone(),
2923 _ => continue,
2924 };
2925 if name == owner_key {
2928 leftover.push((owner_key, id));
2929 continue;
2930 }
2931 if let Some(&target) = fields.get(&name) {
2932 if let Some(slot) = self.module.get_mut(id) {
2933 *slot = Node::Ref(target);
2934 }
2935 self.module.add_edge(target, id);
2936 } else {
2937 leftover.push((owner_key, id));
2938 }
2939 }
2940 leftover
2941 }
2942
2943 fn extract_annotations(&mut self, key: &str, node: NodeId, stmt_line: u32) {
2955 if matches!(self.module.get(node), Some(Node::SchemaTable { .. })) {
2956 return;
2957 }
2958 if self.in_profile {
2962 return;
2963 }
2964 let scope = self.path_str();
2965 let full_path = if scope.is_empty() {
2966 key.to_string()
2967 } else {
2968 format!("{scope}.{key}")
2969 };
2970 let here = self.current_file_name();
2975 let mut line = stmt_line.saturating_sub(1);
2976 let mut block: Vec<(String, Span)> = Vec::new();
2977 while line >= 1 {
2978 let comment = self
2979 .comments
2980 .iter()
2981 .zip(self.comment_files.iter())
2982 .find(|((_, s), f)| s.line == line && *f == &here)
2983 .map(|((text, span), _)| (text.clone(), span.clone()));
2984 let Some((text, span)) = comment else {
2985 break;
2986 };
2987 if self
2988 .statements
2989 .iter()
2990 .any(|st| st.end_line == line && st.file == here)
2991 {
2992 break;
2993 }
2994 block.push((text, span));
2995 if line == 1 {
2996 break;
2997 }
2998 line -= 1;
2999 }
3000 for (text, span) in block.into_iter().rev() {
3002 if let Some(kind) = self.parse_annotation_line(&text, &span, node) {
3003 self.module
3004 .annotations
3005 .entry(node)
3006 .or_default()
3007 .push(Annotation {
3008 kind,
3009 path: full_path.clone(),
3010 span,
3011 });
3012 }
3013 }
3014 }
3015
3016 fn parse_annotation_line(
3020 &mut self,
3021 text: &str,
3022 span: &Span,
3023 target: NodeId,
3024 ) -> Option<AnnotationKind> {
3025 let body = text.strip_prefix('#').unwrap_or(text).trim_start();
3026 let rest = body.strip_prefix('@')?;
3027 let kind_len = rest
3028 .chars()
3029 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
3030 .count();
3031 let (kind, after) = rest.split_at(kind_len);
3032 match kind {
3033 "intent" | "constraint" | "ai_immutable" => {}
3034 _ => return None,
3035 }
3036 if kind == "ai_immutable" && after.trim_start().is_empty() {
3041 return Some(AnnotationKind::AiImmutable(true));
3042 }
3043 let payload = match after.trim_start().strip_prefix(':') {
3044 Some(p) => p.trim_start(),
3045 None => {
3046 self.error_at_span(
3047 "E021",
3048 format!("invalid @{kind} annotation: expected ':' after the kind"),
3049 span,
3050 );
3051 return None;
3052 }
3053 };
3054 match kind {
3055 "intent" => {
3060 let wants_literal = payload.starts_with('"') || payload.starts_with('\'');
3061 match parse_annotation_string(payload) {
3062 Ok(documented) => Some(AnnotationKind::Intent(documented)),
3063 Err(_) if !wants_literal && !payload.is_empty() => {
3064 Some(AnnotationKind::Intent(payload.to_string()))
3065 }
3066 Err(detail) => {
3067 self.error_at_span(
3068 "E021",
3069 format!("invalid @intent annotation: {detail}"),
3070 span,
3071 );
3072 None
3073 }
3074 }
3075 }
3076 "ai_immutable" => match payload {
3077 "true" => Some(AnnotationKind::AiImmutable(true)),
3078 "false" => Some(AnnotationKind::AiImmutable(false)),
3079 _ => {
3080 self.error_at_span(
3081 "E021",
3082 "invalid @ai_immutable annotation: expected 'true' or 'false'",
3083 span,
3084 );
3085 None
3086 }
3087 },
3088 "constraint" => {
3089 if payload.is_empty() {
3090 self.error_at_span(
3091 "E021",
3092 "invalid @constraint annotation: empty predicate",
3093 span,
3094 );
3095 return None;
3096 }
3097 let payload_col =
3100 span.column + (text.chars().count() - payload.chars().count()) as u32;
3101 self.parse_constraint(payload, span, payload_col, target)
3102 .map(|expr| AnnotationKind::Constraint {
3103 source: payload.to_string(),
3104 expr,
3105 })
3106 }
3107 _ => None,
3108 }
3109 }
3110
3111 fn parse_constraint(
3119 &mut self,
3120 payload: &str,
3121 comment: &Span,
3122 payload_col: u32,
3123 target: NodeId,
3124 ) -> Option<NodeId> {
3125 let mut lexer = Lexer::new(payload);
3126 let lexed = lexer.tokenize();
3127 let module = std::mem::replace(&mut self.module, Module::new(""));
3128 let inherited_len = self.ident_scopes.last().map_or(0, |s| s.len());
3129 let mut sub = Parser {
3130 tokens: lexed.tokens,
3131 pos: 0,
3132 module,
3133 errors: Vec::new(),
3134 ident_scopes: self.ident_scopes.clone(),
3135 depth: self.depth,
3136 options: self.options.clone(),
3137 block_depth: 0,
3138 schema_depth: self.schema_depth,
3139 array_depth: self.array_depth,
3140 comments: Vec::new(),
3141 comment_files: Vec::new(),
3142 statements: Vec::new(),
3143 blocks: Vec::new(),
3144 current_path: self.current_path.clone(),
3145 brace_tables: 0,
3146 indent_tables: 0,
3147 current_file: self.current_file.clone(),
3148 include_stack: Vec::new(),
3149 in_include: self.in_include,
3150 in_profile: self.in_profile,
3151 binding_spliced: self.binding_spliced.clone(),
3152 pending_profiles: Vec::new(),
3153 trust: self.trust.clone(),
3154 };
3155 let node_base = sub.module.arena.len() as u32;
3156 let id = sub.parse_expression();
3157 while matches!(sub.peek_kind(), TokenKind::Newline) {
3158 sub.bump();
3159 }
3160 if !matches!(sub.peek_kind(), TokenKind::Eof) {
3161 let leftover = sub.peek().clone();
3162 sub.error_at(
3163 "E021",
3164 format!(
3165 "invalid @constraint annotation: unexpected {:?} after the predicate",
3166 leftover.kind
3167 ),
3168 &leftover,
3169 );
3170 }
3171 let mut rewritten = std::collections::HashSet::new();
3174 for i in node_base..sub.module.arena.len() as u32 {
3175 let nid = NodeId(i);
3176 if matches!(sub.module.get(nid), Some(Node::Ident(name)) if name == "value") {
3177 if let Some(slot) = sub.module.get_mut(nid) {
3178 *slot = Node::Ref(target);
3179 }
3180 sub.module.add_edge(target, nid);
3181 rewritten.insert(nid);
3182 }
3183 }
3184 let new_idents: Vec<(String, NodeId)> = sub
3185 .ident_scopes
3186 .last()
3187 .map(|s| {
3188 s[inherited_len.min(s.len())..]
3189 .iter()
3190 .filter(|(_, nid)| !rewritten.contains(nid))
3191 .cloned()
3192 .collect()
3193 })
3194 .unwrap_or_default();
3195 let lift = |line_1: u32, col_1: u32| -> (u32, u32) {
3199 let line = comment.line + line_1.saturating_sub(1);
3200 let col = if line_1 <= 1 {
3201 payload_col + col_1.saturating_sub(1)
3202 } else {
3203 col_1
3204 };
3205 (line, col)
3206 };
3207 for (node, span) in sub.module.node_spans.iter_mut() {
3208 if node.0 >= node_base {
3209 let (line, col) = lift(span.line, span.column);
3210 span.line = line;
3211 span.column = col;
3212 let (line, col) = lift(span.end_line, span.end_column);
3213 span.end_line = line;
3214 span.end_column = col;
3215 }
3216 }
3217 let mut sub_errors = std::mem::take(&mut sub.errors);
3218 let mut sub_lex_errors = lexed.errors;
3219 for d in sub_errors.iter_mut().chain(sub_lex_errors.iter_mut()) {
3220 if let Some(s) = &mut d.span {
3221 let (line, col) = lift(s.line, s.column);
3222 s.line = line;
3223 s.column = col;
3224 let (line, col) = lift(s.end_line, s.end_column);
3225 s.end_line = line;
3226 s.end_column = col;
3227 }
3228 if d.file.is_none() {
3229 d.file = self.current_file_name();
3230 }
3231 }
3232 self.errors.extend(sub_lex_errors);
3233 self.errors.extend(sub_errors);
3234 self.module = std::mem::replace(&mut sub.module, Module::new(""));
3235 if let Some(scope) = self.ident_scopes.last_mut() {
3236 scope.extend(new_idents);
3237 }
3238 Some(id)
3239 }
3240
3241 fn parse_expression(&mut self) -> NodeId {
3246 let guard_tok = self.peek().clone();
3247 self.depth += 1;
3248 if self.depth > MAX_EXPR_DEPTH {
3249 self.error(
3250 "E004",
3251 format!("maximum expression nesting depth ({MAX_EXPR_DEPTH}) exceeded"),
3252 );
3253 self.depth -= 1;
3254 self.bump(); return self.add_spanned(Node::Literal(Value::Null), &guard_tok);
3256 }
3257 let node = self.parse_or();
3258 self.depth -= 1;
3259 node
3260 }
3261
3262 fn parse_or(&mut self) -> NodeId {
3263 let mut left = self.parse_and();
3264 while matches!(self.peek_kind(), TokenKind::OrOr) {
3265 let op_tok = self.bump();
3266 let right = self.parse_and();
3267 left = self.add_spanned(
3268 Node::Binary {
3269 op: BinOp::Or,
3270 lhs: left,
3271 rhs: right,
3272 },
3273 &op_tok,
3274 );
3275 }
3276 left
3277 }
3278
3279 fn parse_and(&mut self) -> NodeId {
3280 let mut left = self.parse_equality();
3281 while matches!(self.peek_kind(), TokenKind::AndAnd) {
3282 let op_tok = self.bump();
3283 let right = self.parse_equality();
3284 left = self.add_spanned(
3285 Node::Binary {
3286 op: BinOp::And,
3287 lhs: left,
3288 rhs: right,
3289 },
3290 &op_tok,
3291 );
3292 }
3293 left
3294 }
3295
3296 fn parse_equality(&mut self) -> NodeId {
3297 let mut left = self.parse_comparison();
3298 loop {
3299 let op = match self.peek_kind() {
3300 TokenKind::EqEq => BinOp::Eq,
3301 TokenKind::NotEq => BinOp::Ne,
3302 _ => break,
3303 };
3304 let op_tok = self.bump();
3305 let right = self.parse_comparison();
3306 left = self.add_spanned(
3307 Node::Binary {
3308 op,
3309 lhs: left,
3310 rhs: right,
3311 },
3312 &op_tok,
3313 );
3314 }
3315 left
3316 }
3317
3318 fn parse_comparison(&mut self) -> NodeId {
3319 let mut left = self.parse_term();
3320 loop {
3321 let op = match self.peek_kind() {
3322 TokenKind::Lt => BinOp::Lt,
3323 TokenKind::LtEq => BinOp::Le,
3324 TokenKind::Gt => BinOp::Gt,
3325 TokenKind::GtEq => BinOp::Ge,
3326 _ => break,
3327 };
3328 let op_tok = self.bump();
3329 let right = self.parse_term();
3330 left = self.add_spanned(
3331 Node::Binary {
3332 op,
3333 lhs: left,
3334 rhs: right,
3335 },
3336 &op_tok,
3337 );
3338 }
3339 left
3340 }
3341
3342 fn parse_term(&mut self) -> NodeId {
3343 let mut left = self.parse_factor();
3344 loop {
3345 let op = match self.peek_kind() {
3346 TokenKind::Plus => BinOp::Add,
3347 TokenKind::Minus => BinOp::Sub,
3348 _ => break,
3349 };
3350 let op_tok = self.bump();
3351 let right = self.parse_factor();
3352 left = self.add_spanned(
3353 Node::Binary {
3354 op,
3355 lhs: left,
3356 rhs: right,
3357 },
3358 &op_tok,
3359 );
3360 }
3361 left
3362 }
3363
3364 fn parse_factor(&mut self) -> NodeId {
3365 let mut left = self.parse_unary();
3366 loop {
3367 let op = match self.peek_kind() {
3368 TokenKind::Star => BinOp::Mul,
3369 TokenKind::Slash => BinOp::Div,
3370 _ => break,
3371 };
3372 let op_tok = self.bump();
3373 let right = self.parse_unary();
3374 left = self.add_spanned(
3375 Node::Binary {
3376 op,
3377 lhs: left,
3378 rhs: right,
3379 },
3380 &op_tok,
3381 );
3382 }
3383 left
3384 }
3385
3386 fn parse_unary(&mut self) -> NodeId {
3387 match self.peek_kind() {
3388 TokenKind::Minus | TokenKind::Bang => {
3389 let op_tok = self.peek().clone();
3390 self.depth += 1;
3393 if self.depth > MAX_EXPR_DEPTH {
3394 self.error(
3395 "E004",
3396 format!("maximum expression nesting depth ({MAX_EXPR_DEPTH}) exceeded"),
3397 );
3398 self.depth -= 1;
3399 self.bump(); return self.add_spanned(Node::Literal(Value::Null), &op_tok);
3401 }
3402 let op = if matches!(self.peek_kind(), TokenKind::Minus) {
3403 UnOp::Neg
3404 } else {
3405 UnOp::Not
3406 };
3407 self.bump();
3408 let expr = self.parse_unary();
3409 self.depth -= 1;
3410 if op == UnOp::Neg {
3416 if let Some(folded) = self.fold_affine_negation(expr) {
3417 return folded;
3418 }
3419 }
3420 self.add_spanned(Node::Unary { op, expr }, &op_tok)
3421 }
3422 _ => self.parse_primary(),
3423 }
3424 }
3425
3426 fn fold_affine_negation(&mut self, expr: NodeId) -> Option<NodeId> {
3434 let value_id = match self.module.get(expr) {
3435 Some(Node::Quantity {
3436 value,
3437 unit: Unit::Celsius | Unit::Fahrenheit,
3438 }) => *value,
3439 _ => return None,
3440 };
3441 match self.module.get_mut(value_id) {
3442 Some(Node::Literal(Value::Int(n))) => {
3443 *n = n.saturating_neg();
3444 }
3445 Some(Node::Literal(Value::Float(f))) => {
3446 *f = -*f;
3447 }
3448 _ => return None,
3449 }
3450 Some(expr)
3451 }
3452
3453 fn parse_primary(&mut self) -> NodeId {
3458 let tok = self.peek().clone();
3459 match &tok.kind {
3460 TokenKind::String(src_pos) => {
3461 self.bump();
3462 let linemap = linemap_from_charmap(&tok.text, tok.line, src_pos);
3463 let marks = Self::sentinel_marks(&tok.text, &tok.kind);
3464 self.parse_string_literal(&tok.text, &tok, Some(&linemap), &marks)
3465 }
3466
3467 TokenKind::SingleString => {
3468 self.bump();
3471 let text = tok.text.clone();
3472 self.add_spanned(Node::Literal(Value::String(text)), &tok)
3473 }
3474
3475 TokenKind::MlString(src_pos) => {
3476 self.bump();
3482 let (stripped, linemap, marks) =
3483 strip_common_indent_mapped(&tok.text, tok.line, tok.col, 3, Some(src_pos));
3484 self.parse_string_literal(&stripped, &tok, Some(&linemap), &marks)
3485 }
3486
3487 TokenKind::MlSingleString => {
3488 self.bump();
3491 let stripped = strip_common_indent(&tok.text);
3492 self.add_spanned(Node::Literal(Value::String(stripped)), &tok)
3493 }
3494
3495 TokenKind::At => {
3496 self.bump();
3497 if matches!(self.peek_kind(), TokenKind::Ident) {
3499 let tag_tok = self.peek().clone();
3500 let tag = tag_tok.text.clone();
3501 if (tag == "patch" || tag == "remove" || tag == "rename")
3502 && matches!(self.peek_kind_at(1), TokenKind::LParen)
3503 {
3504 self.error_at(
3505 "E019",
3506 "patch statements may only appear at document root, never as values",
3507 &tag_tok,
3508 );
3509 return self.add_spanned(Node::Literal(Value::Null), &tok);
3510 }
3511 }
3512 self.parse_semantic_block(&tok)
3513 }
3514
3515 TokenKind::Integer => {
3516 self.bump();
3517 let n: i64 = match tok.text.parse() {
3523 Ok(n) => n,
3524 Err(_) => {
3525 self.error_at(
3526 "E023",
3527 format!("integer literal out of i64 range: `{}`", tok.text),
3528 &tok,
3529 );
3530 i64::MAX
3531 }
3532 };
3533 let mut node = self.add_spanned(Node::Literal(Value::Int(n)), &tok);
3534 if let TokenKind::Unit(_) = self.peek_kind() {
3536 let unit_tok = self.bump();
3537 if let Some(unit) = Unit::from_suffix(&unit_tok.text) {
3538 node = self.add_spanned(Node::Quantity { value: node, unit }, &tok);
3539 }
3540 }
3541 node
3542 }
3543
3544 TokenKind::Float => {
3545 self.bump();
3546 let f: f64 = tok.text.parse().unwrap_or(0.0);
3547 let mut node = self.add_spanned(Node::Literal(Value::Float(f)), &tok);
3548 if let TokenKind::Unit(_) = self.peek_kind() {
3549 let unit_tok = self.bump();
3550 if let Some(unit) = Unit::from_suffix(&unit_tok.text) {
3551 node = self.add_spanned(Node::Quantity { value: node, unit }, &tok);
3552 }
3553 }
3554 node
3555 }
3556
3557 TokenKind::True => {
3558 self.bump();
3559 self.add_spanned(Node::Literal(Value::Bool(true)), &tok)
3560 }
3561 TokenKind::False => {
3562 self.bump();
3563 self.add_spanned(Node::Literal(Value::Bool(false)), &tok)
3564 }
3565 TokenKind::Null => {
3566 self.bump();
3567 self.add_spanned(Node::Literal(Value::Null), &tok)
3568 }
3569
3570 TokenKind::Env => {
3571 self.bump();
3572 self.expect(TokenKind::Dot);
3573 let var = self
3574 .expect(TokenKind::Ident)
3575 .map(|t| t.text)
3576 .unwrap_or_default();
3577 let default = if matches!(self.peek_kind(), TokenKind::QuestionColon) {
3578 self.bump();
3579 Some(self.parse_expression())
3580 } else {
3581 None
3582 };
3583 self.add_spanned(Node::Env { name: var, default }, &tok)
3584 }
3585
3586 TokenKind::Secret => {
3587 self.bump();
3588 let from_env = if matches!(self.peek_kind(), TokenKind::Dot) {
3592 self.bump();
3593 if !matches!(self.peek_kind(), TokenKind::Env) {
3594 self.error(
3595 "E003",
3596 "expected `env` after `secret.` (use secret.env(name))",
3597 );
3598 } else {
3599 self.bump();
3600 }
3601 true
3602 } else {
3603 false
3604 };
3605 self.expect(TokenKind::LParen);
3606 let arg = self.parse_expression();
3607 self.expect(TokenKind::RParen);
3608 let locator = if from_env {
3609 match self.module.get(arg) {
3610 Some(Node::Literal(Value::String(name))) => self.add_spanned(
3611 Node::Env {
3612 name: name.clone(),
3613 default: None,
3614 },
3615 &tok,
3616 ),
3617 _ => {
3618 self.error(
3619 "E003",
3620 "secret.env() requires a string literal env var name",
3621 );
3622 arg
3623 }
3624 }
3625 } else {
3626 arg
3627 };
3628 self.add_spanned(Node::Secret { locator }, &tok)
3629 }
3630
3631 TokenKind::Ident => {
3632 self.bump();
3633 let text = tok.text.clone();
3637 let id = self.add_spanned(Node::Ident(text), &tok);
3638 if let Some(scope) = self.ident_scopes.last_mut() {
3639 scope.push((String::new(), id));
3640 }
3641 id
3642 }
3643
3644 TokenKind::LBracket => self.parse_array(),
3645 TokenKind::LBrace => self.parse_inline_table(),
3646
3647 TokenKind::LParen => {
3648 self.bump();
3649 let expr = self.parse_expression();
3650 self.expect(TokenKind::RParen);
3651 expr
3652 }
3653
3654 _ => {
3655 self.error("E002", format!("unexpected token {:?}", tok.kind));
3656 self.bump();
3657 self.module.add_node(Node::Literal(Value::Null))
3658 }
3659 }
3660 }
3661
3662 fn parse_semantic_block(&mut self, at_tok: &Token) -> NodeId {
3666 let tag_tok = self.peek().clone();
3667 let tag = match &tag_tok.kind {
3668 TokenKind::Ident => {
3669 self.bump();
3670 tag_tok.text.clone()
3671 }
3672 _ => {
3673 self.error_at(
3674 "E014",
3675 "expected a semantic tag (prompt, policy, template) after `@`",
3676 &tag_tok,
3677 );
3678 String::new()
3679 }
3680 };
3681 let tag = match SemanticTag::parse_tag(&tag) {
3682 Some(t) => t,
3683 None => {
3684 self.error_at(
3685 "E014",
3686 format!("unknown semantic tag `@{tag}` (expected prompt, policy, or template)"),
3687 &tag_tok,
3688 );
3689 SemanticTag::Prompt
3690 }
3691 };
3692 let str_tok = self.peek().clone();
3693 let inner = match &str_tok.kind {
3694 TokenKind::MlString(src_pos) => {
3695 self.bump();
3696 let (stripped, linemap, marks) = strip_common_indent_mapped(
3697 &str_tok.text,
3698 str_tok.line,
3699 str_tok.col,
3700 3,
3701 Some(src_pos),
3702 );
3703 self.parse_string_literal(&stripped, &str_tok, Some(&linemap), &marks)
3704 }
3705 TokenKind::MlSingleString => {
3706 self.bump();
3707 let stripped = strip_common_indent(&str_tok.text);
3708 self.add_spanned(Node::Literal(Value::String(stripped)), &str_tok)
3709 }
3710 _ => {
3711 self.error_at(
3712 "E014",
3713 "a semantic tag must be followed by a multi-line string (`\"\"\"…\"\"\"` or `'''…'''`)",
3714 &str_tok,
3715 );
3716 self.add_spanned(Node::Literal(Value::String(String::new())), at_tok)
3717 }
3718 };
3719 let id = self.add_spanned(Node::Semantic { tag, inner }, at_tok);
3720 id
3723 }
3724}
3725
3726fn strip_common_indent(raw: &str) -> String {
3733 strip_common_indent_mapped(raw, 1, 1, 0, None).0
3734}
3735#[derive(Debug, Clone)]
3738struct MlLineMap {
3739 src_line: u32,
3740 src_cols: Vec<u32>,
3741}
3742
3743fn linemap_from_charmap(text: &str, tok_line: u32, charmap: &[(u32, u32)]) -> Vec<MlLineMap> {
3748 let chars: Vec<char> = text.chars().collect();
3749 let mut lines: Vec<(usize, usize)> = Vec::new();
3750 let mut start = 0usize;
3751 for (i, c) in chars.iter().enumerate() {
3752 if *c == '\n' {
3753 lines.push((start, i));
3754 start = i + 1;
3755 }
3756 }
3757 lines.push((start, chars.len()));
3758 lines
3759 .iter()
3760 .map(|&(s, e)| {
3761 let src_line = tok_line + pos_entry(charmap, s, (0, 0)).0;
3762 let src_cols = (s..e).map(|k| pos_entry(charmap, k, (0, 0)).1).collect();
3763 MlLineMap { src_line, src_cols }
3764 })
3765 .collect()
3766}
3767
3768fn strip_common_indent_mapped(
3775 raw: &str,
3776 tok_line: u32,
3777 tok_col: u32,
3778 delim_len: u32,
3779 src_pos: Option<&[(u32, u32)]>,
3780) -> (String, Vec<MlLineMap>, Vec<bool>) {
3781 let mut lines: Vec<&str> = raw.split('\n').collect();
3782 let mut kept: Vec<usize> = (0..lines.len()).collect();
3786 if !lines.is_empty() && lines[0].trim().is_empty() {
3787 kept.remove(0);
3788 lines.remove(0);
3789 }
3790 if !lines.is_empty() && lines.last().is_some_and(|l| l.trim().is_empty()) {
3791 kept.pop();
3792 lines.pop();
3793 }
3794 let mut prefix: Vec<char> = Vec::new();
3796 let mut seen_content = false;
3797 for line in lines.iter() {
3798 if line.trim().is_empty() {
3799 continue;
3800 }
3801 let indent: Vec<char> = line
3802 .chars()
3803 .take_while(|c| *c == ' ' || *c == '\t')
3804 .collect();
3805 if !seen_content {
3806 prefix = indent;
3807 seen_content = true;
3808 } else {
3809 let shared = prefix
3810 .iter()
3811 .zip(indent.iter())
3812 .take_while(|(a, b)| a == b)
3813 .count();
3814 prefix.truncate(shared);
3815 }
3816 if prefix.is_empty() {
3817 break;
3818 }
3819 }
3820 let mut out = Vec::with_capacity(lines.len());
3821 let mut map = Vec::with_capacity(lines.len());
3822 let mut out_marks: Vec<Vec<bool>> = Vec::with_capacity(lines.len());
3825 let all: Vec<&str> = raw.split('\n').collect();
3830 let mut line_start: Vec<usize> = Vec::with_capacity(all.len());
3831 let mut acc = 0usize;
3832 for l in &all {
3833 line_start.push(acc);
3834 acc += l.chars().count() + 1; }
3836 let line_start_for =
3837 |raw_idx: usize| -> usize { line_start.get(raw_idx).copied().unwrap_or(0) };
3838 for (j, l) in lines.iter().enumerate() {
3839 let raw_idx = kept[j];
3840 let decoded_start = line_start_for(raw_idx);
3841 let at = |k: usize| -> (u32, u32) {
3842 match src_pos {
3843 Some(p) => pos_entry(p, k, (raw_idx as u32, k as u32)),
3846 None => (raw_idx as u32, k as u32),
3847 }
3848 };
3849 let is_marked =
3850 |k: usize| -> bool { src_pos.and_then(|p| p.get(k)).copied() == Some(SENTINEL_POS) };
3851 let shared = if l.trim().is_empty() {
3852 0
3853 } else {
3854 prefix
3855 .iter()
3856 .zip(l.chars())
3857 .take_while(|(a, b)| *a == b)
3858 .count()
3859 };
3860 if l.trim().is_empty() {
3862 let (line_off, _) = at(decoded_start);
3863 map.push(MlLineMap {
3864 src_line: tok_line + line_off,
3865 src_cols: Vec::new(),
3866 });
3867 out.push(String::new());
3868 out_marks.push(Vec::new());
3869 continue;
3870 }
3871 let text: String = l.chars().skip(shared).collect();
3872 let first_kept = decoded_start + shared;
3874 let (line_off, _) = at(first_kept.min(decoded_start + l.chars().count()));
3875 let src_line = tok_line + line_off;
3876 let cols: Vec<u32> = if src_pos.is_some() {
3877 (0..text.chars().count())
3878 .map(|c| at(first_kept + c).1)
3879 .collect()
3880 } else if raw_idx == 0 {
3881 (0..text.chars().count())
3883 .map(|c| tok_col + delim_len + shared as u32 + c as u32)
3884 .collect()
3885 } else {
3886 (0..text.chars().count())
3887 .map(|c| shared as u32 + 1 + c as u32)
3888 .collect()
3889 };
3890 let mline: Vec<bool> = text
3893 .chars()
3894 .enumerate()
3895 .map(|(c, ch)| ch == ESCAPED_DOLLAR && is_marked(first_kept + c))
3896 .collect();
3897 out.push(text);
3898 out_marks.push(mline);
3899 map.push(MlLineMap {
3900 src_line,
3901 src_cols: cols,
3902 });
3903 }
3904 let mut marks = Vec::new();
3905 for (j, mline) in out_marks.iter().enumerate() {
3906 if j > 0 {
3907 marks.push(false);
3908 }
3909 marks.extend(mline.iter().copied());
3910 }
3911 (out.join("\n"), map, marks)
3912}
3913
3914impl Parser {
3915 fn parse_patch_statement(&mut self, at_tok: &Token) {
3925 let nested = !self.current_path.is_empty();
3926 self.bump(); let tag_tok = self.bump(); let tag = tag_tok.text.clone();
3929 if self.expect(TokenKind::LParen).is_none() {
3930 return;
3931 }
3932 let path_tok = self.peek().clone();
3933 let path = match &path_tok.kind {
3934 TokenKind::String(_) | TokenKind::SingleString => {
3935 self.bump();
3936 path_tok.text.clone()
3937 }
3938 _ => {
3939 self.error_at("E019", "patch paths must be string literals", &path_tok);
3940 return;
3941 }
3942 };
3943 if path.contains("${")
3944 || Self::has_marked(&path, &Self::sentinel_marks(&path, &path_tok.kind))
3945 {
3946 self.error_at(
3947 "E019",
3948 "interpolation in patch paths is a hard error; paths must be static",
3949 &path_tok,
3950 );
3951 return;
3952 }
3953 if let Err(why) = validate_patch_path(&path) {
3954 self.error_at(
3955 "E019",
3956 format!("invalid patch path `{path}`: {why}"),
3957 &path_tok,
3958 );
3959 return;
3960 }
3961 let op = match tag.as_str() {
3962 "remove" => {
3963 if self.expect(TokenKind::RParen).is_none() {
3964 return;
3965 }
3966 PatchOp::Remove { path: path.clone() }
3967 }
3968 "rename" => {
3969 if self.expect(TokenKind::Comma).is_none() {
3970 return;
3971 }
3972 let new_tok = self.peek().clone();
3973 let new_name = match &new_tok.kind {
3974 TokenKind::String(_) | TokenKind::SingleString => {
3975 self.bump();
3976 Self::render_marked(
3977 &new_tok.text,
3978 &Self::sentinel_marks(&new_tok.text, &new_tok.kind),
3979 )
3980 }
3981 _ => {
3982 self.error_at(
3983 "E019",
3984 "rename needs a new key name as a string literal",
3985 &new_tok,
3986 );
3987 return;
3988 }
3989 };
3990 if new_name.contains('.') || new_name.contains('[') || new_name.is_empty() {
3991 self.error_at(
3992 "E019",
3993 "rename target must be a plain key name in the same parent",
3994 &new_tok,
3995 );
3996 return;
3997 }
3998 if self.expect(TokenKind::RParen).is_none() {
3999 return;
4000 }
4001 PatchOp::Rename {
4002 from: path.clone(),
4003 to: new_name,
4004 }
4005 }
4006 _ => {
4007 if self.expect(TokenKind::Comma).is_none() {
4009 return;
4010 }
4011 let value = self.parse_expression();
4012 if self.expect(TokenKind::RParen).is_none() {
4013 return;
4014 }
4015 PatchOp::Set {
4016 path: path.clone(),
4017 value,
4018 }
4019 }
4020 };
4021 let stmt_key = match &op {
4022 PatchOp::Set { path, .. } => format!("@patch({path})"),
4023 PatchOp::Remove { path } => format!("@remove({path})"),
4024 PatchOp::Rename { from, .. } => format!("@rename({from})"),
4025 };
4026 if nested {
4027 self.error_at(
4028 "E019",
4029 "patch statements may only appear at document root",
4030 at_tok,
4031 );
4032 return;
4033 }
4034 self.record_stmt(
4035 &stmt_key,
4036 at_tok.line,
4037 self.last_end_line(),
4038 at_tok.col.saturating_sub(1) as usize,
4039 );
4040 let id = self.add_spanned(Node::PatchDirective { op }, at_tok);
4041 if let Some(Node::PatchDirective {
4044 op: PatchOp::Set { value, .. },
4045 }) = self.module.get(id)
4046 {
4047 let value = *value;
4048 self.module.add_edge(value, id);
4049 }
4050 self.module.patches.push(id);
4051 }
4052
4053 fn parse_schema_inline(&mut self, key_tok: &Token) -> NodeId {
4060 let open = self
4061 .expect(TokenKind::LBrace)
4062 .unwrap_or_else(|| key_tok.clone());
4063 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
4064 self.add_spanned(Node::SchemaTable { fields, closed }, &open)
4065 }
4066
4067 fn parse_schema_brace(&mut self, key_tok: &Token) -> NodeId {
4069 let open = self
4070 .expect(TokenKind::LBrace)
4071 .unwrap_or_else(|| key_tok.clone());
4072 self.brace_tables += 1;
4073 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
4074 self.add_spanned(Node::SchemaTable { fields, closed }, &open)
4075 }
4076
4077 fn parse_schema_indent(&mut self, key_tok: &Token, _level: usize) -> NodeId {
4079 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Indent);
4080 self.add_spanned(Node::SchemaTable { fields, closed }, key_tok)
4081 }
4082
4083 fn parse_schema_entries(&mut self, end: SchemaEnd) -> (IndexMap<String, NodeId>, bool) {
4086 let mut fields = IndexMap::new();
4087 let mut closed = false;
4088 loop {
4089 self.skip_newlines();
4090 match self.peek_kind() {
4091 TokenKind::Eof => {
4092 if matches!(end, SchemaEnd::Brace) {
4096 self.error(
4097 "E001",
4098 "unclosed schema block: expected `}` before end of file",
4099 );
4100 }
4101 break;
4102 }
4103 TokenKind::RBrace if matches!(end, SchemaEnd::Brace) => {
4104 self.bump();
4105 break;
4106 }
4107 TokenKind::Dedent if matches!(end, SchemaEnd::Indent) => {
4108 self.bump();
4109 break;
4110 }
4111 TokenKind::RBrace | TokenKind::Dedent => {
4112 break;
4115 }
4116 TokenKind::Comma => {
4117 self.bump();
4118 continue;
4119 }
4120 TokenKind::Ident => {
4121 let name_tok = self.bump();
4122 let name = name_tok.text.clone();
4123 if self.expect(TokenKind::Assign).is_none() {
4124 continue;
4125 }
4126 if name == "closed" {
4127 match self.peek_kind() {
4128 TokenKind::True => {
4129 self.bump();
4130 closed = true;
4131 }
4132 TokenKind::False => {
4133 self.bump();
4134 closed = false;
4135 }
4136 _ => {
4137 let tok = self.peek().clone();
4138 self.error_at("E020", "closed must be true or false", &tok);
4139 }
4140 }
4141 self.skip_newlines();
4142 if matches!(self.peek_kind(), TokenKind::Comma) {
4143 self.bump();
4144 }
4145 continue;
4146 }
4147 let id = self.parse_schema_type(&name_tok);
4148 if self.schema_depth == 0 {
4159 self.record_stmt(
4160 &name,
4161 name_tok.line,
4162 self.last_end_line(),
4163 name_tok.col.saturating_sub(1) as usize,
4164 );
4165 }
4166 fields.insert(name, id);
4167 self.skip_newlines();
4168 if matches!(self.peek_kind(), TokenKind::Comma) {
4169 self.bump();
4170 }
4171 }
4172 _ => {
4173 let tok = self.peek().clone();
4174 self.error_at("E020", "expected a schema entry (`name = Type`)", &tok);
4175 self.bump();
4176 }
4177 }
4178 }
4179 (fields, closed)
4180 }
4181
4182 fn enter_schema_depth(&mut self, tok: &Token) -> bool {
4189 self.schema_depth += 1;
4190 if self.schema_depth > MAX_SCHEMA_DEPTH {
4191 self.schema_depth -= 1;
4192 self.error_at(
4193 "E004",
4194 format!("maximum schema nesting depth ({MAX_SCHEMA_DEPTH}) exceeded"),
4195 tok,
4196 );
4197 return true;
4198 }
4199 false
4200 }
4201
4202 fn exit_schema_depth(&mut self) {
4203 self.schema_depth = self.schema_depth.saturating_sub(1);
4204 }
4205
4206 fn parse_schema_type(&mut self, _name_tok: &Token) -> NodeId {
4210 let tok = self.peek().clone();
4211 let TokenKind::Ident = &tok.kind else {
4212 self.error_at("E020", "expected a schema type", &tok);
4213 self.bump();
4214 return self.fallback_schema_field(&tok);
4215 };
4216 let type_name = tok.text.clone();
4217 if type_name == "Array" {
4219 self.bump();
4220 if self.expect(TokenKind::Lt).is_none() {
4221 self.error_at("E020", "Array needs an element type: Array<T>", &tok);
4222 return self.fallback_schema_field(&tok);
4223 }
4224 let elem_tok = self.peek().clone();
4225 if self.enter_schema_depth(&tok) {
4226 return self.fallback_schema_field(&tok);
4227 }
4228 let element = self.parse_schema_type(&elem_tok);
4229 self.exit_schema_depth();
4230 if self.expect(TokenKind::Gt).is_none() {
4231 return self.fallback_schema_field(&tok);
4232 }
4233 return self.add_spanned(Node::SchemaArray { element }, &tok);
4234 }
4235 if type_name == "Enum" {
4237 self.bump();
4238 if self.expect(TokenKind::LParen).is_none() {
4239 self.error_at("E020", "Enum needs a value list: Enum(\"a\", ...)", &tok);
4240 return self.fallback_schema_field(&tok);
4241 }
4242 let mut values = Vec::new();
4243 let mut deprecated: Option<String> = None;
4244 loop {
4245 self.skip_newlines();
4246 match self.peek_kind() {
4247 TokenKind::RParen | TokenKind::Eof => break,
4248 TokenKind::Comma => {
4249 self.bump();
4250 continue;
4251 }
4252 TokenKind::String(_) | TokenKind::SingleString => {
4253 let v = self.bump();
4254 values.push(Self::render_marked(
4255 &v.text,
4256 &Self::sentinel_marks(&v.text, &v.kind),
4257 ));
4258 }
4259 TokenKind::Ident if self.peek().text == "deprecated" => {
4260 self.bump();
4264 if self.expect(TokenKind::Assign).is_none() {
4265 continue;
4266 }
4267 deprecated = match self.peek_kind() {
4268 TokenKind::True => {
4269 self.bump();
4270 Some(String::new())
4271 }
4272 TokenKind::String(_) | TokenKind::SingleString => {
4273 let v = self.bump();
4274 Some(Self::render_marked(
4275 &v.text,
4276 &Self::sentinel_marks(&v.text, &v.kind),
4277 ))
4278 }
4279 _ => {
4280 let bad = self.peek().clone();
4281 self.error_at(
4282 "E020",
4283 "deprecated takes `true` or a string message",
4284 &bad,
4285 );
4286 None
4287 }
4288 };
4289 }
4290 _ => {
4291 let bad = self.peek().clone();
4292 self.error_at("E020", "Enum values must be string literals", &bad);
4293 self.bump();
4294 }
4295 }
4296 self.skip_newlines();
4297 if matches!(self.peek_kind(), TokenKind::Comma) {
4298 self.bump();
4299 }
4300 }
4301 self.expect(TokenKind::RParen);
4302 if values.is_empty() {
4303 self.error_at("E020", "Enum needs at least one value", &tok);
4304 }
4305 let field = SchemaField {
4306 typ: ActiveType::Enum(values),
4307 min: None,
4308 max: None,
4309 min_len: None,
4310 max_len: None,
4311 pattern: None,
4312 default: None,
4313 deprecated,
4314 span: Some(span_of(&tok)),
4315 };
4316 return self.add_spanned(Node::SchemaField { field }, &tok);
4317 }
4318 if type_name == "Table" && matches!(self.peek_kind_at(1), TokenKind::LBrace) {
4320 self.bump();
4321 let open = self
4322 .expect(TokenKind::LBrace)
4323 .unwrap_or_else(|| tok.clone());
4324 if self.enter_schema_depth(&tok) {
4325 return self.fallback_schema_field(&tok);
4326 }
4327 let (fields, closed) = self.parse_schema_entries(SchemaEnd::Brace);
4328 self.exit_schema_depth();
4329 return self.add_spanned(Node::SchemaTable { fields, closed }, &open);
4330 }
4331 let Some(typ) = ActiveType::bare(&type_name) else {
4333 self.error_at("E020", format!("unknown schema type `{type_name}`"), &tok);
4334 self.bump();
4335 return self.fallback_schema_field(&tok);
4336 };
4337 self.bump();
4338 if !matches!(self.peek_kind(), TokenKind::LParen) {
4339 let field = SchemaField::bare(typ, Some(span_of(&tok)));
4340 return self.add_spanned(Node::SchemaField { field }, &tok);
4341 }
4342 if matches!(typ, ActiveType::Table { .. }) {
4346 self.error_at(
4347 "E020",
4348 "Table takes no call arguments; write Table { ... } for a nested schema",
4349 &tok,
4350 );
4351 self.bump(); let mut paren_depth = 1usize;
4353 while paren_depth > 0 {
4354 match self.peek_kind() {
4355 TokenKind::LParen => {
4356 self.bump();
4357 paren_depth += 1;
4358 }
4359 TokenKind::RParen => {
4360 self.bump();
4361 paren_depth -= 1;
4362 }
4363 TokenKind::Eof => break,
4364 _ => {
4365 self.bump();
4366 }
4367 }
4368 }
4369 return self.fallback_schema_field(&tok);
4370 }
4371 self.bump(); let mut field = SchemaField::bare(typ.clone(), Some(span_of(&tok)));
4373 loop {
4374 self.skip_newlines();
4375 match self.peek_kind() {
4376 TokenKind::RParen | TokenKind::Eof => break,
4377 TokenKind::Comma => {
4378 self.bump();
4379 continue;
4380 }
4381 TokenKind::Ident => {
4382 let arg_tok = self.bump();
4383 let arg = arg_tok.text.clone();
4384 if self.expect(TokenKind::Assign).is_none() {
4385 continue;
4386 }
4387 if arg == "deprecated" {
4391 let value = self.parse_expression();
4392 if field.deprecated.is_none() {
4394 match self.module.get(value) {
4395 Some(Node::Literal(Value::Bool(true))) => {
4396 field.deprecated = Some(String::new());
4397 }
4398 Some(Node::Literal(Value::String(msg))) => {
4399 field.deprecated = Some(msg.clone());
4400 }
4401 _ => {
4402 self.error_at(
4403 "E020",
4404 "deprecated takes `true` or a string message",
4405 &arg_tok,
4406 );
4407 }
4408 }
4409 }
4410 continue;
4411 }
4412 let value = self.parse_expression();
4413 if !Self::store_schema_arg(&mut field, &typ, &arg, value) {
4414 self.error_at(
4415 "E020",
4416 format!("unknown argument `{arg}` for {}", typ.name()),
4417 &arg_tok,
4418 );
4419 }
4420 }
4421 _ => {
4422 let bad = self.peek().clone();
4423 self.error_at("E020", "expected a named argument (`name = value`)", &bad);
4424 self.bump();
4425 }
4426 }
4427 self.skip_newlines();
4428 if matches!(self.peek_kind(), TokenKind::Comma) {
4429 self.bump();
4430 }
4431 }
4432 self.expect(TokenKind::RParen);
4433 self.add_spanned(Node::SchemaField { field }, &tok)
4434 }
4435
4436 fn fallback_schema_field(&mut self, tok: &Token) -> NodeId {
4438 let field = SchemaField::bare(ActiveType::Any, Some(span_of(tok)));
4439 self.add_spanned(Node::SchemaField { field }, tok)
4440 }
4441
4442 fn store_schema_arg(
4446 field: &mut SchemaField,
4447 typ: &ActiveType,
4448 arg: &str,
4449 value: NodeId,
4450 ) -> bool {
4451 use ActiveType::*;
4452 let slot = match (typ, arg) {
4453 (Int | Float | Duration | Bytes, "min") => &mut field.min,
4454 (Int | Float | Duration | Bytes, "max") => &mut field.max,
4455 (String, "min_len") => &mut field.min_len,
4456 (String, "max_len") => &mut field.max_len,
4457 (String, "pattern") => &mut field.pattern,
4458 (_, "default") => &mut field.default,
4459 _ => return false,
4460 };
4461 if slot.is_none() {
4462 *slot = Some(value);
4463 }
4464 true
4465 }
4466 fn sentinel_marks(text: &str, kind: &TokenKind) -> Vec<bool> {
4477 let pos = match kind {
4478 TokenKind::String(p) | TokenKind::MlString(p) => p,
4479 _ => return vec![false; text.chars().count()],
4480 };
4481 text.chars()
4482 .enumerate()
4483 .map(|(i, c)| c == ESCAPED_DOLLAR && pos.get(i) == Some(&SENTINEL_POS))
4484 .collect()
4485 }
4486
4487 fn render_marked(text: &str, marks: &[bool]) -> String {
4489 let mut out = String::with_capacity(text.len());
4490 for (i, c) in text.chars().enumerate() {
4491 if c == ESCAPED_DOLLAR && marks.get(i) == Some(&true) {
4492 out.push('$');
4493 } else {
4494 out.push(c);
4495 }
4496 }
4497 out
4498 }
4499
4500 fn render_char(c: char, marked: bool) -> char {
4502 if c == ESCAPED_DOLLAR && marked {
4503 '$'
4504 } else {
4505 c
4506 }
4507 }
4508
4509 fn has_marked(text: &str, marks: &[bool]) -> bool {
4511 text.chars()
4512 .enumerate()
4513 .any(|(i, c)| c == ESCAPED_DOLLAR && marks.get(i) == Some(&true))
4514 }
4515
4516 fn parse_string_literal(
4517 &mut self,
4518 raw: &str,
4519 tok: &Token,
4520 linemap: Option<&[MlLineMap]>,
4521 marks: &[bool],
4522 ) -> NodeId {
4523 if !raw.contains("${") && !Self::has_marked(raw, marks) {
4526 return self.add_spanned(Node::Literal(Value::String(raw.to_string())), tok);
4527 }
4528
4529 let chars: Vec<char> = raw.chars().collect();
4530 let marked = |i: usize| -> bool { marks.get(i) == Some(&true) };
4531 let mut parts: Vec<InterpPart> = Vec::new();
4532 let mut literal = String::new();
4533 let mut i = 0usize;
4534
4535 while i < chars.len() {
4536 if chars[i] == ESCAPED_DOLLAR && marked(i) {
4540 literal.push('$');
4541 i += 1;
4542 continue;
4543 }
4544 if chars[i] == '$' && i + 1 < chars.len() && chars[i + 1] == '{' {
4545 match self.find_interpolation_end(&chars, i + 2) {
4546 Some(end) => {
4547 if !literal.is_empty() {
4548 parts.push(InterpPart::Literal(std::mem::take(&mut literal)));
4549 }
4550 let inner: String = chars[i + 2..end].iter().collect();
4551 let offset = i + 2;
4554 let id = self.parse_embedded_expression(&inner, tok, raw, offset, linemap);
4555 parts.push(InterpPart::Expr(id));
4556 i = end + 1;
4557 continue;
4558 }
4559 None => {
4560 let span = self.interp_span(tok, raw, i, linemap);
4563 self.error_at(
4564 "E012",
4565 "unterminated interpolation: `${` has no matching `}`",
4566 &span,
4567 );
4568 for (k, c) in chars[i..].iter().enumerate() {
4569 literal.push(Self::render_char(*c, marked(i + k)));
4570 }
4571 break;
4572 }
4573 }
4574 }
4575 literal.push(Self::render_char(chars[i], marked(i)));
4576 i += 1;
4577 }
4578 if !literal.is_empty() {
4579 parts.push(InterpPart::Literal(literal));
4580 }
4581
4582 self.add_spanned(Node::Interpolate { parts }, tok)
4583 }
4584
4585 fn find_interpolation_end(&self, chars: &[char], start: usize) -> Option<usize> {
4589 let mut i = start;
4590 let mut depth = 0usize;
4591 let mut in_string: Option<char> = None;
4592 let mut escaped = false;
4593 while i < chars.len() {
4594 let c = chars[i];
4595 if let Some(quote) = in_string {
4596 if escaped {
4597 escaped = false;
4598 } else if c == '\\' {
4599 escaped = true;
4600 } else if c == quote {
4601 in_string = None;
4602 }
4603 i += 1;
4604 continue;
4605 }
4606 match c {
4607 '"' | '\'' => in_string = Some(c),
4608 '$' if i + 1 < chars.len() && chars[i + 1] == '{' => {
4609 depth += 1;
4610 i += 2;
4611 continue;
4612 }
4613 '{' => depth += 1,
4614 '}' => {
4615 if depth == 0 {
4616 return Some(i);
4617 }
4618 depth -= 1;
4619 }
4620 _ => {}
4621 }
4622 i += 1;
4623 }
4624 None
4625 }
4626
4627 fn interp_span(
4634 &self,
4635 tok: &Token,
4636 stripped: &str,
4637 offset: usize,
4638 linemap: Option<&[MlLineMap]>,
4639 ) -> Token {
4640 let mut t = tok.clone();
4641 match linemap {
4642 Some(map) => {
4643 let mut line = 0usize;
4646 let mut col = 0usize;
4647 for c in stripped.chars().take(offset) {
4648 if c == '\n' {
4649 line += 1;
4650 col = 0;
4651 } else {
4652 col += 1;
4653 }
4654 }
4655 match map
4656 .get(line)
4657 .and_then(|m| m.src_cols.get(col).map(|&c| (m.src_line, c)))
4658 {
4659 Some((src_line, src_col)) => {
4660 t.line = src_line;
4661 t.col = src_col;
4662 t.end_line = src_line;
4663 t.end_col = src_col + 1;
4664 }
4665 None => {
4666 t.col = tok.col + 1 + offset as u32;
4667 t.end_col = t.col + 1;
4668 }
4669 }
4670 }
4671 None => {
4672 t.col = tok.col + 1 + offset as u32;
4673 t.end_col = t.col + 1;
4674 }
4675 }
4676 t
4677 }
4678
4679 fn parse_embedded_expression(
4687 &mut self,
4688 text: &str,
4689 tok: &Token,
4690 stripped: &str,
4691 offset: usize,
4692 linemap: Option<&[MlLineMap]>,
4693 ) -> NodeId {
4694 let leading_ws = text.chars().count() - text.trim_start().chars().count();
4699 let text = text.trim();
4700 let offset = offset + leading_ws;
4701 if text.is_empty() {
4702 let span = self.interp_span(tok, stripped, offset, linemap);
4703 self.error_at(
4704 "E013",
4705 "empty interpolation: `${}` has no expression",
4706 &span,
4707 );
4708 return self.add_spanned(Node::Literal(Value::Null), tok);
4709 }
4710 let mut lexer = Lexer::new(text);
4711 let lexed = lexer.tokenize();
4712
4713 let (anchor_line, anchor_col) = if linemap.is_some() {
4722 let mut al = 0u32;
4723 let mut ac = 0u32;
4724 for c in stripped.chars().take(offset) {
4725 if c == '\n' {
4726 al += 1;
4727 ac = 0;
4728 } else {
4729 ac += 1;
4730 }
4731 }
4732 (al, ac)
4733 } else {
4734 (0, 0)
4735 };
4736
4737 let module = std::mem::replace(&mut self.module, Module::new(""));
4740 let inherited_len = self.ident_scopes.last().map_or(0, |s| s.len());
4744 let mut sub = Parser {
4745 tokens: lexed.tokens,
4746 pos: 0,
4747 module,
4748 errors: Vec::new(),
4749 ident_scopes: self.ident_scopes.clone(),
4750 depth: self.depth,
4751 options: self.options.clone(),
4752 block_depth: 0,
4753 schema_depth: self.schema_depth,
4754 comments: Vec::new(),
4755 comment_files: Vec::new(),
4756 statements: Vec::new(),
4757 blocks: Vec::new(),
4758 current_path: self.current_path.clone(),
4759 brace_tables: 0,
4760 indent_tables: 0,
4761 current_file: self.current_file.clone(),
4762 include_stack: Vec::new(),
4763 in_include: self.in_include,
4764 in_profile: self.in_profile,
4765 array_depth: self.array_depth,
4766 binding_spliced: self.binding_spliced.clone(),
4767 pending_profiles: Vec::new(),
4768 trust: self.trust.clone(),
4769 };
4770 let node_base = sub.module.arena.len() as u32;
4774 let id = sub.parse_expression();
4775 while matches!(sub.peek_kind(), TokenKind::Newline) {
4777 sub.bump();
4778 }
4779 if !matches!(sub.peek_kind(), TokenKind::Eof) {
4780 let leftover = sub.peek().clone();
4781 sub.error_at(
4782 "E002",
4783 format!(
4784 "unexpected {:?} after the interpolated expression",
4785 leftover.kind
4786 ),
4787 &leftover,
4788 );
4789 }
4790
4791 let new_idents: Vec<(String, NodeId)> = sub
4792 .ident_scopes
4793 .last()
4794 .map(|s| s[inherited_len.min(s.len())..].to_vec())
4795 .unwrap_or_default();
4796 let mut sub_errors = std::mem::take(&mut sub.errors);
4797 let mut sub_repairs = std::mem::take(&mut sub.module.healing);
4798 let mut sub_lex_errors = lexed.errors;
4799 let mut sub_lex_repairs = lexed.repairs;
4800 let lift = |line_1: u32, col_1: u32| -> Option<(u32, u32)> {
4816 match linemap {
4817 Some(map) => {
4818 let sl = anchor_line as usize + (line_1 as usize).saturating_sub(1);
4819 let sc = if line_1 <= 1 {
4820 anchor_col as usize + (col_1 as usize).saturating_sub(1)
4821 } else {
4822 (col_1 as usize).saturating_sub(1)
4823 };
4824 let m = map.get(sl)?;
4825 let col = m.src_cols.get(sc).copied()?;
4826 Some((m.src_line, col))
4827 }
4828 None => {
4829 let line = tok.line + line_1.saturating_sub(1);
4830 let col = if line_1 <= 1 {
4831 tok.col + offset as u32 + col_1.saturating_sub(1)
4832 } else {
4833 col_1
4834 };
4835 Some((line, col))
4836 }
4837 }
4838 };
4839 for (node, span) in sub.module.node_spans.iter_mut() {
4840 if node.0 >= node_base {
4841 if let Some((line, col)) = lift(span.line, span.column) {
4842 span.line = line;
4843 span.column = col;
4844 }
4845 if let Some((line, col)) = lift(span.end_line, span.end_column) {
4846 span.end_line = line;
4847 span.end_column = col;
4848 }
4849 }
4850 }
4851 self.module = std::mem::replace(&mut sub.module, Module::new(""));
4852
4853 if let Some(scope) = self.ident_scopes.last_mut() {
4854 scope.extend(new_idents);
4855 }
4856 let rebase = |d: &mut Diagnostic| {
4859 if let Some(s) = &mut d.span {
4860 if let Some((line, col)) = lift(s.line, s.column) {
4861 s.line = line;
4862 s.column = col;
4863 }
4864 if let Some((line, col)) = lift(s.end_line, s.end_column) {
4865 s.end_line = line;
4866 s.end_column = col;
4867 }
4868 }
4869 };
4870 for d in sub_lex_errors
4871 .iter_mut()
4872 .chain(sub_lex_repairs.iter_mut())
4873 .chain(sub_errors.iter_mut())
4874 .chain(sub_repairs.iter_mut())
4875 {
4876 rebase(d);
4877 }
4878 let sub_file = self.current_file_name();
4881 for d in sub_lex_errors.iter_mut().chain(sub_lex_repairs.iter_mut()) {
4882 if d.file.is_none() {
4883 d.file = sub_file.clone();
4884 }
4885 }
4886 self.errors.extend(sub_lex_errors);
4887 self.errors.extend(sub_errors);
4888 self.module.healing.extend(sub_lex_repairs);
4889 self.module.healing.extend(sub_repairs);
4890 id
4891 }
4892
4893 fn parse_array(&mut self) -> NodeId {
4894 let open = self.peek().clone();
4895 self.bump(); self.array_depth += 1;
4899 let mut elements = Vec::new();
4900 loop {
4901 self.skip_newlines();
4902 if matches!(
4905 self.peek_kind(),
4906 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4907 ) {
4908 break;
4909 }
4910 if matches!(self.peek_kind(), TokenKind::Comma) {
4911 let comma = self.bump();
4913 if !self.heal(
4914 "H002",
4915 "removed stray comma".into(),
4916 &comma,
4917 Repair {
4918 original: ",".into(),
4919 suggested: String::new(),
4920 kind: RepairKind::RemoveTrailingComma,
4921 safe: true,
4922 },
4923 thresholds::AUTO_APPLY + 0.03,
4924 ) {
4925 self.error("E002", "unexpected comma");
4926 }
4927 continue;
4928 }
4929 elements.push(self.parse_expression());
4930 self.skip_newlines();
4931 if matches!(self.peek_kind(), TokenKind::Comma) {
4932 let comma = self.bump();
4933 self.skip_newlines();
4934 if matches!(
4935 self.peek_kind(),
4936 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4937 ) {
4938 self.heal(
4939 "H002",
4940 "removed trailing comma before closing bracket".into(),
4941 &comma,
4942 Repair {
4943 original: ",".into(),
4944 suggested: String::new(),
4945 kind: RepairKind::RemoveTrailingComma,
4946 safe: true,
4947 },
4948 thresholds::AUTO_APPLY + 0.03,
4949 );
4950 }
4951 } else if !matches!(
4952 self.peek_kind(),
4953 TokenKind::RBracket | TokenKind::RBrace | TokenKind::Eof
4954 ) {
4955 let tok = self.peek().clone();
4957 if !self.heal(
4958 "H003",
4959 "inserted missing comma between array elements".into(),
4960 &tok,
4961 Repair {
4962 original: String::new(),
4963 suggested: ",".into(),
4964 kind: RepairKind::Other("insert_comma".into()),
4965 safe: false,
4966 },
4967 thresholds::HIGH + 0.05, ) {
4969 self.error(
4970 "E001",
4971 format!("expected `,` between array elements, found {:?}", tok.kind),
4972 );
4973 }
4974 }
4975 }
4976 self.expect_closing_healing(TokenKind::RBracket, "H004");
4977 self.array_depth = self.array_depth.saturating_sub(1);
4978 self.add_spanned(Node::Array { elements }, &open)
4979 }
4980
4981 fn parse_inline_table(&mut self) -> NodeId {
4982 let open = self.peek().clone();
4983 self.bump(); self.ident_scopes.push(Vec::new());
4985 let mut fields = indexmap::IndexMap::new();
4986 loop {
4987 self.skip_newlines();
4988 if matches!(
4989 self.peek_kind(),
4990 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
4991 ) {
4992 break;
4993 }
4994 if matches!(self.peek_kind(), TokenKind::Comma) {
4995 let comma = self.bump();
4996 if !self.heal(
4997 "H002",
4998 "removed stray comma".into(),
4999 &comma,
5000 Repair {
5001 original: ",".into(),
5002 suggested: String::new(),
5003 kind: RepairKind::RemoveTrailingComma,
5004 safe: true,
5005 },
5006 thresholds::AUTO_APPLY + 0.03,
5007 ) {
5008 self.error("E002", "unexpected comma");
5009 }
5010 continue;
5011 }
5012 if let Some((k, id)) = self.parse_key_value() {
5013 let span = self.module.node_spans.get(&id).cloned();
5014 self.note_duplicate_key(&k, span, fields.contains_key(&k));
5015 fields.insert(k, id);
5016 } else {
5017 break;
5018 }
5019 self.skip_newlines();
5020 if matches!(self.peek_kind(), TokenKind::Comma) {
5021 let comma = self.bump();
5022 self.skip_newlines();
5023 if matches!(
5024 self.peek_kind(),
5025 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
5026 ) {
5027 self.heal(
5028 "H002",
5029 "removed trailing comma before closing brace".into(),
5030 &comma,
5031 Repair {
5032 original: ",".into(),
5033 suggested: String::new(),
5034 kind: RepairKind::RemoveTrailingComma,
5035 safe: true,
5036 },
5037 thresholds::AUTO_APPLY + 0.03,
5038 );
5039 }
5040 } else if !matches!(
5041 self.peek_kind(),
5042 TokenKind::RBrace | TokenKind::RBracket | TokenKind::Eof
5043 ) {
5044 let tok = self.peek().clone();
5045 if !self.heal(
5046 "H003",
5047 "inserted missing comma between table entries".into(),
5048 &tok,
5049 Repair {
5050 original: String::new(),
5051 suggested: ",".into(),
5052 kind: RepairKind::Other("insert_comma".into()),
5053 safe: false,
5054 },
5055 thresholds::HIGH + 0.05, ) {
5057 self.error(
5058 "E001",
5059 format!("expected `,` between table entries, found {:?}", tok.kind),
5060 );
5061 }
5062 }
5063 }
5064 self.expect_closing_healing(TokenKind::RBrace, "H004");
5065 let idents = self.ident_scopes.pop().unwrap_or_default();
5066 let _ = self.resolve_sibling_refs(idents, &fields);
5067 self.add_spanned(Node::Table { fields }, &open)
5068 }
5069
5070 fn build_graph(&mut self) {
5075 let bindings = self.module.bindings.clone();
5076 let declared: Vec<(NodeId, Vec<String>)> = self
5081 .module
5082 .depends_on
5083 .iter()
5084 .map(|(id, dep)| (*id, dep.paths.clone()))
5085 .collect();
5086 for (scope, paths) in &declared {
5087 for path in paths {
5088 if let Some(target) = self.module.resolve_value_path(path) {
5089 self.module.add_edge(target, *scope);
5090 }
5091 }
5092 }
5093
5094 for i in 0..self.module.arena.len() {
5095 let id = NodeId(i as u32);
5096 let node = self.module.get(id).cloned();
5097 match node {
5098 Some(Node::Ident(name)) => {
5099 if let Some(&target) = bindings.get(&name) {
5100 if let Some(slot) = self.module.get_mut(id) {
5101 *slot = Node::Ref(target);
5102 }
5103 self.module.add_edge(target, id);
5105 }
5106 }
5107 Some(Node::Env {
5108 default: Some(def), ..
5109 }) => {
5110 self.module.add_edge(def, id);
5111 }
5112 Some(Node::Interpolate { parts }) => {
5113 for part in &parts {
5114 if let InterpPart::Expr(expr_id) = part {
5115 self.module.add_edge(*expr_id, id);
5116 }
5117 }
5118 }
5119 Some(Node::Binary { lhs, rhs, .. }) => {
5120 self.module.add_edge(lhs, id);
5121 self.module.add_edge(rhs, id);
5122 }
5123 Some(Node::Unary { expr, .. }) => {
5124 self.module.add_edge(expr, id);
5125 }
5126 Some(Node::Quantity { value, .. }) => {
5127 self.module.add_edge(value, id);
5128 }
5129 Some(Node::Semantic { inner, .. }) => {
5130 self.module.add_edge(inner, id);
5131 }
5132 Some(Node::SchemaField { field }) => {
5133 for arg in [
5136 field.min,
5137 field.max,
5138 field.min_len,
5139 field.max_len,
5140 field.pattern,
5141 field.default,
5142 ]
5143 .into_iter()
5144 .flatten()
5145 {
5146 self.module.add_edge(arg, id);
5147 }
5148 }
5149 Some(Node::SchemaTable { fields, .. }) => {
5150 for child in fields.values() {
5151 self.module.add_edge(*child, id);
5152 }
5153 }
5154 Some(Node::SchemaArray { element }) => {
5155 self.module.add_edge(element, id);
5156 }
5157 Some(Node::Array { elements }) => {
5158 for e in &elements {
5159 self.module.add_edge(*e, id);
5160 }
5161 }
5162 Some(Node::Table { fields }) => {
5163 for (_, fid) in &fields {
5164 self.module.add_edge(*fid, id);
5165 }
5166 }
5167 Some(Node::Secret { locator }) => {
5168 self.module.add_edge(locator, id);
5169 }
5170 _ => {}
5171 }
5172 }
5173 }
5174}