1use crate::llm::types::{DxDocument, DxLlmValue, DxSection};
44use indexmap::IndexMap;
45use thiserror::Error;
46
47#[derive(Debug, Error)]
49pub enum ParseError {
50 #[error("Unexpected character '{ch}' at position {pos}")]
52 UnexpectedChar {
53 ch: char,
55 pos: usize,
57 },
58
59 #[error("Unexpected end of input")]
61 UnexpectedEof,
62
63 #[error("Invalid value format: {value}")]
65 InvalidValue {
66 value: String,
68 },
69
70 #[error("Schema mismatch: expected {expected} columns, got {got}")]
72 SchemaMismatch {
73 expected: usize,
75 got: usize,
77 },
78
79 #[error("Invalid UTF-8 at byte offset {offset}")]
81 Utf8Error {
82 offset: usize,
84 },
85
86 #[error("Input too large: {size} bytes exceeds maximum of {max} bytes")]
88 InputTooLarge {
89 size: usize,
91 max: usize,
93 },
94
95 #[error("Unclosed bracket at position {pos}")]
97 UnclosedBracket {
98 pos: usize,
100 },
101
102 #[error("Unclosed parenthesis at position {pos}")]
104 UnclosedParen {
105 pos: usize,
107 },
108
109 #[error("Missing value after '=' at position {pos}")]
111 MissingValue {
112 pos: usize,
114 },
115
116 #[error("Invalid table format: {msg}")]
118 InvalidTable {
119 msg: String,
121 },
122}
123
124pub struct LlmParser;
126
127impl LlmParser {
128 pub fn parse(input: &str) -> Result<DxDocument, ParseError> {
130 if input.len() > crate::error::MAX_INPUT_SIZE {
131 return Err(ParseError::InputTooLarge {
132 size: input.len(),
133 max: crate::error::MAX_INPUT_SIZE,
134 });
135 }
136
137 let mut doc = DxDocument::new();
138 let mut parser = DsrParser::new(input);
139 parser.parse_document(&mut doc)?;
140
141 Ok(doc)
142 }
143
144 pub fn parse_bytes(input: &[u8]) -> Result<DxDocument, ParseError> {
146 if input.len() > crate::error::MAX_INPUT_SIZE {
147 return Err(ParseError::InputTooLarge {
148 size: input.len(),
149 max: crate::error::MAX_INPUT_SIZE,
150 });
151 }
152
153 let input_str = std::str::from_utf8(input).map_err(|e| ParseError::Utf8Error {
154 offset: e.valid_up_to(),
155 })?;
156 Self::parse(input_str)
157 }
158
159 pub fn parse_value(s: &str) -> DxLlmValue {
161 let s = s.trim();
162
163 if s == "true" {
165 return DxLlmValue::Bool(true);
166 }
167 if s == "false" {
168 return DxLlmValue::Bool(false);
169 }
170 if s == "null" {
171 return DxLlmValue::Null;
172 }
173
174 if let Ok(n) = s.parse::<i64>() {
176 return DxLlmValue::Num(n as f64);
177 }
178 if let Ok(n) = s.parse::<f64>() {
179 return DxLlmValue::Num(n);
180 }
181
182 if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
184 let unquoted = &s[1..s.len() - 1];
185 let unescaped = unquoted.replace("\\\"", "\"");
187 return DxLlmValue::Str(unescaped);
188 }
189
190 DxLlmValue::Str(s.to_string())
191 }
192
193 pub fn resolve_refs(doc: &DxDocument) -> Result<DxDocument, ParseError> {
197 let mut resolved = doc.clone();
198
199 for value in resolved.context.values_mut() {
201 Self::resolve_value(value, &doc.refs)?;
202 }
203
204 for section in resolved.sections.values_mut() {
206 for row in &mut section.rows {
207 for value in row {
208 Self::resolve_value(value, &doc.refs)?;
209 }
210 }
211 }
212
213 Ok(resolved)
214 }
215
216 fn resolve_value(
218 value: &mut DxLlmValue,
219 refs: &IndexMap<String, String>,
220 ) -> Result<(), ParseError> {
221 match value {
222 DxLlmValue::Ref(key) => {
223 if let Some(resolved) = refs.get(key) {
224 *value = DxLlmValue::Str(resolved.clone());
225 }
226 }
227 DxLlmValue::Arr(items) => {
228 for item in items {
229 Self::resolve_value(item, refs)?;
230 }
231 }
232 _ => {}
233 }
234 Ok(())
235 }
236}
237
238#[derive(Debug, Clone, Default)]
240struct PrefixInfo {
241 prefixes: Vec<String>,
242 suffixes: Vec<String>,
243}
244
245struct DsrParser<'a> {
247 input: &'a str,
248 pos: usize,
249}
250
251impl<'a> DsrParser<'a> {
252 fn new(input: &'a str) -> Self {
253 Self { input, pos: 0 }
254 }
255
256 fn parse_document(&mut self, doc: &mut DxDocument) -> Result<(), ParseError> {
257 self.skip_whitespace();
258
259 while self.pos < self.input.len() {
260 self.parse_statement(doc)?;
261 self.skip_whitespace();
262 }
263
264 Ok(())
265 }
266
267 fn section_id_for_name(&self, doc: &DxDocument, name: &str) -> char {
268 if let Some((section_id, _)) = doc
269 .section_names
270 .iter()
271 .find(|(_, section_name)| section_name.as_str() == name)
272 {
273 return *section_id;
274 }
275
276 for ch in name.chars().filter(|ch| ch.is_ascii_alphanumeric()) {
277 if !doc.sections.contains_key(&ch) {
278 return ch;
279 }
280 }
281
282 for ch in 'a'..='z' {
283 if !doc.sections.contains_key(&ch) {
284 return ch;
285 }
286 }
287 for ch in 'A'..='Z' {
288 if !doc.sections.contains_key(&ch) {
289 return ch;
290 }
291 }
292 for ch in '0'..='9' {
293 if !doc.sections.contains_key(&ch) {
294 return ch;
295 }
296 }
297
298 '_'
299 }
300
301 fn parse_statement(&mut self, doc: &mut DxDocument) -> Result<(), ParseError> {
302 self.skip_whitespace();
303 if self.pos >= self.input.len() {
304 return Ok(());
305 }
306
307 let name = self.parse_identifier()?;
309 if name.is_empty() {
310 let ch = self.current_char();
313 return Err(ParseError::UnexpectedChar { ch, pos: self.pos });
314 }
315
316 let mut parsed_section_id = None;
317
318 self.skip_whitespace();
319 if self.pos >= self.input.len() {
320 doc.context
322 .insert(name.clone(), DxLlmValue::Str(String::new()));
323 doc.entry_order
324 .push(crate::llm::types::EntryRef::Context(name));
325 return Ok(());
326 }
327
328 let ch = self.peek_char();
329
330 match ch {
331 Some('[') => {
332 let start_pos = self.pos;
338 self.advance(); let bracket_content = self.parse_until_char(']')?;
342 self.expect_char(']')?;
343
344 self.skip_whitespace();
345 let next_ch = self.peek_char();
346
347 if next_ch == Some('(') {
348 let schema: Vec<String> = bracket_content
350 .split_whitespace()
351 .map(|s| s.to_string())
352 .collect();
353
354 if schema.is_empty() {
355 return Err(ParseError::InvalidTable {
356 msg: "Empty schema in wrapped dataframe".to_string(),
357 });
358 }
359
360 self.advance(); let section = self.parse_wrapped_dataframe_rows(&schema)?;
363 self.expect_char(')')?;
364
365 let section_id = self.section_id_for_name(doc, &name);
366 doc.sections.insert(section_id, section);
367 doc.section_names.insert(section_id, name.clone());
368 parsed_section_id = Some(section_id);
369 } else {
370 self.pos = start_pos;
372
373 if let Some(_count) = self.try_parse_array_count() {
375 self.skip_whitespace();
377 let delimiter = self.peek_char();
378
379 if delimiter == Some(':') {
380 self.advance();
381 if self.peek_char() == Some(':') {
383 self.advance(); }
385 self.skip_whitespace();
386 let items_str = self.parse_until_delimiter(&['\n', '\r'])?;
387 let items: Vec<DxLlmValue> = items_str
388 .split(',')
389 .map(|s| LlmParser::parse_value(s.trim()))
390 .collect();
391 doc.context.insert(name.clone(), DxLlmValue::Arr(items));
392 } else if delimiter == Some('=') {
393 self.advance();
394 let items_str = self.parse_until_delimiter(&['\n', '\r'])?;
395 let items: Vec<DxLlmValue> = items_str
396 .split(',')
397 .map(|s| LlmParser::parse_value(s.trim()))
398 .collect();
399 doc.context.insert(name.clone(), DxLlmValue::Arr(items));
400 }
401 } else {
402 let obj = self.parse_inline_object(&name)?;
404 doc.context.insert(name.clone(), obj);
405 }
406 }
407 }
408 Some('(') => {
409 self.advance(); let obj = self.parse_parenthesized_object()?;
412 self.expect_char(')')?;
413 doc.context.insert(name.clone(), obj);
414 }
415 Some('=') => {
416 self.advance(); if self.peek_char() == Some('[') {
421 self.advance(); let items_str = self.parse_until_char(']')?;
423 self.expect_char(']')?;
424
425 let items = self.parse_quoted_items(&items_str);
427 doc.context.insert(name.clone(), DxLlmValue::Arr(items));
428 } else {
429 let value = self.parse_value_until_delimiter(&[',', '\n', '\r', ']'])?;
431 doc.context.insert(name.clone(), value);
432 }
433 }
434 Some(':') => {
435 self.advance();
436
437 let is_leaf = self.peek_char() == Some(':');
439 if is_leaf {
440 self.advance(); }
442
443 self.skip_whitespace();
444
445 let next = self.peek_char();
447
448 if !is_leaf && next == Some('(') {
449 let section = self.parse_table("")?;
451 let section_id = self.section_id_for_name(doc, &name);
452 doc.sections.insert(section_id, section);
453 doc.section_names.insert(section_id, name.clone());
454 parsed_section_id = Some(section_id);
455 } else if !is_leaf && next.map(|c| c.is_ascii_digit()).unwrap_or(false) {
456 let start_pos = self.pos;
462 let mut count_str = String::new();
463 while self.pos < self.input.len() {
464 let ch = self.current_char();
465 if ch.is_ascii_digit() {
466 count_str.push(ch);
467 self.advance();
468 } else {
469 break;
470 }
471 }
472
473 self.skip_whitespace();
474
475 if self.peek_char() == Some('(') {
476 let section = self.parse_table(&count_str)?;
478 let section_id = self.section_id_for_name(doc, &name);
479 doc.sections.insert(section_id, section);
480 doc.section_names.insert(section_id, name.clone());
481 parsed_section_id = Some(section_id);
482 } else if self.peek_char() == Some('@') {
483 let next_pos = self.pos + 1;
485 if next_pos < self.input.len() && self.input[next_pos..].starts_with('=') {
486 let count = count_str.parse::<usize>().unwrap_or(0);
488 let obj = self.parse_compact_object(&name, count)?;
489 doc.context.insert(name.clone(), obj);
490 } else {
491 self.pos = start_pos;
493 let value_str = self.parse_until_delimiter(&['\n', '\r'])?;
494 doc.context
495 .insert(name.clone(), LlmParser::parse_value(&value_str));
496 }
497 } else if self.peek_char() == Some('[') {
498 let obj = self.parse_inline_object(&name)?;
500 doc.context.insert(name.clone(), obj);
501 } else if self.peek_char() == Some('=') {
502 self.advance();
505 let items_str = self.parse_until_delimiter(&['\n', '\r'])?;
506
507 let items: Vec<DxLlmValue> = if items_str.contains(',') {
509 items_str
511 .split(',')
512 .map(|s| LlmParser::parse_value(s.trim()))
513 .collect()
514 } else {
515 items_str
517 .split_whitespace()
518 .map(LlmParser::parse_value)
519 .collect()
520 };
521 doc.context.insert(name.clone(), DxLlmValue::Arr(items));
522 } else {
523 self.pos = start_pos;
525 let value_str = self.parse_until_delimiter(&['\n', '\r'])?;
526 doc.context
527 .insert(name.clone(), LlmParser::parse_value(&value_str));
528 }
529 } else {
530 let value_str = self.parse_until_delimiter(&['\n', '\r'])?;
532 doc.context
533 .insert(name.clone(), LlmParser::parse_value(&value_str));
534 }
535 }
536 Some('|') => {
537 self.advance();
539 let value = self.parse_value_until_delimiter(&['\n', '\r'])?;
540 doc.context.insert(name.clone(), value);
541 }
542 _ => {
543 doc.context
545 .insert(name.clone(), DxLlmValue::Str(String::new()));
546 }
547 }
548
549 if doc.context.contains_key(&name) {
552 let entry_ref = crate::llm::types::EntryRef::Context(name.clone());
554 if !doc.entry_order.contains(&entry_ref) {
555 doc.entry_order.push(entry_ref);
556 }
557 } else {
558 if let Some(section_id) = parsed_section_id {
560 let entry_ref = crate::llm::types::EntryRef::Section(section_id);
561 if !doc.entry_order.contains(&entry_ref) {
562 doc.entry_order.push(entry_ref);
563 }
564 }
565 }
566
567 Ok(())
568 }
569
570 fn try_parse_array_count(&mut self) -> Option<usize> {
573 let start_pos = self.pos;
574
575 if self.peek_char() != Some('[') {
576 return None;
577 }
578 self.advance();
579
580 let mut num_str = String::new();
581 while self.pos < self.input.len() {
582 let ch = self.current_char();
583 if ch.is_ascii_digit() {
584 num_str.push(ch);
585 self.advance();
586 } else if ch == ']' {
587 self.advance();
588 if let Ok(count) = num_str.parse::<usize>() {
589 return Some(count);
590 }
591 break;
592 } else {
593 break;
594 }
595 }
596
597 self.pos = start_pos;
599 None
600 }
601
602 fn parse_inline_object(&mut self, _name: &str) -> Result<DxLlmValue, ParseError> {
603 let start_pos = self.pos;
604 self.expect_char('[')?;
605
606 let separator = self.detect_object_separator()?;
608
609 let mut fields: IndexMap<String, DxLlmValue> = IndexMap::new();
610 let mut iteration_count = 0;
611 const MAX_ITERATIONS: usize = 100_000; loop {
614 iteration_count += 1;
615 if iteration_count > MAX_ITERATIONS {
616 return Err(ParseError::InvalidTable {
617 msg: "Object parsing exceeded maximum iterations".to_string(),
618 });
619 }
620
621 self.skip_whitespace();
622
623 if self.peek_char() == Some(']') {
624 self.advance();
625 break;
626 }
627
628 if self.pos >= self.input.len() {
629 return Err(ParseError::UnclosedBracket { pos: start_pos });
630 }
631
632 let key = self.parse_identifier()?;
634 if key.is_empty() {
635 if self.peek_char() == Some(']') {
636 self.advance();
637 break;
638 }
639 let ch = self.current_char();
641 return Err(ParseError::UnexpectedChar { ch, pos: self.pos });
642 }
643
644 self.skip_whitespace();
645
646 if self.peek_char() == Some('=') {
647 self.advance();
648 let delimiters = if separator == ' ' {
650 vec![' ', ']', '\n']
651 } else {
652 vec![',', ']', '\n']
653 };
654 let value = self.parse_value_until_delimiter(&delimiters)?;
655 fields.insert(key, value);
656 } else if self.peek_char() == Some('[') {
657 if let Some(_count) = self.try_parse_array_count() {
659 self.skip_whitespace();
661 if self.peek_char() == Some('=') {
662 self.advance();
663 let items = self.parse_space_separated_items(separator)?;
664 fields.insert(key, DxLlmValue::Arr(items));
665 } else {
666 fields.insert(key, DxLlmValue::Null);
668 }
669 } else {
670 let nested = self.parse_inline_object(&key)?;
672 fields.insert(key, nested);
673 }
674 } else if self.peek_char() == Some(':') {
675 self.advance();
677 let rest = self.parse_until_delimiter(&['(', '=', ',', ']', '\n'])?;
678
679 if self.peek_char() == Some('(') {
680 let section = self.parse_table(&rest)?;
681 let arr = self.section_to_array(§ion);
683 fields.insert(key, arr);
684 } else if self.peek_char() == Some('=') {
685 self.advance();
686 let items_str = self.parse_until_delimiter(&[',', ']', '\n'])?;
687
688 let items: Vec<DxLlmValue> = if items_str.contains(',') {
690 items_str
692 .split(',')
693 .map(|s| LlmParser::parse_value(s.trim()))
694 .collect()
695 } else {
696 items_str
698 .split_whitespace()
699 .map(LlmParser::parse_value)
700 .collect()
701 };
702 fields.insert(key, DxLlmValue::Arr(items));
703 } else {
704 fields.insert(key, LlmParser::parse_value(&rest));
705 }
706 } else {
707 fields.insert(key, DxLlmValue::Null);
708 }
709
710 self.skip_whitespace();
712 if self.peek_char() == Some(separator) {
713 self.advance();
714 if separator == ' ' {
716 self.skip_whitespace();
717 }
718 }
719 }
720
721 if fields.is_empty() {
723 Ok(DxLlmValue::Null)
724 } else {
725 Ok(DxLlmValue::Obj(fields))
726 }
727 }
728
729 fn detect_object_separator(&self) -> Result<char, ParseError> {
732 let mut temp_pos = self.pos;
733 let mut depth = 0;
734 let mut after_value = false;
735
736 while temp_pos < self.input.len() {
737 let ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
738
739 match ch {
740 '[' | '(' => depth += 1,
741 ']' | ')' => {
742 if depth == 0 {
743 return Ok(' ');
745 }
746 depth -= 1;
747 }
748 '=' => {
749 after_value = true;
751 }
752 ',' if depth == 0 && after_value => {
753 return Ok(',');
755 }
756 ' ' if depth == 0 && after_value => {
757 let mut next_pos = temp_pos + ch.len_utf8();
760 while next_pos < self.input.len() {
761 let next_ch = self.input[next_pos..].chars().next().unwrap_or('\0');
762 if next_ch == ' ' || next_ch == '\t' {
763 next_pos += next_ch.len_utf8();
764 continue;
765 }
766 if next_ch.is_alphanumeric() || next_ch == '_' {
768 return Ok(' ');
769 }
770 break;
771 }
772 }
773 _ => {}
774 }
775
776 temp_pos += ch.len_utf8();
777 }
778
779 Ok(' ')
781 }
782
783 fn detect_row_separator(&self) -> Result<char, ParseError> {
787 let mut temp_pos = self.pos;
788 let mut depth = 0;
789
790 while temp_pos < self.input.len() {
791 let ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
792
793 match ch {
794 '[' | '(' => depth += 1,
795 ']' | ')' => {
796 if depth == 0 {
797 return Ok('\n');
799 }
800 depth -= 1;
801 }
802 ',' if depth == 0 => return Ok(','),
803 ';' if depth == 0 => return Ok(';'),
804 ':' if depth == 0 => return Ok(':'),
805 '\n' if depth == 0 => return Ok('\n'),
806 _ => {}
807 }
808
809 temp_pos += ch.len_utf8();
810 }
811
812 Ok('\n')
814 }
815
816 fn parse_space_separated_items(
820 &mut self,
821 separator: char,
822 ) -> Result<Vec<DxLlmValue>, ParseError> {
823 let mut items = Vec::new();
824 let mut current_item = String::new();
825 let mut in_nested: i32 = 0;
826
827 let item_separator = separator;
831
832 while self.pos < self.input.len() {
833 let ch = self.current_char();
834
835 match ch {
836 '[' | '(' => {
837 in_nested += 1;
838 current_item.push(ch);
839 self.advance();
840 }
841 ']' if in_nested == 0 => {
842 if !current_item.trim().is_empty() {
844 items.push(LlmParser::parse_value(current_item.trim()));
845 }
846 break;
847 }
848 ']' | ')' => {
849 in_nested = in_nested.saturating_sub(1);
850 current_item.push(ch);
851 self.advance();
852 }
853 '\n' | '\r' => {
854 if !current_item.trim().is_empty() {
856 items.push(LlmParser::parse_value(current_item.trim()));
857 }
858 break;
859 }
860 ' ' if item_separator == ' ' && in_nested == 0 => {
861 let mut temp_pos = self.pos + 1;
864
865 while temp_pos < self.input.len() {
867 let next_ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
868 if next_ch == ' ' || next_ch == '\t' {
869 temp_pos += next_ch.len_utf8();
870 } else {
871 break;
872 }
873 }
874
875 let mut has_equals = false;
877 let mut has_bracket = false;
878 let mut identifier_chars = 0;
879
880 while temp_pos < self.input.len() {
881 let next_ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
882 if next_ch.is_alphanumeric()
883 || next_ch == '_'
884 || next_ch == '-'
885 || next_ch == '.'
886 {
887 identifier_chars += 1;
888 temp_pos += next_ch.len_utf8();
889 } else if next_ch == '=' && identifier_chars > 0 {
890 has_equals = true;
891 break;
892 } else if next_ch == '[' && identifier_chars > 0 {
893 has_bracket = true;
894 break;
895 } else {
896 break;
897 }
898 }
899
900 if has_equals || has_bracket {
901 if !current_item.trim().is_empty() {
903 items.push(LlmParser::parse_value(current_item.trim()));
904 }
905 break;
906 } else {
907 if !current_item.trim().is_empty() {
909 items.push(LlmParser::parse_value(current_item.trim()));
910 current_item.clear();
911 }
912 self.advance();
913 while self.pos < self.input.len() && self.current_char() == ' ' {
915 self.advance();
916 }
917 }
918 }
919 ',' if item_separator == ',' && in_nested == 0 => {
920 let mut temp_pos = self.pos + 1;
923
924 while temp_pos < self.input.len() {
926 let next_ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
927 if next_ch == ' ' || next_ch == '\t' {
928 temp_pos += next_ch.len_utf8();
929 } else {
930 break;
931 }
932 }
933
934 let mut has_equals = false;
936 let mut has_bracket = false;
937 let mut identifier_chars = 0;
938
939 while temp_pos < self.input.len() {
940 let next_ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
941 if next_ch.is_alphanumeric()
942 || next_ch == '_'
943 || next_ch == '-'
944 || next_ch == '.'
945 {
946 identifier_chars += 1;
947 temp_pos += next_ch.len_utf8();
948 } else if next_ch == '=' && identifier_chars > 0 {
949 has_equals = true;
950 break;
951 } else if next_ch == '[' && identifier_chars > 0 {
952 has_bracket = true;
953 break;
954 } else {
955 break;
956 }
957 }
958
959 if has_equals || has_bracket {
960 if !current_item.trim().is_empty() {
962 items.push(LlmParser::parse_value(current_item.trim()));
963 }
964 break;
965 } else {
966 if !current_item.trim().is_empty() {
968 items.push(LlmParser::parse_value(current_item.trim()));
969 current_item.clear();
970 }
971 self.advance();
972 }
973 }
974 _ => {
975 current_item.push(ch);
976 self.advance();
977 }
978 }
979 }
980
981 Ok(items)
982 }
983
984 fn parse_compact_object(
988 &mut self,
989 _name: &str,
990 _count: usize,
991 ) -> Result<DxLlmValue, ParseError> {
992 let start_pos = self.pos;
993
994 self.expect_char('@')?;
996 self.expect_char('=')?;
997 self.expect_char('[')?;
998
999 let mut fields = IndexMap::new();
1000 let mut tokens = Vec::new();
1001
1002 loop {
1004 self.skip_whitespace();
1005
1006 if self.peek_char() == Some(']') {
1007 self.advance();
1008 break;
1009 }
1010
1011 if self.pos >= self.input.len() {
1012 return Err(ParseError::UnclosedBracket { pos: start_pos });
1013 }
1014
1015 let token = self.parse_identifier()?;
1016 if token.is_empty() {
1017 break;
1018 }
1019 tokens.push(token);
1020 }
1021
1022 if tokens.len() % 2 != 0 {
1024 return Err(ParseError::InvalidTable {
1025 msg: format!(
1026 "Compact syntax requires even number of tokens, got {}",
1027 tokens.len()
1028 ),
1029 });
1030 }
1031
1032 for chunk in tokens.chunks(2) {
1033 let key = chunk[0].clone();
1034 let value = LlmParser::parse_value(&chunk[1]);
1035 fields.insert(key, value);
1036 }
1037
1038 Ok(DxLlmValue::Obj(fields))
1039 }
1040
1041 fn parse_table(&mut self, count_str: &str) -> Result<DxSection, ParseError> {
1042 let start_pos = self.pos;
1043
1044 let _count: usize = count_str.trim().parse().unwrap_or(0);
1046
1047 self.expect_char('(')?;
1050 let schema_str = self.parse_until_char(')')?;
1051 self.expect_char(')')?;
1052
1053 let schema: Vec<String> = if schema_str.contains(',') {
1055 schema_str
1056 .split(',')
1057 .map(|s| s.trim().to_string())
1058 .filter(|s| !s.is_empty())
1059 .collect()
1060 } else {
1061 schema_str
1062 .split_whitespace()
1063 .map(|s| s.to_string())
1064 .collect()
1065 };
1066
1067 if schema.is_empty() {
1068 return Err(ParseError::InvalidTable {
1069 msg: "Empty schema".to_string(),
1070 });
1071 }
1072
1073 let prefix_info = self.parse_prefix_markers()?;
1075
1076 self.skip_whitespace();
1078 self.expect_char('[')?;
1079
1080 let row_separator = self.detect_row_separator()?;
1082
1083 let mut section = DxSection::new(schema.clone());
1084
1085 match row_separator {
1087 '\n' => {
1088 let mut iteration_count = 0;
1090 const MAX_ITERATIONS: usize = 10_000_000; loop {
1094 iteration_count += 1;
1095 if iteration_count > MAX_ITERATIONS {
1096 return Err(ParseError::InvalidTable {
1097 msg: "Table parsing exceeded maximum iterations".to_string(),
1098 });
1099 }
1100
1101 self.skip_whitespace_no_newline();
1102
1103 if self.peek_char() == Some(']') {
1104 self.advance();
1105 break;
1106 }
1107
1108 if self.pos >= self.input.len() {
1109 return Err(ParseError::UnclosedBracket { pos: start_pos });
1110 }
1111
1112 if self.peek_char() == Some('\n') || self.peek_char() == Some('\r') {
1114 self.advance();
1115 continue;
1116 }
1117
1118 let mut row = self.parse_table_row(schema.len())?;
1120 if !row.is_empty() {
1121 if row.len() != schema.len() {
1122 return Err(ParseError::SchemaMismatch {
1123 expected: schema.len(),
1124 got: row.len(),
1125 });
1126 }
1127 self.apply_prefixes_to_row(&mut row, &prefix_info, &schema);
1129 section.rows.push(row);
1130 }
1131 }
1132 }
1133 ',' | ';' | ':' => {
1134 self.parse_inline_separated_rows(
1136 &mut section,
1137 &schema,
1138 row_separator,
1139 &prefix_info,
1140 )?;
1141 self.expect_char(']')?;
1142 }
1143 _ => {
1144 return Err(ParseError::InvalidTable {
1145 msg: format!("Unknown row separator: {}", row_separator),
1146 });
1147 }
1148 }
1149
1150 Ok(section)
1151 }
1152
1153 fn parse_inline_separated_rows(
1157 &mut self,
1158 section: &mut DxSection,
1159 schema: &[String],
1160 separator: char,
1161 prefix_info: &PrefixInfo,
1162 ) -> Result<(), ParseError> {
1163 let mut iteration_count = 0;
1164 const MAX_ITERATIONS: usize = 10_000_000; loop {
1167 iteration_count += 1;
1168 if iteration_count > MAX_ITERATIONS {
1169 return Err(ParseError::InvalidTable {
1170 msg: "Inline row parsing exceeded maximum iterations".to_string(),
1171 });
1172 }
1173
1174 self.skip_whitespace_no_newline();
1175
1176 if self.peek_char() == Some(']') {
1177 break;
1178 }
1179
1180 if self.pos >= self.input.len() {
1181 return Err(ParseError::UnexpectedEof);
1182 }
1183
1184 let mut row = self.parse_inline_row(schema.len(), separator)?;
1186 if !row.is_empty() {
1187 if row.len() != schema.len() {
1188 return Err(ParseError::SchemaMismatch {
1189 expected: schema.len(),
1190 got: row.len(),
1191 });
1192 }
1193 self.apply_prefixes_to_row(&mut row, prefix_info, schema);
1195 section.rows.push(row);
1196 }
1197
1198 self.skip_whitespace_no_newline();
1200 if self.peek_char() == Some(separator) {
1201 self.advance();
1202 self.skip_whitespace_no_newline();
1203 }
1204 }
1205
1206 Ok(())
1207 }
1208
1209 fn parse_inline_row(
1212 &mut self,
1213 expected_cols: usize,
1214 row_separator: char,
1215 ) -> Result<Vec<DxLlmValue>, ParseError> {
1216 let mut values = Vec::with_capacity(expected_cols);
1217 let mut current_value = String::new();
1218 let mut in_nested: i32 = 0;
1219
1220 while self.pos < self.input.len() {
1221 let ch = self.current_char();
1222
1223 match ch {
1224 '[' | '(' => {
1225 in_nested += 1;
1226 current_value.push(ch);
1227 self.advance();
1228 }
1229 ']' if in_nested == 0 => {
1230 if !current_value.trim().is_empty() {
1232 values.push(self.parse_table_value(¤t_value, ' '));
1233 }
1234 break;
1235 }
1236 ']' | ')' => {
1237 in_nested = in_nested.saturating_sub(1);
1238 current_value.push(ch);
1239 self.advance();
1240 }
1241 c if c == row_separator && in_nested == 0 => {
1242 if !current_value.trim().is_empty() {
1244 values.push(self.parse_table_value(¤t_value, ' '));
1245 }
1246 break;
1247 }
1248 ' ' if in_nested == 0 => {
1249 if !current_value.trim().is_empty() {
1251 values.push(self.parse_table_value(¤t_value, ' '));
1252 current_value.clear();
1253 }
1254 self.advance();
1255 while self.pos < self.input.len() && self.current_char() == ' ' {
1257 self.advance();
1258 }
1259 }
1260 '\n' | '\r' => {
1261 if !current_value.trim().is_empty() {
1263 values.push(self.parse_table_value(¤t_value, ' '));
1264 }
1265 break;
1266 }
1267 _ => {
1268 current_value.push(ch);
1269 self.advance();
1270 }
1271 }
1272 }
1273
1274 Ok(values)
1275 }
1276
1277 fn parse_table_row(&mut self, expected_cols: usize) -> Result<Vec<DxLlmValue>, ParseError> {
1278 let mut values = Vec::with_capacity(expected_cols);
1279 let mut current_value = String::new();
1280 let mut in_parens: i32 = 0;
1281
1282 let _line_start = self.pos;
1285 let mut has_comma = false;
1286 let mut temp_pos = self.pos;
1287 while temp_pos < self.input.len() {
1288 let ch = self.input[temp_pos..].chars().next().unwrap_or('\0');
1289 if ch == '\n' || ch == '\r' || ch == ']' {
1290 break;
1291 }
1292 if ch == ',' {
1293 has_comma = true;
1294 break;
1295 }
1296 temp_pos += ch.len_utf8();
1297 }
1298
1299 let separator = if has_comma { ',' } else { ' ' };
1300
1301 while self.pos < self.input.len() {
1302 let ch = self.current_char();
1303
1304 match ch {
1305 '\n' | '\r' => {
1306 if !current_value.is_empty() || !values.is_empty() {
1308 values.push(self.parse_table_value(¤t_value, separator));
1309 }
1310 self.advance();
1311 break;
1312 }
1313 ']' if in_parens == 0 => {
1314 if !current_value.is_empty() || !values.is_empty() {
1316 values.push(self.parse_table_value(¤t_value, separator));
1317 }
1318 break;
1319 }
1320 c if c == separator && in_parens == 0 => {
1321 values.push(self.parse_table_value(¤t_value, separator));
1322 current_value.clear();
1323 self.advance();
1324 if separator == ' ' {
1326 while self.pos < self.input.len() && self.current_char() == ' ' {
1327 self.advance();
1328 }
1329 }
1330 }
1331 '(' => {
1332 in_parens += 1;
1333 current_value.push(ch);
1334 self.advance();
1335 }
1336 ')' => {
1337 in_parens = in_parens.saturating_sub(1);
1338 current_value.push(ch);
1339 self.advance();
1340 }
1341 _ => {
1342 current_value.push(ch);
1343 self.advance();
1344 }
1345 }
1346 }
1347
1348 Ok(values)
1349 }
1350
1351 fn parse_table_value(&self, s: &str, _separator: char) -> DxLlmValue {
1353 let s = s.trim();
1354
1355 if s == "true" {
1357 return DxLlmValue::Bool(true);
1358 }
1359 if s == "false" {
1360 return DxLlmValue::Bool(false);
1361 }
1362 if s == "null" {
1363 return DxLlmValue::Null;
1364 }
1365
1366 if let Ok(n) = s.parse::<i64>() {
1368 return DxLlmValue::Num(n as f64);
1369 }
1370 if let Ok(n) = s.parse::<f64>() {
1371 return DxLlmValue::Num(n);
1372 }
1373
1374 DxLlmValue::Str(s.to_string())
1375 }
1376
1377 fn parse_prefix_markers(&mut self) -> Result<PrefixInfo, ParseError> {
1382 let mut prefixes = Vec::new();
1383 let mut suffixes = Vec::new();
1384
1385 loop {
1386 self.skip_whitespace_no_newline();
1387
1388 if self.peek_char() != Some('@') {
1390 break;
1391 }
1392
1393 self.advance(); let is_suffix = if self.peek_char() == Some('@') {
1397 self.advance(); true
1399 } else {
1400 false
1401 };
1402
1403 let mut value = String::new();
1406 while self.pos < self.input.len() {
1407 let ch = self.current_char();
1408 if ch == '[' || ch == ' ' || ch == '\n' || ch == '\r' {
1409 break;
1410 }
1411 value.push(ch);
1412 self.advance();
1413 }
1414
1415 if !value.is_empty() {
1416 if is_suffix {
1417 suffixes.push(value);
1418 } else {
1419 prefixes.push(value);
1420 }
1421 }
1422 }
1423
1424 Ok(PrefixInfo { prefixes, suffixes })
1425 }
1426
1427 fn apply_prefixes_to_row(
1431 &self,
1432 row: &mut [DxLlmValue],
1433 prefix_info: &PrefixInfo,
1434 schema: &[String],
1435 ) {
1436 if !prefix_info.prefixes.is_empty() {
1438 for (i, value) in row.iter_mut().enumerate() {
1439 if let DxLlmValue::Str(s) = value {
1440 let col_name = schema.get(i).map(|s| s.as_str()).unwrap_or("");
1441 let col_lower = col_name.to_lowercase();
1442
1443 if col_lower.contains("endpoint")
1446 || col_lower.contains("path")
1447 || col_lower.contains("url")
1448 || col_lower.contains("route")
1449 || col_lower.contains("uri")
1450 || col_lower.contains("file")
1451 || col_lower.contains("directory")
1452 || col_lower.contains("dir")
1453 {
1454 *s = format!("{}{}", prefix_info.prefixes[0], s);
1456 }
1457 }
1458 }
1459 }
1460
1461 if !prefix_info.suffixes.is_empty() {
1463 for (i, value) in row.iter_mut().enumerate() {
1464 if let DxLlmValue::Str(s) = value {
1465 let col_name = schema.get(i).map(|s| s.as_str()).unwrap_or("");
1466 let col_lower = col_name.to_lowercase();
1467
1468 if col_lower.contains("email")
1471 || col_lower.contains("domain")
1472 || col_lower.contains("host")
1473 {
1474 *s = format!("{}{}", s, prefix_info.suffixes[0]);
1476 }
1477 }
1478 }
1479 }
1480 }
1481
1482 fn parse_wrapped_dataframe_rows(&mut self, schema: &[String]) -> Result<DxSection, ParseError> {
1485 let mut section = DxSection::new(schema.to_vec());
1486
1487 loop {
1488 self.skip_whitespace();
1489
1490 if self.peek_char() == Some(')') {
1492 break;
1493 }
1494
1495 if self.pos >= self.input.len() {
1496 return Err(ParseError::UnexpectedEof);
1497 }
1498
1499 let row = self.parse_wrapped_row(schema.len())?;
1501 if !row.is_empty() {
1502 if row.len() != schema.len() {
1503 return Err(ParseError::SchemaMismatch {
1504 expected: schema.len(),
1505 got: row.len(),
1506 });
1507 }
1508 section.rows.push(row);
1509 }
1510 }
1511
1512 Ok(section)
1513 }
1514
1515 fn parse_wrapped_row(&mut self, expected_cols: usize) -> Result<Vec<DxLlmValue>, ParseError> {
1517 let mut values = Vec::with_capacity(expected_cols);
1518 let mut current_value = String::new();
1519 let mut in_quotes = false;
1520 let mut escape_next = false;
1521
1522 while self.pos < self.input.len() {
1523 let ch = self.current_char();
1524
1525 if escape_next {
1526 current_value.push(ch);
1527 escape_next = false;
1528 self.advance();
1529 continue;
1530 }
1531
1532 match ch {
1533 '\\' if in_quotes => {
1534 escape_next = true;
1535 self.advance();
1536 }
1537 '"' => {
1538 in_quotes = !in_quotes;
1539 current_value.push(ch);
1540 self.advance();
1541 }
1542 ' ' if !in_quotes => {
1543 if !current_value.trim().is_empty() {
1545 values.push(LlmParser::parse_value(current_value.trim()));
1546 current_value.clear();
1547 }
1548 self.advance();
1549 }
1550 '\n' | '\r' if !in_quotes => {
1551 if !current_value.trim().is_empty() {
1553 values.push(LlmParser::parse_value(current_value.trim()));
1554 }
1555 self.advance();
1556 break;
1557 }
1558 ')' if !in_quotes => {
1559 if !current_value.trim().is_empty() {
1561 values.push(LlmParser::parse_value(current_value.trim()));
1562 }
1563 break;
1564 }
1565 _ => {
1566 current_value.push(ch);
1567 self.advance();
1568 }
1569 }
1570 }
1571
1572 Ok(values)
1573 }
1574
1575 fn parse_parenthesized_object(&mut self) -> Result<DxLlmValue, ParseError> {
1577 let mut fields = IndexMap::new();
1578
1579 loop {
1580 self.skip_whitespace();
1581
1582 if self.peek_char() == Some(')') {
1583 break;
1584 }
1585
1586 if self.pos >= self.input.len() {
1587 return Err(ParseError::UnexpectedEof);
1588 }
1589
1590 let key = self.parse_identifier()?;
1592 if key.is_empty() {
1593 break;
1594 }
1595
1596 self.skip_whitespace();
1597
1598 if self.peek_char() != Some('=') {
1600 return Err(ParseError::UnexpectedChar {
1601 ch: self.current_char(),
1602 pos: self.pos,
1603 });
1604 }
1605 self.advance();
1606
1607 if self.peek_char() == Some('[') {
1610 self.advance(); let items_str = self.parse_until_char(']')?;
1612 self.expect_char(']')?;
1613 let items = self.parse_quoted_items(&items_str);
1614 fields.insert(key, DxLlmValue::Arr(items));
1615 } else {
1616 let value_str = self.parse_until_delimiter(&[' ', ')', '\n'])?;
1617 fields.insert(key, LlmParser::parse_value(&value_str));
1618 }
1619
1620 self.skip_whitespace();
1621 }
1622
1623 Ok(DxLlmValue::Obj(fields))
1624 }
1625
1626 fn parse_quoted_items(&self, input: &str) -> Vec<DxLlmValue> {
1628 let mut items = Vec::new();
1629 let mut current = String::new();
1630 let mut in_quotes = false;
1631 let mut escape_next = false;
1632
1633 for ch in input.chars() {
1634 if escape_next {
1635 current.push(ch);
1636 escape_next = false;
1637 continue;
1638 }
1639
1640 match ch {
1641 '\\' if in_quotes => {
1642 escape_next = true;
1643 }
1644 '"' => {
1645 in_quotes = !in_quotes;
1646 current.push(ch);
1647 }
1648 ' ' | ',' if !in_quotes => {
1649 if !current.trim().is_empty() {
1650 items.push(LlmParser::parse_value(current.trim()));
1651 current.clear();
1652 }
1653 }
1654 _ => {
1655 current.push(ch);
1656 }
1657 }
1658 }
1659
1660 if !current.trim().is_empty() {
1661 items.push(LlmParser::parse_value(current.trim()));
1662 }
1663
1664 items
1665 }
1666
1667 fn parse_identifier(&mut self) -> Result<String, ParseError> {
1668 let mut name = String::new();
1669
1670 while self.pos < self.input.len() {
1671 let ch = self.current_char();
1672 if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '.' || ch == '@' {
1673 name.push(ch);
1674 self.advance();
1675 } else {
1676 break;
1677 }
1678 }
1679
1680 Ok(name)
1681 }
1682
1683 fn parse_value_until_delimiter(
1684 &mut self,
1685 delimiters: &[char],
1686 ) -> Result<DxLlmValue, ParseError> {
1687 let value_str = self.parse_until_delimiter(delimiters)?;
1688 Ok(LlmParser::parse_value(&value_str))
1689 }
1690
1691 fn parse_until_delimiter(&mut self, delimiters: &[char]) -> Result<String, ParseError> {
1692 let mut value = String::new();
1693 let mut in_nested = 0;
1694
1695 while self.pos < self.input.len() {
1696 let ch = self.current_char();
1697
1698 if ch == '[' || ch == '(' {
1699 in_nested += 1;
1700 value.push(ch);
1701 self.advance();
1702 } else if ch == ']' || ch == ')' {
1703 if in_nested > 0 {
1704 in_nested -= 1;
1705 value.push(ch);
1706 self.advance();
1707 } else if delimiters.contains(&ch) {
1708 break;
1709 } else {
1710 value.push(ch);
1711 self.advance();
1712 }
1713 } else if delimiters.contains(&ch) && in_nested == 0 {
1714 break;
1715 } else {
1716 value.push(ch);
1717 self.advance();
1718 }
1719 }
1720
1721 Ok(value.trim().to_string())
1722 }
1723
1724 fn parse_until_char(&mut self, target: char) -> Result<String, ParseError> {
1725 let mut value = String::new();
1726 let mut depth = 0;
1727 let mut in_quotes = false;
1728 let mut escape_next = false;
1729
1730 while self.pos < self.input.len() {
1731 let ch = self.current_char();
1732
1733 if escape_next {
1734 value.push(ch);
1735 escape_next = false;
1736 self.advance();
1737 } else if ch == '\\' && in_quotes {
1738 value.push(ch);
1739 escape_next = true;
1740 self.advance();
1741 } else if ch == '"' {
1742 in_quotes = !in_quotes;
1743 value.push(ch);
1744 self.advance();
1745 } else if !in_quotes && ch == '(' {
1746 depth += 1;
1747 value.push(ch);
1748 self.advance();
1749 } else if !in_quotes && ch == ')' {
1750 if depth > 0 {
1751 depth -= 1;
1752 value.push(ch);
1753 self.advance();
1754 } else if ch == target {
1755 break;
1757 } else {
1758 value.push(ch);
1759 self.advance();
1760 }
1761 } else if !in_quotes && ch == target && depth == 0 {
1762 break;
1763 } else {
1764 value.push(ch);
1765 self.advance();
1766 }
1767 }
1768
1769 Ok(value)
1770 }
1771
1772 fn expect_char(&mut self, expected: char) -> Result<(), ParseError> {
1773 self.skip_whitespace();
1774 if self.pos >= self.input.len() {
1775 return Err(ParseError::UnexpectedEof);
1776 }
1777 let ch = self.current_char();
1778 if ch != expected {
1779 return Err(ParseError::UnexpectedChar { ch, pos: self.pos });
1780 }
1781 self.advance();
1782 Ok(())
1783 }
1784
1785 fn current_char(&self) -> char {
1786 self.input[self.pos..].chars().next().unwrap_or('\0')
1787 }
1788
1789 fn peek_char(&self) -> Option<char> {
1790 self.input[self.pos..].chars().next()
1791 }
1792
1793 fn advance(&mut self) {
1794 if self.pos < self.input.len() {
1795 self.pos += self.current_char().len_utf8();
1796 }
1797 }
1798
1799 fn skip_whitespace(&mut self) {
1800 loop {
1801 while self.pos < self.input.len() {
1802 let ch = self.current_char();
1803 if ch.is_whitespace() {
1804 self.advance();
1805 } else {
1806 break;
1807 }
1808 }
1809
1810 if self.pos >= self.input.len() {
1811 break;
1812 }
1813
1814 let rest = &self.input[self.pos..];
1815 if rest.starts_with('#') || rest.starts_with("//") {
1816 self.skip_line_comment();
1817 continue;
1818 }
1819
1820 break;
1821 }
1822 }
1823
1824 fn skip_whitespace_no_newline(&mut self) {
1825 while self.pos < self.input.len() {
1826 let ch = self.current_char();
1827 if ch == ' ' || ch == '\t' {
1828 self.advance();
1829 } else {
1830 break;
1831 }
1832 }
1833 }
1834
1835 fn skip_line_comment(&mut self) {
1836 while self.pos < self.input.len() {
1837 let ch = self.current_char();
1838 self.advance();
1839 if ch == '\n' || ch == '\r' {
1840 break;
1841 }
1842 }
1843 }
1844
1845 fn section_to_array(&self, section: &DxSection) -> DxLlmValue {
1846 let mut arr = Vec::new();
1847 for row in §ion.rows {
1848 let row_str = row
1850 .iter()
1851 .map(Self::value_to_string)
1852 .collect::<Vec<_>>()
1853 .join(",");
1854 arr.push(DxLlmValue::Str(row_str));
1855 }
1856 DxLlmValue::Arr(arr)
1857 }
1858
1859 fn value_to_string(v: &DxLlmValue) -> String {
1860 match v {
1861 DxLlmValue::Str(s) => s.clone(),
1862 DxLlmValue::Num(n) => {
1863 if n.fract() == 0.0 {
1864 format!("{}", *n as i64)
1865 } else {
1866 format!("{}", n)
1867 }
1868 }
1869 DxLlmValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1870 DxLlmValue::Null => "null".to_string(),
1871 DxLlmValue::Arr(items) => {
1872 let s: Vec<String> = items.iter().map(Self::value_to_string).collect();
1873 s.join(",")
1874 }
1875 DxLlmValue::Obj(fields) => {
1876 let s: Vec<String> = fields
1877 .iter()
1878 .map(|(k, v)| format!("{}={}", k, Self::value_to_string(v)))
1879 .collect();
1880 format!("[{}]", s.join(","))
1881 }
1882 DxLlmValue::Ref(r) => format!("^{}", r),
1883 }
1884 }
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889 use super::*;
1890
1891 #[test]
1892 fn test_parse_empty() {
1893 let doc = LlmParser::parse("").unwrap();
1894 assert!(doc.is_empty());
1895 }
1896
1897 #[test]
1898 fn test_parse_simple_key_value() {
1899 let input = "environment: development\nversion: 2.2.16";
1900 let doc = LlmParser::parse(input).unwrap();
1901
1902 assert_eq!(doc.context.len(), 2);
1903 assert_eq!(
1904 doc.context.get("environment").unwrap().as_str(),
1905 Some("development")
1906 );
1907 assert_eq!(doc.context.get("version").unwrap().as_str(), Some("2.2.16"));
1908 }
1909
1910 #[test]
1911 fn test_parse_full_line_comments() {
1912 let input = r#"
1913# DX ecosystem config
1914project.name="dx-www"
1915
1916// Style and icon tools live in the same extensionless dx file.
1917tooling.dx_style.mode="generated-css"
1918tooling.icons.generated_dir="components/icons"
1919"#;
1920 let doc = LlmParser::parse(input).unwrap();
1921
1922 assert_eq!(doc.context.len(), 3);
1923 assert_eq!(
1924 doc.context.get("project.name").unwrap().as_str(),
1925 Some("dx-www")
1926 );
1927 assert_eq!(
1928 doc.context.get("tooling.dx_style.mode").unwrap().as_str(),
1929 Some("generated-css")
1930 );
1931 assert_eq!(
1932 doc.context
1933 .get("tooling.icons.generated_dir")
1934 .unwrap()
1935 .as_str(),
1936 Some("components/icons")
1937 );
1938 }
1939
1940 #[test]
1941 fn test_parse_wrapped_dataframe_tables_with_same_initial() {
1942 let input = r#"
1943protected_crates[name](
1944dx-www-browser
1945dx-serializer
1946)
1947
1948paths[name value](
1949ui components/ui
1950styles styles
1951)
1952
1953packages[id version source surfaces](
1954"shadcn/ui/button" 0.1.0 forge "button docs"
1955)
1956"#;
1957 let doc = LlmParser::parse(input).unwrap();
1958
1959 assert_eq!(doc.sections.len(), 3);
1960 assert!(
1961 doc.section_names
1962 .values()
1963 .any(|name| name == "protected_crates")
1964 );
1965 assert!(doc.section_names.values().any(|name| name == "paths"));
1966 assert!(doc.section_names.values().any(|name| name == "packages"));
1967
1968 let packages_id = doc
1969 .section_names
1970 .iter()
1971 .find_map(|(id, name)| (name == "packages").then_some(*id))
1972 .unwrap();
1973 let packages = doc.sections.get(&packages_id).unwrap();
1974 assert_eq!(packages.schema, vec!["id", "version", "source", "surfaces"]);
1975 assert_eq!(packages.rows.len(), 1);
1976 assert_eq!(packages.rows[0][0].as_str(), Some("shadcn/ui/button"));
1977 }
1978
1979 #[test]
1980 fn test_parse_leaf_inlining() {
1981 let input = "forge.repository:: https://dx.vercel.app/user/repo\nstyle.path:: @/style";
1983 let doc = LlmParser::parse(input).unwrap();
1984
1985 assert_eq!(doc.context.len(), 2);
1986 assert_eq!(
1987 doc.context.get("forge.repository").unwrap().as_str(),
1988 Some("https://dx.vercel.app/user/repo")
1989 );
1990 assert_eq!(
1991 doc.context.get("style.path").unwrap().as_str(),
1992 Some("@/style")
1993 );
1994 }
1995
1996 #[test]
1997 fn test_parse_mixed_standard_and_leaf() {
1998 let input = "name: dx\nversion: 0.0.1\nforge.repository:: https://example.com\neditors.default: neovim";
1999 let doc = LlmParser::parse(input).unwrap();
2000
2001 assert_eq!(doc.context.len(), 4);
2002 assert_eq!(doc.context.get("name").unwrap().as_str(), Some("dx"));
2003 assert_eq!(doc.context.get("version").unwrap().as_str(), Some("0.0.1"));
2004 assert_eq!(
2005 doc.context.get("forge.repository").unwrap().as_str(),
2006 Some("https://example.com")
2007 );
2008 assert_eq!(
2009 doc.context.get("editors.default").unwrap().as_str(),
2010 Some("neovim")
2011 );
2012 }
2013
2014 #[test]
2015 fn test_parse_array_with_count() {
2016 let input = "editors.items[3]: neovim,zed,vscode";
2017 let doc = LlmParser::parse(input).unwrap();
2018
2019 let items = doc.context.get("editors.items").unwrap();
2020 if let DxLlmValue::Arr(arr) = items {
2021 assert_eq!(arr.len(), 3);
2022 assert_eq!(arr[0].as_str(), Some("neovim"));
2023 assert_eq!(arr[1].as_str(), Some("zed"));
2024 assert_eq!(arr[2].as_str(), Some("vscode"));
2025 } else {
2026 panic!("Expected array");
2027 }
2028 }
2029
2030 #[test]
2031 fn test_parse_object() {
2032 let input = "config[host=localhost,port=8080,debug=true]";
2033 let doc = LlmParser::parse(input).unwrap();
2034
2035 assert!(doc.context.contains_key("config"));
2036 let config = doc.context.get("config").unwrap();
2037
2038 if let DxLlmValue::Obj(fields) = config {
2040 assert_eq!(fields.get("host").unwrap().as_str(), Some("localhost"));
2041 assert_eq!(fields.get("port").unwrap().as_num(), Some(8080.0));
2042 assert_eq!(fields.get("debug").unwrap().as_bool(), Some(true));
2043 } else {
2044 panic!("Expected Obj variant, got {:?}", config);
2045 }
2046 }
2047
2048 #[test]
2049 fn test_parse_table() {
2050 let input = "metrics:3(date views clicks)[\n2025-01-01 4836 193\n2025-01-02 6525 196\n2025-01-03 7927 238]";
2052 let doc = LlmParser::parse(input).unwrap();
2053
2054 assert_eq!(doc.sections.len(), 1);
2055 let section = doc.sections.values().next().unwrap();
2056 assert_eq!(section.schema, vec!["date", "views", "clicks"]);
2057 assert_eq!(section.rows.len(), 3);
2058 }
2059
2060 #[test]
2061 fn test_parse_table_comma_separated() {
2062 let input = "metrics:3(date,views,clicks)[\n2025-01-01,4836,193\n2025-01-02,6525,196\n2025-01-03,7927,238]";
2064 let doc = LlmParser::parse(input).unwrap();
2065
2066 assert_eq!(doc.sections.len(), 1);
2067 let section = doc.sections.values().next().unwrap();
2068 assert_eq!(section.schema, vec!["date", "views", "clicks"]);
2069 assert_eq!(section.rows.len(), 3);
2070 }
2071
2072 #[test]
2073 fn test_parse_table_with_quoted_names() {
2074 let input =
2076 "employees[id name dept](\n1 \"James Smith\" Engineering\n2 \"Mary Johnson\" Sales\n)";
2077 let doc = LlmParser::parse(input).unwrap();
2078
2079 assert_eq!(doc.sections.len(), 1);
2080 let section = doc.sections.values().next().unwrap();
2081 assert_eq!(section.schema, vec!["id", "name", "dept"]);
2082 assert_eq!(section.rows.len(), 2);
2083
2084 assert_eq!(section.rows[0][1].as_str(), Some("James Smith"));
2086 assert_eq!(section.rows[1][1].as_str(), Some("Mary Johnson"));
2087 }
2088
2089 #[test]
2090 fn test_parse_simple_array() {
2091 let input = "friends:3=ana,luis,sam";
2092 let doc = LlmParser::parse(input).unwrap();
2093
2094 let friends = doc.context.get("friends").unwrap();
2095 if let DxLlmValue::Arr(items) = friends {
2096 assert_eq!(items.len(), 3);
2097 assert_eq!(items[0].as_str(), Some("ana"));
2098 assert_eq!(items[1].as_str(), Some("luis"));
2099 assert_eq!(items[2].as_str(), Some("sam"));
2100 } else {
2101 panic!("Expected array");
2102 }
2103 }
2104
2105 #[test]
2106 fn test_parse_toml_style_quoted_array_values() {
2107 let input = r#"routes=["/docs", "/docs/[[...slug]]", "/docs/readiness"]
2108empty=[]"#;
2109 let doc = LlmParser::parse(input).unwrap();
2110
2111 let routes = doc.context.get("routes").unwrap();
2112 if let DxLlmValue::Arr(items) = routes {
2113 assert_eq!(items.len(), 3);
2114 assert_eq!(items[0].as_str(), Some("/docs"));
2115 assert_eq!(items[1].as_str(), Some("/docs/[[...slug]]"));
2116 assert_eq!(items[2].as_str(), Some("/docs/readiness"));
2117 } else {
2118 panic!("Expected routes array");
2119 }
2120
2121 let empty = doc.context.get("empty").unwrap();
2122 if let DxLlmValue::Arr(items) = empty {
2123 assert!(items.is_empty());
2124 } else {
2125 panic!("Expected empty array");
2126 }
2127 }
2128
2129 #[test]
2130 fn test_parse_inline_object_with_count() {
2131 let input = "config:3[host=localhost port=8080 debug=true]";
2133 let doc = LlmParser::parse(input).unwrap();
2134
2135 assert!(doc.context.contains_key("config"));
2136 let config = doc.context.get("config").unwrap();
2137
2138 if let DxLlmValue::Obj(fields) = config {
2140 assert_eq!(fields.len(), 3);
2141 assert_eq!(fields.get("host").unwrap().as_str(), Some("localhost"));
2142 assert_eq!(fields.get("port").unwrap().as_num(), Some(8080.0));
2143 assert_eq!(fields.get("debug").unwrap().as_bool(), Some(true));
2144 } else {
2145 panic!("Expected Obj variant, got {:?}", config);
2146 }
2147 }
2148
2149 #[test]
2150 fn test_parse_inline_object_with_count_comma_separated() {
2151 let input = "config:3[host=localhost,port=8080,debug=true]";
2153 let doc = LlmParser::parse(input).unwrap();
2154
2155 assert!(doc.context.contains_key("config"));
2156 let config = doc.context.get("config").unwrap();
2157
2158 if let DxLlmValue::Obj(fields) = config {
2159 assert_eq!(fields.len(), 3);
2160 assert_eq!(fields.get("host").unwrap().as_str(), Some("localhost"));
2161 assert_eq!(fields.get("port").unwrap().as_num(), Some(8080.0));
2162 assert_eq!(fields.get("debug").unwrap().as_bool(), Some(true));
2163 } else {
2164 panic!("Expected Obj variant, got {:?}", config);
2165 }
2166 }
2167
2168 #[test]
2169 fn test_parse_compact_syntax() {
2170 let input = "config:3@=[host localhost port 8080 debug true]";
2172 let doc = LlmParser::parse(input).unwrap();
2173
2174 assert!(doc.context.contains_key("config"));
2175 let config = doc.context.get("config").unwrap();
2176
2177 if let DxLlmValue::Obj(fields) = config {
2178 assert_eq!(fields.len(), 3);
2179 assert_eq!(fields.get("host").unwrap().as_str(), Some("localhost"));
2180 assert_eq!(fields.get("port").unwrap().as_num(), Some(8080.0));
2181 assert_eq!(fields.get("debug").unwrap().as_bool(), Some(true));
2182 } else {
2183 panic!("Expected Obj variant, got {:?}", config);
2184 }
2185 }
2186
2187 #[test]
2188 fn test_parse_compact_syntax_odd_tokens() {
2189 let input = "config:2@=[host localhost port]";
2191 let result = LlmParser::parse(input);
2192
2193 assert!(result.is_err());
2194 if let Err(ParseError::InvalidTable { msg }) = result {
2195 assert!(msg.contains("even number of tokens"));
2196 } else {
2197 panic!("Expected InvalidTable error for odd token count");
2198 }
2199 }
2200
2201 #[test]
2202 fn test_parse_compact_syntax_empty() {
2203 let input = "config:0@=[]";
2205 let doc = LlmParser::parse(input).unwrap();
2206
2207 assert!(doc.context.contains_key("config"));
2208 let config = doc.context.get("config").unwrap();
2209
2210 if let DxLlmValue::Obj(fields) = config {
2211 assert_eq!(fields.len(), 0);
2212 } else {
2213 panic!("Expected Obj variant, got {:?}", config);
2214 }
2215 }
2216
2217 #[test]
2218 fn test_parse_booleans() {
2219 let input = "active: true\ndeleted: false";
2220 let doc = LlmParser::parse(input).unwrap();
2221
2222 assert_eq!(doc.context.get("active").unwrap().as_bool(), Some(true));
2223 assert_eq!(doc.context.get("deleted").unwrap().as_bool(), Some(false));
2224 }
2225
2226 #[test]
2227 fn test_parse_numbers() {
2228 let input = "count: 42\nprice: 19.99";
2229 let doc = LlmParser::parse(input).unwrap();
2230
2231 assert_eq!(doc.context.get("count").unwrap().as_num(), Some(42.0));
2232 assert_eq!(doc.context.get("price").unwrap().as_num(), Some(19.99));
2233 }
2234
2235 #[test]
2236 fn test_parse_dots_in_keys() {
2237 let input = "js.dependencies.react:: 19.0.1\npython.dependencies.django:: latest";
2239 let doc = LlmParser::parse(input).unwrap();
2240
2241 assert_eq!(
2242 doc.context.get("js.dependencies.react").unwrap().as_str(),
2243 Some("19.0.1")
2244 );
2245 assert_eq!(
2246 doc.context
2247 .get("python.dependencies.django")
2248 .unwrap()
2249 .as_str(),
2250 Some("latest")
2251 );
2252 }
2253
2254 #[test]
2255 fn test_parse_bytes_valid_utf8() {
2256 let input = b"name: Test";
2257 let doc = LlmParser::parse_bytes(input).unwrap();
2258 assert_eq!(doc.context.get("name").unwrap().as_str(), Some("Test"));
2259 }
2260
2261 #[test]
2262 fn test_parse_bytes_invalid_utf8() {
2263 let input = &[0x6e, 0x61, 0x6d, 0x65, 0xFF, 0x3d, 0x54]; let err = LlmParser::parse_bytes(input).unwrap_err();
2265 if let ParseError::Utf8Error { offset } = err {
2266 assert_eq!(offset, 4);
2267 } else {
2268 panic!("Expected Utf8Error, got {:?}", err);
2269 }
2270 }
2271
2272 #[test]
2273 fn test_parse_table_inline_comma_separated_rows() {
2274 let input =
2276 "users:3(id name email)[1 Alice alice@ex.com, 2 Bob bob@ex.com, 3 Carol carol@ex.com]";
2277 let doc = LlmParser::parse(input).unwrap();
2278
2279 assert_eq!(doc.sections.len(), 1);
2280 let section = doc.sections.values().next().unwrap();
2281 assert_eq!(section.schema, vec!["id", "name", "email"]);
2282 assert_eq!(section.rows.len(), 3);
2283
2284 assert_eq!(section.rows[0][0].as_num(), Some(1.0));
2286 assert_eq!(section.rows[0][1].as_str(), Some("Alice"));
2287 assert_eq!(section.rows[0][2].as_str(), Some("alice@ex.com"));
2288
2289 assert_eq!(section.rows[1][0].as_num(), Some(2.0));
2291 assert_eq!(section.rows[1][1].as_str(), Some("Bob"));
2292 assert_eq!(section.rows[1][2].as_str(), Some("bob@ex.com"));
2293
2294 assert_eq!(section.rows[2][0].as_num(), Some(3.0));
2296 assert_eq!(section.rows[2][1].as_str(), Some("Carol"));
2297 assert_eq!(section.rows[2][2].as_str(), Some("carol@ex.com"));
2298 }
2299
2300 #[test]
2301 fn test_parse_table_inline_semicolon_separated_rows() {
2302 let input = "products:2(id name price)[101 Widget 9.99; 102 Gadget 19.99]";
2304 let doc = LlmParser::parse(input).unwrap();
2305
2306 assert_eq!(doc.sections.len(), 1);
2307 let section = doc.sections.values().next().unwrap();
2308 assert_eq!(section.schema, vec!["id", "name", "price"]);
2309 assert_eq!(section.rows.len(), 2);
2310
2311 assert_eq!(section.rows[0][0].as_num(), Some(101.0));
2313 assert_eq!(section.rows[0][1].as_str(), Some("Widget"));
2314 assert_eq!(section.rows[0][2].as_num(), Some(9.99));
2315
2316 assert_eq!(section.rows[1][0].as_num(), Some(102.0));
2318 assert_eq!(section.rows[1][1].as_str(), Some("Gadget"));
2319 assert_eq!(section.rows[1][2].as_num(), Some(19.99));
2320 }
2321
2322 #[test]
2323 fn test_parse_table_inline_colon_separated_rows() {
2324 let input = "status:3(code message count)[200 OK 150: 404 NotFound 25: 500 Error 5]";
2326 let doc = LlmParser::parse(input).unwrap();
2327
2328 assert_eq!(doc.sections.len(), 1);
2329 let section = doc.sections.values().next().unwrap();
2330 assert_eq!(section.schema, vec!["code", "message", "count"]);
2331 assert_eq!(section.rows.len(), 3);
2332
2333 assert_eq!(section.rows[0][0].as_num(), Some(200.0));
2335 assert_eq!(section.rows[0][1].as_str(), Some("OK"));
2336 assert_eq!(section.rows[0][2].as_num(), Some(150.0));
2337
2338 assert_eq!(section.rows[1][0].as_num(), Some(404.0));
2340 assert_eq!(section.rows[1][1].as_str(), Some("NotFound"));
2341 assert_eq!(section.rows[1][2].as_num(), Some(25.0));
2342
2343 assert_eq!(section.rows[2][0].as_num(), Some(500.0));
2345 assert_eq!(section.rows[2][1].as_str(), Some("Error"));
2346 assert_eq!(section.rows[2][2].as_num(), Some(5.0));
2347 }
2348
2349 #[test]
2350 fn test_parse_table_inline_comma_with_whitespace() {
2351 let input = "data:2(x y)[ 1 2 , 3 4 , 5 6 ]";
2353 let doc = LlmParser::parse(input).unwrap();
2354
2355 assert_eq!(doc.sections.len(), 1);
2356 let section = doc.sections.values().next().unwrap();
2357 assert_eq!(section.schema, vec!["x", "y"]);
2358 assert_eq!(section.rows.len(), 3);
2359
2360 assert_eq!(section.rows[0][0].as_num(), Some(1.0));
2361 assert_eq!(section.rows[0][1].as_num(), Some(2.0));
2362 assert_eq!(section.rows[1][0].as_num(), Some(3.0));
2363 assert_eq!(section.rows[1][1].as_num(), Some(4.0));
2364 assert_eq!(section.rows[2][0].as_num(), Some(5.0));
2365 assert_eq!(section.rows[2][1].as_num(), Some(6.0));
2366 }
2367
2368 #[test]
2369 fn test_parse_table_inline_empty_rows() {
2370 let input = "empty:0(a b c)[]";
2372 let doc = LlmParser::parse(input).unwrap();
2373
2374 assert_eq!(doc.sections.len(), 1);
2375 let section = doc.sections.values().next().unwrap();
2376 assert_eq!(section.schema, vec!["a", "b", "c"]);
2377 assert_eq!(section.rows.len(), 0);
2378 }
2379}