Skip to main content

serializer/llm/
parser.rs

1//! DX Serializer LLM Format Parser
2//!
3//! Parses the token-optimized LLM format into DxDocument.
4//! 52-73% more token-efficient than JSON.
5//!
6//! ## LLM Format Syntax (Wrapped Dataframe)
7//!
8//! ```text
9//! # Key-Value Pairs
10//! name=MyApp
11//! port=8080
12//! description="Multi word string"
13//!
14//! # Arrays (square brackets)
15//! tags=[rust performance serialization]
16//! editors=[neovim zed "firebase studio"]
17//!
18//! # Objects (parentheses)
19//! config(host=localhost port=5432 debug=true)
20//! server(url="https://api.example.com" timeout=30)
21//!
22//! # Tables (wrapped dataframes - deterministic parsing)
23//! users[id name email](
24//! 1 Alice alice@ex.com
25//! 2 Bob bob@ex.com
26//! 3 Carol carol@ex.com
27//! )
28//!
29//! # Multi-word values use quotes
30//! employees[id name dept](
31//! 1 "James Smith" Engineering
32//! 2 "Mary Johnson" "Research and Development"
33//! )
34//! ```
35//!
36//! ## Removed Features (for TypeScript compatibility)
37//!
38//! The following features were removed to maintain compatibility with the TypeScript implementation:
39//!
40//! - **Boolean shorthand**: `+` for true, `-` for false (use `true`/`false` instead)
41//! - **Null shorthand**: `~` for null (use `null` instead)
42
43use crate::llm::types::{DxDocument, DxLlmValue, DxSection};
44use indexmap::IndexMap;
45use thiserror::Error;
46
47/// Parse errors for Dx Serializer format
48#[derive(Debug, Error)]
49pub enum ParseError {
50    /// Parser encountered a character that is not valid at the current offset.
51    #[error("Unexpected character '{ch}' at position {pos}")]
52    UnexpectedChar {
53        /// Character that caused parsing to fail.
54        ch: char,
55        /// Byte position where the character was found.
56        pos: usize,
57    },
58
59    /// Parser reached the end of input before a complete value or structure was read.
60    #[error("Unexpected end of input")]
61    UnexpectedEof,
62
63    /// Parser found a scalar or compound value that does not match the LLM format.
64    #[error("Invalid value format: {value}")]
65    InvalidValue {
66        /// Original value fragment that could not be parsed.
67        value: String,
68    },
69
70    /// A table row did not match the declared schema width.
71    #[error("Schema mismatch: expected {expected} columns, got {got}")]
72    SchemaMismatch {
73        /// Number of columns declared by the table schema.
74        expected: usize,
75        /// Number of values parsed from the row.
76        got: usize,
77    },
78
79    /// Input bytes were not valid UTF-8.
80    #[error("Invalid UTF-8 at byte offset {offset}")]
81    Utf8Error {
82        /// Byte offset reported by UTF-8 validation.
83        offset: usize,
84    },
85
86    /// Input exceeded the configured serializer safety limit.
87    #[error("Input too large: {size} bytes exceeds maximum of {max} bytes")]
88    InputTooLarge {
89        /// Actual input size in bytes.
90        size: usize,
91        /// Maximum accepted input size in bytes.
92        max: usize,
93    },
94
95    /// Array or schema bracket was opened without a matching close bracket.
96    #[error("Unclosed bracket at position {pos}")]
97    UnclosedBracket {
98        /// Byte position where the unmatched bracket was opened.
99        pos: usize,
100    },
101
102    /// Object or table parenthesis was opened without a matching close parenthesis.
103    #[error("Unclosed parenthesis at position {pos}")]
104    UnclosedParen {
105        /// Byte position where the unmatched parenthesis was opened.
106        pos: usize,
107    },
108
109    /// A key-value assignment was missing the right-hand value.
110    #[error("Missing value after '=' at position {pos}")]
111    MissingValue {
112        /// Byte position of the incomplete assignment.
113        pos: usize,
114    },
115
116    /// Table syntax could not be parsed into schema and rows.
117    #[error("Invalid table format: {msg}")]
118    InvalidTable {
119        /// Human-readable table parse failure.
120        msg: String,
121    },
122}
123
124/// Dx Serializer format parser
125pub struct LlmParser;
126
127impl LlmParser {
128    /// Parse Dx Serializer format string into DxDocument
129    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    /// Parse Dx Serializer format from bytes with UTF-8 validation
145    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    /// Parse a single value string preserving natural spaces
160    pub fn parse_value(s: &str) -> DxLlmValue {
161        let s = s.trim();
162
163        // Boolean values: true/false only (removed +/- shorthand)
164        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        // Try to parse as number
175        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        // Handle quoted strings - strip quotes
183        if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
184            let unquoted = &s[1..s.len() - 1];
185            // Unescape any escaped quotes
186            let unescaped = unquoted.replace("\\\"", "\"");
187            return DxLlmValue::Str(unescaped);
188        }
189
190        DxLlmValue::Str(s.to_string())
191    }
192
193    /// Resolve references in a document
194    ///
195    /// Replaces `DxLlmValue::Ref` values with their resolved values from `doc.refs`.
196    pub fn resolve_refs(doc: &DxDocument) -> Result<DxDocument, ParseError> {
197        let mut resolved = doc.clone();
198
199        // Resolve context values
200        for value in resolved.context.values_mut() {
201            Self::resolve_value(value, &doc.refs)?;
202        }
203
204        // Resolve section values
205        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    /// Resolve a single value recursively
217    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/// Prefix and suffix information for prefix elimination
239#[derive(Debug, Clone, Default)]
240struct PrefixInfo {
241    prefixes: Vec<String>,
242    suffixes: Vec<String>,
243}
244
245/// Internal parser state
246struct 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        // Parse identifier (may contain dots for nested paths)
308        let name = self.parse_identifier()?;
309        if name.is_empty() {
310            // If we couldn't parse an identifier, return an error
311            // to ensure we don't return partial results on invalid input
312            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            // Just a name with no value - treat as empty string
321            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                // NEW FORMAT: Check if it's wrapped dataframe table: name[headers](rows)
333                // or array: name=[items]
334                // or OLD FORMAT: name[count] or name[key=value]
335
336                // Peek ahead to see what follows the bracket
337                let start_pos = self.pos;
338                self.advance(); // consume '['
339
340                // Read content inside brackets
341                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                    // Wrapped dataframe table: name[headers](rows)
349                    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                    // Parse wrapped rows
361                    self.advance(); // consume '('
362                    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                    // Restore position and try old format parsing
371                    self.pos = start_pos;
372
373                    // Check if it's array syntax: name[n]: or name[n]=
374                    if let Some(_count) = self.try_parse_array_count() {
375                        // Array: name[n]: items or name[n]= items
376                        self.skip_whitespace();
377                        let delimiter = self.peek_char();
378
379                        if delimiter == Some(':') {
380                            self.advance();
381                            // Check for leaf inlining (::)
382                            if self.peek_char() == Some(':') {
383                                self.advance(); // consume second ':'
384                            }
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                        // Object: name[key=value,key2=value2]
403                        let obj = self.parse_inline_object(&name)?;
404                        doc.context.insert(name.clone(), obj);
405                    }
406                }
407            }
408            Some('(') => {
409                // NEW FORMAT: Inline object: name(key=val key2=val2)
410                self.advance(); // consume '('
411                let obj = self.parse_parenthesized_object()?;
412                self.expect_char(')')?;
413                doc.context.insert(name.clone(), obj);
414            }
415            Some('=') => {
416                // NEW FORMAT: Simple key=value or array: name=[item1 item2]
417                self.advance(); // consume '='
418
419                // Check if it's an array: =[...]
420                if self.peek_char() == Some('[') {
421                    self.advance(); // consume '['
422                    let items_str = self.parse_until_char(']')?;
423                    self.expect_char(']')?;
424
425                    // Parse space-separated items (with quote support)
426                    let items = self.parse_quoted_items(&items_str);
427                    doc.context.insert(name.clone(), DxLlmValue::Arr(items));
428                } else {
429                    // Simple value
430                    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                // Check for leaf inlining (::)
438                let is_leaf = self.peek_char() == Some(':');
439                if is_leaf {
440                    self.advance(); // consume second ':'
441                }
442
443                self.skip_whitespace();
444
445                // Check what follows
446                let next = self.peek_char();
447
448                if !is_leaf && next == Some('(') {
449                    // Table without count: name:(schema)[data]
450                    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                    // Could be table with count: name:100(schema)[data]
457                    // Or inline object with count: name:3[key=value key2=value2]
458                    // Or simple array: name:3=item1 item2 item3
459                    // Or could be a version number like 2.2.16
460                    // Peek ahead to see what follows the digits
461                    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                        // It's a table: name:count(schema)[data]
477                        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                        // Check for compact syntax: name:count@=[key value key value]
484                        let next_pos = self.pos + 1;
485                        if next_pos < self.input.len() && self.input[next_pos..].starts_with('=') {
486                            // It's compact syntax
487                            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                            // Not compact syntax, restore position and parse as regular value
492                            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                        // It's an inline object with count: name:count[key=value key2=value2]
499                        let obj = self.parse_inline_object(&name)?;
500                        doc.context.insert(name.clone(), obj);
501                    } else if self.peek_char() == Some('=') {
502                        // Simple array: name:count=item1 item2 item3 (space-separated)
503                        // or name:count=item1,item2,item3 (comma-separated, legacy)
504                        self.advance();
505                        let items_str = self.parse_until_delimiter(&['\n', '\r'])?;
506
507                        // Auto-detect separator: comma (legacy) or space (new format)
508                        let items: Vec<DxLlmValue> = if items_str.contains(',') {
509                            // Comma-separated (legacy format)
510                            items_str
511                                .split(',')
512                                .map(|s| LlmParser::parse_value(s.trim()))
513                                .collect()
514                        } else {
515                            // Space-separated (new format)
516                            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                        // Not a table or inline object - restore position and parse as regular value
524                        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                    // Standard or leaf value: name: value or name:: value
531                    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                // Pipe format: key|value (used in LLM format)
538                self.advance();
539                let value = self.parse_value_until_delimiter(&['\n', '\r'])?;
540                doc.context.insert(name.clone(), value);
541            }
542            _ => {
543                // No delimiter - might be end of line or next statement
544                doc.context
545                    .insert(name.clone(), DxLlmValue::Str(String::new()));
546            }
547        }
548
549        // Track entry order: check if this name was added as context or section
550        // We need to check both because the name could have been added to either
551        if doc.context.contains_key(&name) {
552            // Only add if not already in entry_order (avoid duplicates)
553            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            // Check if it was added as a section
559            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    /// Try to parse array count syntax: [n] where n is a number
571    /// Returns Some(count) if successful, None otherwise (restores position)
572    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        // Not array count syntax, restore position
598        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        // Detect separator: scan ahead to see if we have commas or spaces between fields
607        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; // Safety limit
612
613        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            // Parse key=value
633            let key = self.parse_identifier()?;
634            if key.is_empty() {
635                if self.peek_char() == Some(']') {
636                    self.advance();
637                    break;
638                }
639                // Return error on unexpected character instead of silently skipping
640                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                // Parse value until we hit the separator or closing bracket
649                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                // Check if it's a nested array: key[count]= or nested object: key[...]
658                if let Some(_count) = self.try_parse_array_count() {
659                    // Nested array: key[count]=item1 item2 item3
660                    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                        // No '=' after [count], treat as error or null
667                        fields.insert(key, DxLlmValue::Null);
668                    }
669                } else {
670                    // Nested object
671                    let nested = self.parse_inline_object(&key)?;
672                    fields.insert(key, nested);
673                }
674            } else if self.peek_char() == Some(':') {
675                // Nested table or array
676                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                    // Convert section to array of objects
682                    let arr = self.section_to_array(&section);
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                    // Auto-detect separator: comma (legacy) or space (new format)
689                    let items: Vec<DxLlmValue> = if items_str.contains(',') {
690                        // Comma-separated (legacy format)
691                        items_str
692                            .split(',')
693                            .map(|s| LlmParser::parse_value(s.trim()))
694                            .collect()
695                    } else {
696                        // Space-separated (new format)
697                        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            // Skip separator if present
711            self.skip_whitespace();
712            if self.peek_char() == Some(separator) {
713                self.advance();
714                // For space separator, skip any additional spaces
715                if separator == ' ' {
716                    self.skip_whitespace();
717                }
718            }
719        }
720
721        // Return proper Obj variant for type safety
722        if fields.is_empty() {
723            Ok(DxLlmValue::Null)
724        } else {
725            Ok(DxLlmValue::Obj(fields))
726        }
727    }
728
729    /// Detect the separator used in an inline object by scanning ahead
730    /// Returns ',' for comma-separated (legacy) or ' ' for space-separated (new format)
731    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                        // Reached end without finding separator, default to space
744                        return Ok(' ');
745                    }
746                    depth -= 1;
747                }
748                '=' => {
749                    // After '=', we're in a value
750                    after_value = true;
751                }
752                ',' if depth == 0 && after_value => {
753                    // Found comma separator at depth 0 after a value
754                    return Ok(',');
755                }
756                ' ' if depth == 0 && after_value => {
757                    // Check if this space is between fields (after a value, before next key)
758                    // Look ahead to see if next non-space char is an identifier start
759                    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 char is alphanumeric, this space is a separator
767                        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        // Default to space-separated (new format)
780        Ok(' ')
781    }
782
783    /// Detect the row separator used in tabular data by scanning ahead
784    /// Returns ',', ';', ':', or '\n' based on content
785    /// Tracks nesting depth to avoid detecting separators inside nested structures
786    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                        // Reached end of table without finding separator, default to newline
798                        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        // Default to newline
813        Ok('\n')
814    }
815
816    /// Parse space-separated array items for nested arrays in inline objects
817    /// Handles both space-separated (new format) and comma-separated (legacy) items
818    /// Used for syntax like: key[count]=item1 item2 item3
819    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        // Determine the item separator based on the object separator
828        // If object uses space separator, array items are space-separated
829        // If object uses comma separator, array items are comma-separated
830        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                    // End of inline object - don't consume the ']'
843                    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                    // End of line - end of array items
855                    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                    // For space-separated items, check if this space marks the end of array items
862                    // Look ahead to see if we have key=value pattern (next field in object)
863                    let mut temp_pos = self.pos + 1;
864
865                    // Skip any additional spaces
866                    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                    // Check if we have an identifier followed by '=' or '[' (next field)
876                    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                        // This space marks the end of array items, next field starts
902                        if !current_item.trim().is_empty() {
903                            items.push(LlmParser::parse_value(current_item.trim()));
904                        }
905                        break;
906                    } else {
907                        // This is a space between array items
908                        if !current_item.trim().is_empty() {
909                            items.push(LlmParser::parse_value(current_item.trim()));
910                            current_item.clear();
911                        }
912                        self.advance();
913                        // Skip extra spaces
914                        while self.pos < self.input.len() && self.current_char() == ' ' {
915                            self.advance();
916                        }
917                    }
918                }
919                ',' if item_separator == ',' && in_nested == 0 => {
920                    // For comma-separated items, check if this comma marks the end of array items
921                    // Look ahead to see if we have key=value pattern (next field in object)
922                    let mut temp_pos = self.pos + 1;
923
924                    // Skip any spaces after comma
925                    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                    // Check if we have an identifier followed by '=' or '[' (next field)
935                    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                        // This comma marks the end of array items, next field starts
961                        if !current_item.trim().is_empty() {
962                            items.push(LlmParser::parse_value(current_item.trim()));
963                        }
964                        break;
965                    } else {
966                        // This is a comma between array items
967                        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    /// Parse compact syntax object: name:count@=[key value key value]
985    /// Format uses @= marker followed by space-separated key-value pairs without = signs
986    /// Tokens are paired up: first token is key, second is value, third is key, fourth is value, etc.
987    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        // Expect @=[
995        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        // Parse all tokens until ]
1003        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        // Pair up tokens as key-value pairs
1023        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        // Parse count (optional)
1045        let _count: usize = count_str.trim().parse().unwrap_or(0);
1046
1047        // Parse schema: (col1 col2 col3) or (col1,col2,col3)
1048        // Space-separated is the new default, comma-separated for backward compatibility
1049        self.expect_char('(')?;
1050        let schema_str = self.parse_until_char(')')?;
1051        self.expect_char(')')?;
1052
1053        // Detect separator: if contains comma, use comma; otherwise use space
1054        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        // Parse prefix/suffix markers (if present)
1074        let prefix_info = self.parse_prefix_markers()?;
1075
1076        // Parse data: [row1\nrow2\n...]
1077        self.skip_whitespace();
1078        self.expect_char('[')?;
1079
1080        // Detect row separator by scanning ahead
1081        let row_separator = self.detect_row_separator()?;
1082
1083        let mut section = DxSection::new(schema.clone());
1084
1085        // Route to appropriate parser based on separator
1086        match row_separator {
1087            '\n' => {
1088                // Newline-separated rows (existing logic)
1089                let mut iteration_count = 0;
1090                const MAX_ITERATIONS: usize = 10_000_000; // Safety limit (matches MAX_TABLE_ROWS)
1091
1092                // Parse rows until closing bracket
1093                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                    // Skip empty lines
1113                    if self.peek_char() == Some('\n') || self.peek_char() == Some('\r') {
1114                        self.advance();
1115                        continue;
1116                    }
1117
1118                    // Parse row
1119                    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                        // Apply prefixes/suffixes to the row
1128                        self.apply_prefixes_to_row(&mut row, &prefix_info, &schema);
1129                        section.rows.push(row);
1130                    }
1131                }
1132            }
1133            ',' | ';' | ':' => {
1134                // Inline separated rows
1135                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    /// Parse inline separated rows for comma, semicolon, or colon separators
1154    /// Format: [row1, row2, row3] or [row1; row2; row3] or [row1: row2: row3]
1155    /// Each row contains space-separated column values
1156    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; // Safety limit
1165
1166        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            // Parse a single inline row
1185            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                // Apply prefixes/suffixes to the row
1194                self.apply_prefixes_to_row(&mut row, prefix_info, schema);
1195                section.rows.push(row);
1196            }
1197
1198            // Skip separator if present
1199            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    /// Parse a single inline row with space-separated column values
1210    /// Stops at the row separator (comma, semicolon, colon) or closing bracket
1211    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                    // End of table data - don't consume the ']'
1231                    if !current_value.trim().is_empty() {
1232                        values.push(self.parse_table_value(&current_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                    // End of this row - don't consume the separator
1243                    if !current_value.trim().is_empty() {
1244                        values.push(self.parse_table_value(&current_value, ' '));
1245                    }
1246                    break;
1247                }
1248                ' ' if in_nested == 0 => {
1249                    // Space separates column values within a row
1250                    if !current_value.trim().is_empty() {
1251                        values.push(self.parse_table_value(&current_value, ' '));
1252                        current_value.clear();
1253                    }
1254                    self.advance();
1255                    // Skip extra spaces
1256                    while self.pos < self.input.len() && self.current_char() == ' ' {
1257                        self.advance();
1258                    }
1259                }
1260                '\n' | '\r' => {
1261                    // Newline within inline format - treat as end of row
1262                    if !current_value.trim().is_empty() {
1263                        values.push(self.parse_table_value(&current_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        // Detect separator: space or comma
1283        // Look ahead to determine which separator is used
1284        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                    // End of row
1307                    if !current_value.is_empty() || !values.is_empty() {
1308                        values.push(self.parse_table_value(&current_value, separator));
1309                    }
1310                    self.advance();
1311                    break;
1312                }
1313                ']' if in_parens == 0 => {
1314                    // End of table data
1315                    if !current_value.is_empty() || !values.is_empty() {
1316                        values.push(self.parse_table_value(&current_value, separator));
1317                    }
1318                    break;
1319                }
1320                c if c == separator && in_parens == 0 => {
1321                    values.push(self.parse_table_value(&current_value, separator));
1322                    current_value.clear();
1323                    self.advance();
1324                    // Skip extra spaces if space-separated
1325                    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    /// Parse a table value preserving natural spaces for optimal tokenization
1352    fn parse_table_value(&self, s: &str, _separator: char) -> DxLlmValue {
1353        let s = s.trim();
1354
1355        // Boolean values: true/false only (removed +/- shorthand)
1356        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        // Try to parse as number
1367        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    /// Parse prefix and suffix markers before table data
1378    /// Recognizes `@prefix` patterns and `@@suffix` patterns (double @)
1379    /// Format: @prefix1 @prefix2 @@suffix1 [table data]
1380    /// Returns PrefixInfo with collected prefixes and suffixes
1381    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            // Check if we're at a prefix/suffix marker
1389            if self.peek_char() != Some('@') {
1390                break;
1391            }
1392
1393            self.advance(); // consume first @
1394
1395            // Check for suffix marker (@@)
1396            let is_suffix = if self.peek_char() == Some('@') {
1397                self.advance(); // consume second @
1398                true
1399            } else {
1400                false
1401            };
1402
1403            // Parse the prefix/suffix value until we hit a delimiter
1404            // Stop at: '[' (table start), ' ' (next marker), '\n', '\r'
1405            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    /// Apply prefixes and suffixes to a row based on column name heuristics
1428    /// Modifies string values in-place by prepending prefixes or appending suffixes
1429    /// Uses column names to determine which columns should receive prefixes/suffixes
1430    fn apply_prefixes_to_row(
1431        &self,
1432        row: &mut [DxLlmValue],
1433        prefix_info: &PrefixInfo,
1434        schema: &[String],
1435    ) {
1436        // Apply prefixes to appropriate columns
1437        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                    // Heuristic: Apply prefix to columns that look like they need it
1444                    // Common patterns: endpoint, path, url, route, uri, file, directory
1445                    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                        // Apply first prefix (most common case)
1455                        *s = format!("{}{}", prefix_info.prefixes[0], s);
1456                    }
1457                }
1458            }
1459        }
1460
1461        // Apply suffixes to appropriate columns
1462        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                    // Heuristic: Apply suffix to columns that look like they need it
1469                    // Common patterns: email, domain, host, hostname
1470                    if col_lower.contains("email")
1471                        || col_lower.contains("domain")
1472                        || col_lower.contains("host")
1473                    {
1474                        // Apply first suffix (most common case)
1475                        *s = format!("{}{}", s, prefix_info.suffixes[0]);
1476                    }
1477                }
1478            }
1479        }
1480    }
1481
1482    /// Parse wrapped dataframe rows: rows inside parentheses, one per line
1483    /// Format: (row1\nrow2\nrow3)
1484    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            // Check for end of wrapped rows
1491            if self.peek_char() == Some(')') {
1492                break;
1493            }
1494
1495            if self.pos >= self.input.len() {
1496                return Err(ParseError::UnexpectedEof);
1497            }
1498
1499            // Parse a row (space-separated values with quote support)
1500            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    /// Parse a single wrapped dataframe row (space-separated with quote support)
1516    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                    // Space separates values
1544                    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                    // End of row
1552                    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                    // End of wrapped dataframe - don't consume
1560                    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    /// Parse parenthesized object: (key=val key2=val2)
1576    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            // Parse key
1591            let key = self.parse_identifier()?;
1592            if key.is_empty() {
1593                break;
1594            }
1595
1596            self.skip_whitespace();
1597
1598            // Expect '='
1599            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            // Parse value (until space or closing paren)
1608            // Check if value is an array: =[...]
1609            if self.peek_char() == Some('[') {
1610                self.advance(); // consume '['
1611                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    /// Parse space- or comma-separated items with quote support.
1627    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                    // Found the closing paren we're looking for
1756                    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 &section.rows {
1848            // Convert row to string representation
1849            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        // Leaf inlining with :: for primitive values
1982        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        // Verify it's an Obj variant with proper fields
2039        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        // Space-separated format (new default)
2051        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        // Comma-separated format (backward compatibility)
2063        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        // NEW FORMAT: Wrapped dataframe with quoted strings
2075        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        // Names with spaces use quotes
2085        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        // Test section:count[key=value key2=value2] syntax
2132        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        // Verify it's an Obj variant with proper fields
2139        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        // Test backward compatibility with comma-separated fields
2152        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        // Test compact syntax: section:count@=[key value key value]
2171        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        // Test that compact syntax with odd number of tokens returns error
2190        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        // Test compact syntax with no tokens
2204        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        // Dots in keys are allowed - no escaping needed
2238        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]; // "name" + invalid + "=T"
2264        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        // Inline rows with comma separator
2275        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        // Verify first row
2285        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        // Verify second row
2290        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        // Verify third row
2295        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        // Inline rows with semicolon separator
2303        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        // Verify first row
2312        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        // Verify second row
2317        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        // Inline rows with colon separator (simple data without colons in values)
2325        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        // Verify first row
2334        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        // Verify second row
2339        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        // Verify third row
2344        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        // Inline rows with comma separator and extra whitespace
2352        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        // Inline format with empty table
2371        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}