Skip to main content

copybook_core/
parser.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! COBOL copybook parser
3//!
4//! This module implements the parsing logic for COBOL copybooks,
5//! including lexical analysis and AST construction.
6
7use crate::error::ErrorCode;
8use crate::error::error;
9use crate::feature_flags::{Feature, FeatureFlags};
10use crate::lexer::{Lexer, Token, TokenPos};
11use crate::pic::PicClause;
12use crate::schema::{Field, FieldKind, Occurs, Schema, SignPlacement, SignSeparateInfo};
13use crate::utils::VecExt;
14use crate::{Error, Result};
15
16mod field;
17
18/// Returns true if `s` is a valid COBOL data name (letter-start, alphanumeric + hyphen).
19fn is_cobol_data_name(s: &str) -> bool {
20    let mut chars = s.chars();
21    match chars.next() {
22        Some(c) if c.is_ascii_alphabetic() => {}
23        _ => return false,
24    }
25    chars.all(|c| c.is_ascii_alphanumeric() || c == '-')
26}
27
28/// Extract a data name from a token, accepting `Identifier` directly and
29/// `EditedPic`/`PicClause` when the text is a valid COBOL data name.
30fn try_extract_data_name(token_pos: &TokenPos) -> Option<String> {
31    match &token_pos.token {
32        Token::Identifier(name) => Some(name.clone()),
33        Token::EditedPic(text) | Token::PicClause(text) if is_cobol_data_name(text) => {
34            Some(text.clone())
35        }
36        _ => None,
37    }
38}
39
40/// Parse a COBOL copybook text into a schema
41///
42/// # Errors
43/// Returns an error if the copybook contains syntax errors or unsupported features.
44#[inline]
45#[must_use = "Handle the Result or propagate the error"]
46pub fn parse(text: &str) -> Result<Schema> {
47    parse_with_options(text, &ParseOptions::default())
48}
49
50/// Parse a COBOL copybook text into a schema with specific options
51///
52/// # Errors
53/// Returns an error if the copybook contains syntax errors or unsupported features.
54#[inline]
55#[must_use = "Handle the Result or propagate the error"]
56pub fn parse_with_feature_flags(
57    text: &str,
58    options: &ParseOptions,
59    feature_flags: &FeatureFlags,
60) -> Result<Schema> {
61    parse_with_options_and_feature_flags(text, options, feature_flags)
62}
63
64/// Parse a COBOL copybook with explicit options and feature flags.
65///
66/// # Errors
67/// Returns an error if the copybook contains syntax errors or unsupported features.
68#[inline]
69#[must_use = "Handle the Result or propagate the error"]
70pub fn parse_with_options_and_feature_flags(
71    text: &str,
72    options: &ParseOptions,
73    feature_flags: &FeatureFlags,
74) -> Result<Schema> {
75    if text.trim().is_empty() {
76        return Err(error!(ErrorCode::CBKP001_SYNTAX, "Empty copybook text"));
77    }
78
79    let tokens = Lexer::new_with_options(text, options).tokenize();
80    let mut parser = Parser::with_options(tokens, options.clone(), feature_flags.clone());
81    parser.parse_schema()
82}
83
84/// Parse a COBOL copybook text into a schema with specific options
85///
86/// # Errors
87/// Returns an error if the copybook contains syntax errors or unsupported features.
88#[inline]
89#[must_use = "Handle the Result or propagate the error"]
90pub fn parse_with_options(text: &str, options: &ParseOptions) -> Result<Schema> {
91    parse_with_options_and_feature_flags(text, options, FeatureFlags::global())
92}
93
94/// Options for controlling COBOL copybook parsing behavior.
95///
96/// Configures how the parser handles various COBOL dialect features,
97/// comment styles, and validation strictness.
98#[derive(Debug, Clone)]
99#[allow(clippy::struct_excessive_bools)]
100pub struct ParseOptions {
101    /// Whether to emit FILLER fields in the parsed schema output.
102    pub emit_filler: bool,
103    /// Codepage identifier used for fingerprint calculation (e.g., `"cp037"`).
104    pub codepage: String,
105    /// Whether to allow COBOL-2002 inline comments (`*>`).
106    pub allow_inline_comments: bool,
107    /// Whether to run in strict mode with less error tolerance.
108    pub strict: bool,
109    /// Whether to enforce strict comment parsing rules.
110    pub strict_comments: bool,
111    /// Dialect for ODO `min_count` interpretation.
112    pub dialect: crate::dialect::Dialect,
113}
114
115impl Default for ParseOptions {
116    fn default() -> Self {
117        Self {
118            emit_filler: false,
119            codepage: "cp037".to_string(),
120            allow_inline_comments: true,
121            strict: false,
122            strict_comments: false,
123            dialect: crate::dialect::Dialect::Normative,
124        }
125    }
126}
127
128/// Parser state for COBOL copybook parsing
129struct Parser {
130    tokens: Vec<TokenPos>,
131    current: usize,
132    options: ParseOptions,
133    feature_flags: FeatureFlags,
134}
135
136impl Parser {
137    fn with_options(
138        tokens: Vec<TokenPos>,
139        options: ParseOptions,
140        feature_flags: FeatureFlags,
141    ) -> Self {
142        Self {
143            tokens,
144            current: 0,
145            options,
146            feature_flags,
147        }
148    }
149
150    fn require_feature_enabled(
151        &self,
152        feature: Feature,
153        field_name: &str,
154        feature_name: &str,
155        syntax: &str,
156    ) -> Result<()> {
157        if self.feature_flags.is_enabled(feature) {
158            return Ok(());
159        }
160
161        Err(Error::new(
162            ErrorCode::CBKP011_UNSUPPORTED_CLAUSE,
163            format!(
164                "{syntax} is not supported for field '{field_name}' (enable with --enable-features {feature_name})"
165            ),
166        ))
167    }
168
169    /// Parse the complete schema
170    fn parse_schema(&mut self) -> Result<Schema> {
171        // Skip any leading comments or empty lines
172        self.skip_comments_and_newlines()?;
173
174        // Parse all field definitions into a flat list first
175        let mut flat_fields = Vec::new();
176        while !self.is_at_end() {
177            if let Some(field) = self.parse_field()? {
178                flat_fields.push(field);
179            }
180            self.skip_comments_and_newlines()?;
181        }
182
183        if flat_fields.is_empty() {
184            return Err(error!(
185                ErrorCode::CBKP001_SYNTAX,
186                "No valid field definitions found"
187            ));
188        }
189
190        // Build hierarchical structure from flat fields
191        let mut hierarchical_fields = self.build_hierarchy(flat_fields)?;
192
193        // Disambiguate duplicate sibling names and recompute paths
194        finalize_names_and_paths(&mut hierarchical_fields, None, self.options.emit_filler);
195
196        // Validate the structure (REDEFINES targets, ODO constraints, etc.)
197        self.validate_structure(&hierarchical_fields)?;
198
199        // Create schema with fingerprint
200        let mut schema = Schema::from_fields(hierarchical_fields);
201
202        // Resolve field layouts and compute offsets (dialect affects ODO min_count)
203        crate::layout::resolve_layout_with_feature_flags(
204            &mut schema,
205            self.options.dialect,
206            &self.feature_flags,
207        )?;
208
209        self.calculate_schema_fingerprint(&mut schema);
210
211        Ok(schema)
212    }
213
214    /// Build hierarchical structure from flat field list
215    fn build_hierarchy(&mut self, mut flat_fields: Vec<Field>) -> Result<Vec<Field>> {
216        if flat_fields.is_empty() {
217            return Ok(Vec::new());
218        }
219
220        // Handle duplicate names and FILLER fields
221        self.process_field_names(&mut flat_fields);
222
223        // Build hierarchical structure using a stack-based approach
224        let mut stack: Vec<Field> = Vec::new();
225        let mut result: Vec<Field> = Vec::new();
226
227        for mut field in flat_fields {
228            // Set initial path
229            field.path = field.name.clone();
230
231            // Special handling for level-88 (condition values) and level-66 (RENAMES)
232            if field.level == 88 {
233                // Level-88 is a child of the immediately preceding field
234                if let Some(parent) = stack.last_mut() {
235                    field.path = format!("{}.{}", parent.path, field.name);
236                    parent.children.push(field);
237                } else {
238                    return Err(Error::new(
239                        ErrorCode::CBKP001_SYNTAX,
240                        "Level-88 condition must follow a data field".to_string(),
241                    ));
242                }
243                continue;
244            }
245
246            // Level-66 (RENAMES) is a non-storage sibling under the same parent group
247            let is_renames = field.level == 66;
248
249            // Special handling for RENAMES: close all scopes back to level-01
250            if is_renames {
251                // Level-66 should trigger closing of all open scopes (groups and leaf fields)
252                // back to the level-01 record, just like encountering a new level-01 sibling would.
253                // We pop fields and attach each to its parent as we go (like normal field processing),
254                // stopping when we reach the level-01 record.
255                while let Some(top) = stack.last() {
256                    // Stop at level-01 (always keep it on stack)
257                    if top.level == 1 {
258                        break;
259                    }
260
261                    // Pop this field and attach it to its parent
262                    let mut completed_field = stack.pop_or_cbkp_error(
263                        ErrorCode::CBKP001_SYNTAX,
264                        "Parser stack underflow while attaching RENAMES",
265                    )?;
266
267                    // Mark as group only if it has storage-bearing children (exclude level-88).
268                    if completed_field
269                        .children
270                        .iter()
271                        .any(|child| child.level != 88)
272                    {
273                        completed_field.kind = FieldKind::Group;
274                    }
275
276                    // Attach to parent (like normal field processing at lines 220-223)
277                    if let Some(parent) = stack.last_mut() {
278                        completed_field.path = format!("{}.{}", parent.path, completed_field.name);
279                        parent.children.push(completed_field);
280                    } else {
281                        return Err(Error::new(
282                            ErrorCode::CBKP001_SYNTAX,
283                            "Level-66 RENAMES must be within a record group".to_string(),
284                        ));
285                    }
286                }
287
288                // Now attach the level-66 field itself as a sibling
289                let parent = stack.last_mut().ok_or_else(|| {
290                    Error::new(
291                        ErrorCode::CBKP001_SYNTAX,
292                        "Level-66 RENAMES must be within a record group".to_string(),
293                    )
294                })?;
295
296                field.path = format!("{}.{}", parent.path, field.name);
297                parent.children.push(field);
298                continue;
299            }
300
301            // Pop fields from stack that are at same or higher level (normal fields)
302            while let Some(top) = stack.last() {
303                if top.level >= field.level {
304                    let mut completed_field = stack.pop_or_cbkp_error(
305                        ErrorCode::CBKP001_SYNTAX,
306                        "Parser stack underflow: expected field to pop but stack was empty",
307                    )?;
308
309                    // If this field has storage-bearing children, make it a group
310                    if completed_field
311                        .children
312                        .iter()
313                        .any(|child| child.level != 88)
314                    {
315                        completed_field.kind = FieldKind::Group;
316                    }
317
318                    // Add to parent or result
319                    if let Some(parent) = stack.last_mut() {
320                        // Update path to include parent
321                        completed_field.path = format!("{}.{}", parent.path, completed_field.name);
322                        parent.children.push(completed_field);
323                    } else {
324                        result.push(completed_field);
325                    }
326                } else {
327                    break;
328                }
329            }
330
331            // Update path if we have a parent
332            if let Some(parent) = stack.last() {
333                field.path = format!("{}.{}", parent.path, field.name);
334            }
335
336            stack.push(field);
337        }
338
339        // Pop remaining fields from stack
340        while let Some(mut field) = stack.pop() {
341            // If this field has storage-bearing children, make it a group
342            // (Level-88 conditions are non-storage and should not promote parent to Group)
343            if field.children.iter().any(|child| child.level != 88) {
344                field.kind = FieldKind::Group;
345            }
346
347            // Level-77 is an independent item, always goes to result (not under a parent)
348            if field.level == 77 {
349                result.push(field);
350                continue;
351            }
352
353            // Add to parent or result
354            if let Some(parent) = stack.last_mut() {
355                parent.children.push(field);
356            } else {
357                result.push(field);
358            }
359        }
360
361        Ok(result)
362    }
363
364    /// Process field names for FILLER handling (on flat list before hierarchy)
365    fn process_field_names(&mut self, fields: &mut [Field]) {
366        for field in fields.iter_mut() {
367            if field.name.to_uppercase() == "FILLER" {
368                if self.options.emit_filler {
369                    // Replace FILLER with _filler_<offset> (offset will be calculated later in layout resolution)
370                    // For now, use a placeholder that will be updated
371                    field.name = format!("_filler_{}", 0);
372                } else {
373                    // Keep FILLER name for now, will be filtered out in layout resolution
374                    field.name = "FILLER".to_string();
375                }
376            }
377        }
378    }
379
380    /// Validate the parsed structure
381    fn validate_structure(&self, fields: &[Field]) -> Result<()> {
382        // Validate REDEFINES targets
383        self.validate_redefines(fields)?;
384
385        // Validate ODO constraints
386        self.validate_odo_constraints(fields)?;
387
388        Ok(())
389    }
390
391    /// Validate REDEFINES relationships
392    fn validate_redefines(&self, fields: &[Field]) -> Result<()> {
393        let all_fields = Self::collect_all_fields(fields);
394
395        for field in &all_fields {
396            if let Some(ref target) = field.redefines_of {
397                // Find the target field
398                let target_found = all_fields
399                    .iter()
400                    .any(|f| f.name == *target || f.path == *target);
401
402                if !target_found {
403                    return Err(Error::new(
404                        ErrorCode::CBKP001_SYNTAX,
405                        format!(
406                            "REDEFINES target '{}' not found for field '{}'",
407                            target, field.name
408                        ),
409                    ));
410                }
411            }
412        }
413
414        Ok(())
415    }
416
417    /// Validate ODO constraints using hierarchical structure
418    fn validate_odo_constraints(&self, fields: &[Field]) -> Result<()> {
419        // Collect all fields for counter lookups
420        let all_fields = Self::collect_all_fields(fields);
421
422        // Validate each field group hierarchically
423        for field in fields {
424            self.validate_odo_in_group(field, &all_fields, false)?;
425        }
426
427        Ok(())
428    }
429
430    /// Check if a field is inside a REDEFINES region by walking the path
431    fn is_inside_redefines(&self, field_path: &str, all_fields: &[&Field]) -> bool {
432        // Check all ancestor paths to see if any have redefines_of
433        for ancestor in all_fields {
434            if field_path.starts_with(&format!("{}.", ancestor.path))
435                && ancestor.redefines_of.is_some()
436            {
437                return true;
438            }
439        }
440        false
441    }
442
443    /// Recursively validate ODO constraints within a field group
444    ///
445    /// # Arguments
446    /// * `field` - The field to validate
447    /// * `all_fields` - All fields for counter lookups
448    /// * `inside_occurs` - Whether we're already inside an OCCURS/ODO array
449    fn validate_odo_in_group(
450        &self,
451        field: &Field,
452        all_fields: &[&Field],
453        inside_occurs: bool,
454    ) -> Result<()> {
455        // Check if this field is an ODO array
456        if let Some(Occurs::ODO { counter_path, .. }) = &field.occurs {
457            // O5: Check for nested ODO (ODO inside OCCURS/ODO)
458            if inside_occurs {
459                return Err(Error::new(
460                    ErrorCode::CBKP022_NESTED_ODO,
461                    format!(
462                        "Nested ODO not supported: field '{}' has OCCURS DEPENDING ON inside another OCCURS/ODO array",
463                        field.path
464                    ),
465                ));
466            }
467
468            // O6: Check for ODO inside REDEFINES region
469            if self.is_inside_redefines(&field.path, all_fields) || field.redefines_of.is_some() {
470                return Err(Error::new(
471                    ErrorCode::CBKP023_ODO_REDEFINES,
472                    format!(
473                        "ODO over REDEFINES not supported: field '{}' has OCCURS DEPENDING ON inside a REDEFINES region",
474                        field.path
475                    ),
476                ));
477            }
478            // Find the counter field
479            let counter_field = all_fields
480                .iter()
481                .find(|f| f.name == *counter_path || f.path == *counter_path);
482
483            if counter_field.is_none() {
484                return Err(Error::new(
485                    ErrorCode::CBKS121_COUNTER_NOT_FOUND,
486                    format!(
487                        "ODO counter field '{}' not found for array '{}'",
488                        counter_path, field.name
489                    ),
490                ));
491            }
492
493            // Validate that counter is not inside REDEFINES or ODO region
494            if let Some(counter) = counter_field {
495                if counter.redefines_of.is_some() {
496                    return Err(Error::new(
497                        ErrorCode::CBKS121_COUNTER_NOT_FOUND,
498                        format!(
499                            "ODO counter '{}' cannot be inside a REDEFINES region",
500                            counter_path
501                        ),
502                    ));
503                }
504
505                if self.is_inside_redefines(&counter.path, all_fields) {
506                    return Err(Error::new(
507                        ErrorCode::CBKS121_COUNTER_NOT_FOUND,
508                        format!(
509                            "ODO counter '{}' cannot be inside a REDEFINES region",
510                            counter_path
511                        ),
512                    ));
513                }
514
515                if counter.occurs.is_some() {
516                    return Err(Error::new(
517                        ErrorCode::CBKS121_COUNTER_NOT_FOUND,
518                        format!(
519                            "ODO counter '{}' cannot be inside an ODO region",
520                            counter_path
521                        ),
522                    ));
523                }
524            }
525        }
526
527        // Determine if we're now inside an OCCURS/ODO region
528        let child_inside_occurs = inside_occurs || field.occurs.is_some();
529
530        // Recursively validate children and check ODO tail constraints
531        for (i, child) in field.children.iter().enumerate() {
532            // Check if child is ODO and enforce tail position rule
533            if child
534                .occurs
535                .as_ref()
536                .is_some_and(|o| matches!(o, Occurs::ODO { .. }))
537                && !self.is_odo_at_tail_sibling_based(child, &field.children, i)
538            {
539                return Err(Error::new(
540                    ErrorCode::CBKP021_ODO_NOT_TAIL,
541                    format!(
542                        "ODO array '{}' must be last storage field under '{}'",
543                        child.path, field.path
544                    ),
545                ));
546            }
547
548            // Recursively validate this child's subtree, passing down OCCURS context
549            self.validate_odo_in_group(child, all_fields, child_inside_occurs)?;
550        }
551
552        Ok(())
553    }
554
555    /// Check if ODO array is the last storage sibling (structural, sibling-based logic)
556    fn is_odo_at_tail_sibling_based(
557        &self,
558        _odo_field: &Field,
559        siblings: &[Field],
560        odo_index: usize,
561    ) -> bool {
562        // Check if there are any storage fields after this ODO field among siblings
563        !siblings
564            .iter()
565            .skip(odo_index + 1)
566            .any(|sibling| self.is_storage_field(sibling))
567        // Return true if no storage siblings found after ODO
568    }
569
570    /// Check if a field is a storage field (excludes level 88 and non-storage field types)
571    fn is_storage_field(&self, field: &Field) -> bool {
572        // Exclude level 88 (condition names)
573        if field.level == 88 {
574            return false;
575        }
576
577        // Check if field kind would have storage (independent of calculated length)
578        match &field.kind {
579            FieldKind::Group => {
580                // Groups have storage if they have storage children or aren't just containers
581                // For validation purposes, consider groups as having potential storage
582                true
583            }
584            FieldKind::Alphanum { .. }
585            | FieldKind::ZonedDecimal { .. }
586            | FieldKind::BinaryInt { .. }
587            | FieldKind::PackedDecimal { .. }
588            | FieldKind::EditedNumeric { .. }
589            | FieldKind::FloatSingle
590            | FieldKind::FloatDouble => true,
591            FieldKind::Condition { .. } => false, // Level-88 fields don't have storage
592            FieldKind::Renames { .. } => false,   // Level-66 fields don't have storage
593        }
594    }
595
596    /// Collect all fields in a flat list
597    fn collect_all_fields(fields: &[Field]) -> Vec<&Field> {
598        let mut result = Vec::new();
599        for field in fields {
600            result.push(field);
601            let children = Self::collect_all_fields(&field.children);
602            result.extend(children);
603        }
604        result
605    }
606
607    /// Calculate schema fingerprint using SHA-256 including parse options
608    fn calculate_schema_fingerprint(&self, schema: &mut Schema) {
609        use sha2::{Digest, Sha256};
610
611        // Use schema's canonical JSON representation
612        let canonical_json = schema.create_canonical_json();
613
614        // Create hasher and add canonical JSON
615        let mut hasher = Sha256::new();
616        hasher.update(canonical_json.as_bytes());
617
618        // Add parse-specific options that affect fingerprint
619        hasher.update(self.options.codepage.as_bytes());
620        hasher.update([if self.options.emit_filler { 1 } else { 0 }]);
621
622        // Compute final hash
623        let result = hasher.finalize();
624        schema.fingerprint = format!("{:x}", result);
625    }
626
627    /// Parse PIC clause
628    fn parse_pic_clause(&mut self, field: &mut Field) -> Result<()> {
629        // Collect PIC clause tokens - might be split across multiple tokens
630        let mut pic_parts = Vec::new();
631
632        // Track the starting line for same-line checks
633        let token_line = self.current_token().map_or(0, |t| t.line);
634
635        // First token should be a PIC clause or identifier
636        match self.current_token() {
637            Some(TokenPos {
638                token: Token::PicClause(pic),
639                ..
640            }) => {
641                pic_parts.push(pic.clone());
642                self.advance();
643            }
644            Some(TokenPos {
645                token: Token::EditedPic(pic),
646                ..
647            }) => {
648                // Phase E1: Accept edited PIC and push to parts for parsing
649                pic_parts.push(pic.clone());
650                self.advance();
651            }
652            Some(TokenPos {
653                token: Token::Identifier(id),
654                ..
655            }) => {
656                // This might be part of a PIC clause like "S9(7)" followed by "V99"
657                pic_parts.push(id.clone());
658                self.advance();
659            }
660            Some(TokenPos {
661                token: Token::Number(n),
662                ..
663            }) => {
664                // Lexer tokenizes e.g. "999" as Number (priority 4 > PicClause priority 3)
665                pic_parts.push(n.to_string());
666                self.advance();
667            }
668            _ => {
669                return Err(Error::new(
670                    ErrorCode::CBKP001_SYNTAX,
671                    "Expected PIC clause after PIC keyword",
672                ));
673            }
674        }
675
676        // Track whether we're inside parentheses
677        let mut paren_depth: i32 = 0;
678
679        // Collect repetition count and sign indicators if present
680        while let Some(token) = self.current_token() {
681            match &token.token {
682                Token::LeftParen => {
683                    paren_depth += 1;
684                    pic_parts.push("(".to_string());
685                    self.advance();
686                }
687                Token::Number(n) if paren_depth > 0 => {
688                    pic_parts.push(n.to_string());
689                    self.advance();
690                }
691                Token::RightParen if paren_depth > 0 => {
692                    paren_depth -= 1;
693                    pic_parts.push(")".to_string());
694                    self.advance();
695                }
696                Token::Comma => {
697                    // Comma in edited PIC patterns like $ZZ,ZZZ.99
698                    pic_parts.push(",".to_string());
699                    self.advance();
700                }
701                Token::EditedPic(ep) => {
702                    // Continuation of edited PIC after comma/period
703                    pic_parts.push(ep.clone());
704                    self.advance();
705                }
706                Token::Period => {
707                    // Period could be decimal point or sentence terminator.
708                    // Only treat as decimal point if:
709                    // 1. On the same line as the PIC start
710                    // 2. The last part is NOT a closing paren (e.g., `PIC X(20).` → terminator)
711                    // 3. Something follows on the same line that looks like more PIC
712                    let t = token.clone();
713                    let last_is_rparen = pic_parts
714                        .last()
715                        .is_some_and(|s| s == ")" || s.ends_with(')'));
716                    if !last_is_rparen
717                        && t.line == token_line
718                        && let Some(next) = self.peek_next()
719                    {
720                        let is_pic_continuation = matches!(
721                            next.token,
722                            Token::Number(_)
723                                | Token::PicClause(_)
724                                | Token::EditedPic(_)
725                                | Token::Identifier(_)
726                        );
727                        if next.line == token_line && is_pic_continuation {
728                            pic_parts.push(".".to_string());
729                            self.advance();
730                            continue;
731                        }
732                    }
733                    break;
734                }
735                Token::Number(n) => {
736                    // Number outside parens on same line (e.g., "ZZZ9" split as EditedPic + Number)
737                    let t = token.clone();
738                    if t.line == token_line {
739                        pic_parts.push(n.to_string());
740                        self.advance();
741                    } else {
742                        break;
743                    }
744                }
745                Token::Identifier(id) if id.starts_with('V') || id.starts_with('v') => {
746                    pic_parts.push(id.clone());
747                    self.advance();
748                }
749                // Collect sign-related identifiers (CR, DB, etc.)
750                Token::Identifier(id) => {
751                    let id_upper = id.to_ascii_uppercase();
752                    if id_upper == "CR" || id_upper == "DB" {
753                        pic_parts.push(id.clone());
754                        self.advance();
755                    } else {
756                        break;
757                    }
758                }
759                _ => break,
760            }
761        }
762
763        let pic_str = pic_parts.join("");
764        let pic = PicClause::parse(&pic_str)?;
765
766        field.kind = match pic.kind {
767            crate::pic::PicKind::Alphanumeric => FieldKind::Alphanum {
768                len: pic.digits as u32,
769            },
770            crate::pic::PicKind::NumericDisplay => FieldKind::ZonedDecimal {
771                digits: pic.digits,
772                scale: pic.scale,
773                signed: pic.signed,
774                sign_separate: None,
775            },
776            crate::pic::PicKind::Edited => {
777                // Phase E2: Parse edited PIC into schema with scale
778                FieldKind::EditedNumeric {
779                    pic_string: pic_str.clone(),
780                    width: pic.digits,
781                    scale: pic.scale as u16,
782                    signed: pic.signed,
783                }
784            }
785        };
786
787        Ok(())
788    }
789
790    /// Parse USAGE clause
791    fn parse_usage_clause(&mut self, field: &mut Field) -> Result<()> {
792        match self.current_token() {
793            Some(TokenPos {
794                token: Token::Display,
795                ..
796            }) => {
797                self.advance();
798                // USAGE DISPLAY is the default, no change needed
799            }
800            Some(TokenPos {
801                token: Token::Comp, ..
802            }) => {
803                self.advance();
804                self.convert_to_binary_field(field)?;
805            }
806            Some(TokenPos {
807                token: Token::Comp3,
808                ..
809            }) => {
810                self.advance();
811                self.convert_to_packed_field(field)?;
812            }
813            Some(TokenPos {
814                token: Token::Comp1,
815                ..
816            }) => {
817                self.advance();
818                self.require_feature_enabled(
819                    Feature::Comp1,
820                    &field.name,
821                    "comp_1",
822                    "USAGE COMP-1",
823                )?;
824                field.kind = FieldKind::FloatSingle;
825            }
826            Some(TokenPos {
827                token: Token::Comp2,
828                ..
829            }) => {
830                self.advance();
831                self.require_feature_enabled(
832                    Feature::Comp2,
833                    &field.name,
834                    "comp_2",
835                    "USAGE COMP-2",
836                )?;
837                field.kind = FieldKind::FloatDouble;
838            }
839            Some(TokenPos {
840                token: Token::Binary,
841                ..
842            }) => {
843                self.advance();
844                self.convert_to_binary_field(field)?;
845            }
846            _ => {
847                return Err(Error::new(
848                    ErrorCode::CBKP001_SYNTAX,
849                    "Expected USAGE type after USAGE keyword",
850                ));
851            }
852        }
853        Ok(())
854    }
855
856    /// Parse REDEFINES clause
857    fn parse_redefines_clause(&mut self, field: &mut Field) -> Result<()> {
858        let target = match self.current_token().and_then(try_extract_data_name) {
859            Some(n) => {
860                self.advance();
861                n
862            }
863            _ => {
864                return Err(Error::new(
865                    ErrorCode::CBKP001_SYNTAX,
866                    "Expected field name after REDEFINES",
867                ));
868            }
869        };
870
871        field.redefines_of = Some(target);
872        Ok(())
873    }
874
875    /// Parse OCCURS clause
876    fn parse_occurs_clause(&mut self, field: &mut Field) -> Result<()> {
877        let min = match self.current_token() {
878            Some(TokenPos {
879                token: Token::Number(n),
880                ..
881            }) => {
882                let min = *n;
883                self.advance();
884                min
885            }
886            // Accept Level tokens as numbers in OCCURS context (01-49 are lexed as Level)
887            Some(TokenPos {
888                token: Token::Level(n),
889                ..
890            }) => {
891                let min = u32::from(*n);
892                self.advance();
893                min
894            }
895            _ => {
896                return Err(Error::new(
897                    ErrorCode::CBKP001_SYNTAX,
898                    "Expected number after OCCURS",
899                ));
900            }
901        };
902
903        // Check for TO keyword (range syntax)
904        let max = if self.check(&Token::To) {
905            self.advance(); // consume TO
906            match self.current_token() {
907                Some(TokenPos {
908                    token: Token::Number(n),
909                    ..
910                }) => {
911                    let max = *n;
912                    self.advance();
913                    max
914                }
915                // Accept Level tokens as numbers in OCCURS context (01-49 are lexed as Level)
916                Some(TokenPos {
917                    token: Token::Level(n),
918                    ..
919                }) => {
920                    let max = u32::from(*n);
921                    self.advance();
922                    max
923                }
924                _ => {
925                    return Err(Error::new(
926                        ErrorCode::CBKP001_SYNTAX,
927                        "Expected number after TO in OCCURS clause",
928                    ));
929                }
930            }
931        } else {
932            min // If no TO, then min == max (fixed count)
933        };
934
935        // Skip optional TIMES keyword
936        if self.check(&Token::Times) {
937            self.advance();
938        }
939
940        // Look for DEPENDING ON (might not be immediately next)
941        let depending_pos = self.find_depending_in_clause();
942
943        if let Some(depending_idx) = depending_pos {
944            // Found DEPENDING, advance to it
945            self.current = depending_idx;
946            self.advance(); // consume DEPENDING
947
948            if !self.consume(&Token::On) {
949                return Err(Error::new(
950                    ErrorCode::CBKP001_SYNTAX,
951                    "Expected ON after DEPENDING",
952                ));
953            }
954
955            let counter_field = match self.current_token().and_then(try_extract_data_name) {
956                Some(n) => {
957                    self.advance();
958                    n
959                }
960                _ => {
961                    return Err(Error::new(
962                        ErrorCode::CBKP001_SYNTAX,
963                        "Expected field name after DEPENDING ON",
964                    ));
965                }
966            };
967
968            field.occurs = Some(Occurs::ODO {
969                min,
970                max,
971                counter_path: counter_field,
972            });
973        } else {
974            // No DEPENDING ON found
975            if min != max {
976                return Err(Error::new(
977                    ErrorCode::CBKP001_SYNTAX,
978                    "Range syntax (min TO max) requires DEPENDING ON clause",
979                ));
980            }
981            field.occurs = Some(Occurs::Fixed { count: min });
982        }
983
984        Ok(())
985    }
986
987    /// Find DEPENDING token in the current clause (before the next field starts)
988    fn find_depending_in_clause(&self) -> Option<usize> {
989        for i in self.current..self.tokens.len() {
990            match &self.tokens[i].token {
991                Token::Depending => return Some(i),
992                // Stop looking when we hit the start of a new field
993                Token::Level(_) | Token::Level66 | Token::Level77 | Token::Level88 => break,
994                _ => {}
995            }
996        }
997        None
998    }
999
1000    /// Parse BLANK WHEN ZERO clause
1001    fn parse_blank_when_zero_clause(&mut self, field: &mut Field) -> Result<()> {
1002        if !self.consume(&Token::When) {
1003            return Err(Error::new(
1004                ErrorCode::CBKP001_SYNTAX,
1005                "Expected WHEN after BLANK",
1006            ));
1007        }
1008
1009        if !self.consume(&Token::Zero) {
1010            return Err(Error::new(
1011                ErrorCode::CBKP001_SYNTAX,
1012                "Expected ZERO after BLANK WHEN",
1013            ));
1014        }
1015
1016        field.blank_when_zero = true;
1017        Ok(())
1018    }
1019
1020    /// Parse SIGN clause (`SIGN [IS] [LEADING|TRAILING] [SEPARATE]` or
1021    /// `SIGN [IS] SEPARATE [LEADING|TRAILING]`).
1022    fn parse_sign_clause(&mut self, field: &mut Field) -> Result<()> {
1023        // Optional IS keyword
1024        self.consume(&Token::Is);
1025
1026        // Parse SIGN clause components in either order:
1027        //   SIGN [IS] [SEPARATE] [LEADING|TRAILING]
1028        //   SIGN [IS] [LEADING|TRAILING] [SEPARATE]
1029        // COBOL's standard action for an omitted placement is trailing.
1030        let mut placement = SignPlacement::Trailing;
1031        let mut saw_placement = false;
1032        let mut saw_separate = false;
1033
1034        for _ in 0..2 {
1035            if self.consume(&Token::Separate) {
1036                if saw_separate {
1037                    return Err(Error::new(
1038                        ErrorCode::CBKP001_SYNTAX,
1039                        "Invalid SIGN clause syntax: duplicate SEPARATE",
1040                    ));
1041                }
1042                saw_separate = true;
1043                continue;
1044            }
1045
1046            if self.consume(&Token::Leading) {
1047                if saw_placement {
1048                    return Err(Error::new(
1049                        ErrorCode::CBKP001_SYNTAX,
1050                        "Invalid SIGN clause syntax: duplicate LEADING/TRAILING",
1051                    ));
1052                }
1053                placement = SignPlacement::Leading;
1054                saw_placement = true;
1055                continue;
1056            }
1057
1058            if self.consume(&Token::Trailing) {
1059                if saw_placement {
1060                    return Err(Error::new(
1061                        ErrorCode::CBKP001_SYNTAX,
1062                        "Invalid SIGN clause syntax: duplicate LEADING/TRAILING",
1063                    ));
1064                }
1065                placement = SignPlacement::Trailing;
1066                saw_placement = true;
1067                continue;
1068            }
1069
1070            break;
1071        }
1072
1073        if !saw_separate {
1074            // SIGN without SEPARATE is not supported in this implementation
1075            // (it would use overpunching which is already handled by signed zoned decimals)
1076            return Err(Error::new(
1077                ErrorCode::CBKP001_SYNTAX,
1078                "SIGN clause without SEPARATE is not supported (use S in PIC clause for overpunching)",
1079            ));
1080        }
1081
1082        // Do not allow unknown tokens to silently attach to the SIGN clause.
1083        // The next token should be a field clause boundary or the field terminator.
1084        if let Some(next) = self.current_token() {
1085            let next_is_boundary = matches!(
1086                &next.token,
1087                Token::Period
1088                    | Token::Newline
1089                    | Token::InlineComment(_)
1090                    | Token::Pic
1091                    | Token::Usage
1092                    | Token::Redefines
1093                    | Token::Renames
1094                    | Token::Occurs
1095                    | Token::Synchronized
1096                    | Token::Value
1097                    | Token::Blank
1098                    | Token::Sign
1099                    | Token::Comp
1100                    | Token::Comp3
1101                    | Token::Comp1
1102                    | Token::Comp2
1103                    | Token::Binary
1104            );
1105
1106            if !next_is_boundary {
1107                return Err(Error::new(
1108                    ErrorCode::CBKP001_SYNTAX,
1109                    "Invalid SIGN clause syntax. Expected SEPARATE with optional LEADING/TRAILING",
1110                ));
1111            }
1112        }
1113
1114        // Validate that field is a numeric display field
1115        match &mut field.kind {
1116            FieldKind::ZonedDecimal {
1117                digits,
1118                scale,
1119                signed,
1120                sign_separate: _,
1121            } => {
1122                *signed = true;
1123                // Add sign separate info to the field
1124                let sign_separate_info = SignSeparateInfo { placement };
1125                // Update field kind to include sign separate info
1126                field.kind = FieldKind::ZonedDecimal {
1127                    digits: *digits,
1128                    scale: *scale,
1129                    signed: true,
1130                    sign_separate: Some(sign_separate_info),
1131                };
1132            }
1133            _ => {
1134                return Err(Error::new(
1135                    ErrorCode::CBKP001_SYNTAX,
1136                    format!(
1137                        "SIGN SEPARATE clause can only be used with numeric display fields (PIC 9 or PIC S9), not field '{}'",
1138                        field.name
1139                    ),
1140                ));
1141            }
1142        }
1143
1144        // Cannot combine SIGN SEPARATE with BLANK WHEN ZERO
1145        if field.blank_when_zero {
1146            return Err(Error::new(
1147                ErrorCode::CBKP001_SYNTAX,
1148                "SIGN SEPARATE clause cannot be combined with BLANK WHEN ZERO",
1149            ));
1150        }
1151
1152        Ok(())
1153    }
1154
1155    /// Parse Level-88 VALUE clause
1156    fn parse_level88_value_clause(&mut self, field: &mut Field) -> Result<()> {
1157        let mut values = Vec::new();
1158
1159        // Parse VALUE clauses - can be literals, ranges, or multiple values
1160        loop {
1161            match self.current_token() {
1162                Some(TokenPos {
1163                    token: Token::StringLiteral(s),
1164                    ..
1165                }) => {
1166                    values.push(s.clone());
1167                    self.advance();
1168                }
1169                Some(TokenPos {
1170                    token: Token::Number(n),
1171                    ..
1172                }) => {
1173                    values.push(n.to_string());
1174                    self.advance();
1175                }
1176                Some(TokenPos {
1177                    token: Token::Identifier(id),
1178                    ..
1179                }) if id.to_uppercase() == "ZEROS" || id.to_uppercase() == "ZEROES" => {
1180                    values.push("ZEROS".to_string());
1181                    self.advance();
1182                }
1183                Some(TokenPos {
1184                    token: Token::Identifier(id),
1185                    ..
1186                }) if id.to_uppercase() == "SPACES" => {
1187                    values.push("SPACES".to_string());
1188                    self.advance();
1189                }
1190                _ => break,
1191            }
1192
1193            // Check for THROUGH/THRU ranges or additional values
1194            let range_keyword = match self.current_token() {
1195                Some(TokenPos {
1196                    token: Token::Through,
1197                    ..
1198                }) => {
1199                    self.advance();
1200                    "THROUGH"
1201                }
1202                Some(TokenPos {
1203                    token: Token::Thru, ..
1204                }) => {
1205                    self.advance();
1206                    "THRU"
1207                }
1208                _ => {
1209                    // No range keyword, continue to next value/comma
1210                    if let Some(TokenPos {
1211                        token: Token::Comma,
1212                        ..
1213                    }) = self.current_token()
1214                    {
1215                        self.advance();
1216                    }
1217                    continue;
1218                }
1219            };
1220
1221            // Parse the range end value
1222            match self.current_token() {
1223                Some(TokenPos {
1224                    token: Token::StringLiteral(s),
1225                    ..
1226                }) => {
1227                    // Replace last value with range notation
1228                    if let Some(last) = values.last_mut() {
1229                        *last = format!("{} {} {}", last, range_keyword, s);
1230                    }
1231                    self.advance();
1232                }
1233                Some(TokenPos {
1234                    token: Token::Number(n),
1235                    ..
1236                }) => {
1237                    if let Some(last) = values.last_mut() {
1238                        *last = format!("{} {} {}", last, range_keyword, n);
1239                    }
1240                    self.advance();
1241                }
1242                _ => break,
1243            }
1244
1245            // Optionally consume a comma before the next value
1246            self.consume(&Token::Comma);
1247        }
1248
1249        if values.is_empty() {
1250            return Err(Error::new(
1251                ErrorCode::CBKP001_SYNTAX,
1252                "Level-88 VALUE clause requires at least one value",
1253            ));
1254        }
1255
1256        // Set field kind to Condition
1257        field.kind = FieldKind::Condition { values };
1258
1259        Ok(())
1260    }
1261
1262    /// Parse a qualified name (QNAME): IDENT (OF IDENT)*
1263    fn parse_qualified_name(&mut self) -> Result<String> {
1264        let mut parts = Vec::new();
1265
1266        // Parse first identifier
1267        match self.current_token().and_then(try_extract_data_name) {
1268            Some(n) => {
1269                parts.push(n);
1270                self.advance();
1271            }
1272            _ => {
1273                return Err(Error::new(
1274                    ErrorCode::CBKP001_SYNTAX,
1275                    "Expected identifier in qualified name",
1276                ));
1277            }
1278        }
1279
1280        // Parse optional "OF IDENT" sequences
1281        loop {
1282            match self.current_token() {
1283                Some(TokenPos {
1284                    token: Token::Identifier(name),
1285                    ..
1286                }) if name.eq_ignore_ascii_case("OF") => {
1287                    self.advance(); // consume OF
1288                    match self.current_token().and_then(try_extract_data_name) {
1289                        Some(n) => {
1290                            parts.push("OF".to_string());
1291                            parts.push(n);
1292                            self.advance();
1293                        }
1294                        _ => {
1295                            return Err(Error::new(
1296                                ErrorCode::CBKP001_SYNTAX,
1297                                "Expected identifier after OF in qualified name",
1298                            ));
1299                        }
1300                    }
1301                }
1302                _ => break,
1303            }
1304        }
1305
1306        Ok(parts.join(" "))
1307    }
1308
1309    /// Parse Level-66 RENAMES clause
1310    ///
1311    /// Syntax: 66 NAME RENAMES from-field THROUGH|THRU thru-field.
1312    /// Field names can be qualified: IDENT (OF IDENT)*
1313    fn parse_renames(&mut self, field: &mut Field) -> Result<()> {
1314        // Expect RENAMES keyword
1315        match self.current_token() {
1316            Some(TokenPos {
1317                token: Token::Renames,
1318                ..
1319            }) => {
1320                self.advance();
1321            }
1322            _ => {
1323                return Err(Error::new(
1324                    ErrorCode::CBKP001_SYNTAX,
1325                    "Expected RENAMES keyword after level-66 field name",
1326                ));
1327            }
1328        }
1329
1330        // Parse from-field qualified name
1331        let from_field = self.parse_qualified_name()?;
1332
1333        let thru_field = if self.check(&Token::Through) || self.check(&Token::Thru) {
1334            self.advance();
1335            self.parse_qualified_name()?
1336        } else {
1337            // R5 support uses single-field RENAMES without explicit THROUGH/THRU.
1338            from_field.clone()
1339        };
1340
1341        // Set field kind to Renames
1342        field.kind = FieldKind::Renames {
1343            from_field,
1344            thru_field,
1345        };
1346
1347        Ok(())
1348    }
1349
1350    /// Skip VALUE clause (not needed for layout)
1351    fn skip_value_clause(&mut self) -> Result<()> {
1352        self.advance(); // consume VALUE
1353
1354        // Skip until we find a keyword or period
1355        while !self.is_at_end() && !self.check(&Token::Period) {
1356            if self.is_keyword() {
1357                break;
1358            }
1359            self.advance();
1360        }
1361
1362        Ok(())
1363    }
1364
1365    /// Convert numeric field to binary
1366    fn convert_to_binary_field(&mut self, field: &mut Field) -> Result<()> {
1367        match &field.kind {
1368            FieldKind::ZonedDecimal { digits, signed, .. } => {
1369                let bits = match digits {
1370                    1..=4 => 16,
1371                    5..=9 => 32,
1372                    10..=18 => 64,
1373                    _ => {
1374                        return Err(Error::new(
1375                            ErrorCode::CBKP001_SYNTAX,
1376                            format!("Binary field with {} digits not supported", digits),
1377                        ));
1378                    }
1379                };
1380
1381                field.kind = FieldKind::BinaryInt {
1382                    bits,
1383                    signed: *signed,
1384                };
1385            }
1386            _ => {
1387                return Err(Error::new(
1388                    ErrorCode::CBKP001_SYNTAX,
1389                    "USAGE COMP/BINARY can only be applied to numeric fields",
1390                ));
1391            }
1392        }
1393        Ok(())
1394    }
1395
1396    /// Convert numeric field to packed decimal
1397    fn convert_to_packed_field(&mut self, field: &mut Field) -> Result<()> {
1398        match &field.kind {
1399            FieldKind::ZonedDecimal {
1400                digits,
1401                scale,
1402                signed,
1403                ..
1404            } => {
1405                field.kind = FieldKind::PackedDecimal {
1406                    digits: *digits,
1407                    scale: *scale,
1408                    signed: *signed,
1409                };
1410            }
1411            _ => {
1412                return Err(Error::new(
1413                    ErrorCode::CBKP001_SYNTAX,
1414                    "USAGE COMP-3 can only be applied to numeric fields",
1415                ));
1416            }
1417        }
1418        Ok(())
1419    }
1420
1421    /// Skip comments and newlines
1422    fn skip_comments_and_newlines(&mut self) -> Result<()> {
1423        while let Some(token) = self.current_token() {
1424            match &token.token {
1425                Token::InlineComment(_) => {
1426                    if self.options.strict_comments {
1427                        return Err(Error::new(
1428                            ErrorCode::CBKP001_SYNTAX,
1429                            "Inline comments (*>) are not allowed in strict mode",
1430                        ));
1431                    }
1432                    self.advance();
1433                }
1434                Token::Newline => {
1435                    self.advance();
1436                }
1437                _ => break,
1438            }
1439        }
1440        Ok(())
1441    }
1442
1443    /// Check if current token is a keyword
1444    fn is_keyword(&self) -> bool {
1445        matches!(
1446            self.current_token().map(|t| &t.token),
1447            Some(
1448                Token::Pic
1449                    | Token::Usage
1450                    | Token::Redefines
1451                    | Token::Occurs
1452                    | Token::Synchronized
1453                    | Token::Value
1454                    | Token::Blank
1455                    | Token::Sign
1456            )
1457        )
1458    }
1459
1460    /// Get current token
1461    fn current_token(&self) -> Option<&TokenPos> {
1462        self.tokens.get(self.current)
1463    }
1464
1465    /// Peek at the next token without advancing
1466    fn peek_next(&self) -> Option<&TokenPos> {
1467        self.tokens.get(self.current + 1)
1468    }
1469
1470    /// Advance to next token
1471    fn advance(&mut self) {
1472        if !self.is_at_end() {
1473            self.current += 1;
1474        }
1475    }
1476
1477    /// Check if we're at the end
1478    fn is_at_end(&self) -> bool {
1479        matches!(
1480            self.current_token().map(|t| &t.token),
1481            Some(Token::Eof) | None
1482        )
1483    }
1484
1485    /// Check if current token matches the given token
1486    fn check(&self, token: &Token) -> bool {
1487        if let Some(current) = self.current_token() {
1488            std::mem::discriminant(&current.token) == std::mem::discriminant(token)
1489        } else {
1490            false
1491        }
1492    }
1493
1494    /// Consume token if it matches, return true if consumed
1495    fn consume(&mut self, token: &Token) -> bool {
1496        if self.check(token) {
1497            self.advance();
1498            true
1499        } else {
1500            false
1501        }
1502    }
1503}
1504
1505/// Disambiguate duplicate names among true siblings (post-hierarchy) and recompute paths.
1506///
1507/// Walks the hierarchical field tree. At each level, siblings sharing a name
1508/// get `__dup2`, `__dup3`, … suffixes. Paths are rebuilt from the parent path
1509/// after renaming so they stay consistent.
1510fn finalize_names_and_paths(fields: &mut [Field], parent_path: Option<&str>, emit_filler: bool) {
1511    use std::collections::HashMap;
1512
1513    let mut counts: HashMap<String, u32> = HashMap::new();
1514
1515    for field in fields.iter_mut() {
1516        // Skip FILLER fields that won't be emitted — they don't need unique names
1517        if field.name == "FILLER" && !emit_filler {
1518            field.path = match parent_path {
1519                Some(parent) => format!("{parent}.{}", field.name),
1520                None => field.name.clone(),
1521            };
1522            continue;
1523        }
1524
1525        let base = field.name.clone();
1526        let count = counts.entry(base.clone()).or_insert(0);
1527        *count += 1;
1528        if *count > 1 {
1529            field.name = format!("{base}__dup{count}");
1530        }
1531
1532        field.path = match parent_path {
1533            Some(parent) => format!("{parent}.{}", field.name),
1534            None => field.name.clone(),
1535        };
1536    }
1537
1538    for field in fields.iter_mut() {
1539        let parent = field.path.clone();
1540        finalize_names_and_paths(&mut field.children, Some(&parent), emit_filler);
1541    }
1542}
1543
1544#[cfg(test)]
1545#[allow(clippy::expect_used)]
1546#[allow(clippy::unwrap_used)]
1547#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1548mod tests {
1549    use super::*;
1550
1551    #[test]
1552    fn test_simple_field_parsing() {
1553        let input = "01 CUSTOMER-ID PIC X(10).";
1554        let schema = parse(input)
1555            .map_err(|e| {
1556                Error::new(
1557                    ErrorCode::CBKS141_RECORD_TOO_LARGE,
1558                    format!("Failed to parse copybook: {}", e),
1559                )
1560            })
1561            .expect("Failed to parse copybook");
1562
1563        assert_eq!(schema.fields.len(), 1);
1564        let field = &schema.fields[0];
1565        assert_eq!(field.name, "CUSTOMER-ID");
1566        assert_eq!(field.level, 1);
1567        assert!(matches!(field.kind, FieldKind::Alphanum { len: 10 }));
1568    }
1569
1570    #[test]
1571    fn test_numeric_field_parsing() {
1572        let input = "01 AMOUNT PIC S9(7)V99.";
1573
1574        // Debug: test tokenization
1575        let mut lexer = crate::lexer::Lexer::new(input);
1576        let tokens = lexer.tokenize();
1577        for (i, token) in tokens.iter().enumerate() {
1578            println!("Token {}: {:?}", i, token.token);
1579        }
1580
1581        // Debug: test PIC parsing directly
1582        let pic_result = crate::pic::PicClause::parse("S9(7)V99");
1583        println!("PIC parse result: {:?}", pic_result);
1584
1585        let schema = parse(input).unwrap();
1586
1587        assert_eq!(schema.fields.len(), 1);
1588        let field = &schema.fields[0];
1589        assert_eq!(field.name, "AMOUNT");
1590
1591        // Debug print the actual field kind
1592        println!("Field kind: {:?}", field.kind);
1593
1594        assert!(matches!(
1595            field.kind,
1596            FieldKind::ZonedDecimal {
1597                digits: 9,
1598                scale: 2,
1599                signed: true,
1600                ..
1601            }
1602        ));
1603    }
1604
1605    #[test]
1606    fn test_binary_field_parsing() {
1607        let input = "01 COUNT PIC 9(5) USAGE COMP.";
1608        let schema = parse(input).unwrap();
1609
1610        assert_eq!(schema.fields.len(), 1);
1611        let field = &schema.fields[0];
1612        assert!(matches!(
1613            field.kind,
1614            FieldKind::BinaryInt {
1615                bits: 32,
1616                signed: false
1617            }
1618        ));
1619    }
1620
1621    #[test]
1622    fn test_occurs_parsing() {
1623        let input = "01 ARRAY-FIELD PIC X(10) OCCURS 5 TIMES.";
1624        let schema = parse(input).unwrap();
1625
1626        assert_eq!(schema.fields.len(), 1);
1627        let field = &schema.fields[0];
1628        assert!(matches!(field.occurs, Some(Occurs::Fixed { count: 5 })));
1629    }
1630
1631    #[test]
1632    fn test_redefines_parsing() {
1633        let input = r"
163401 FIELD-A PIC X(10).
163501 FIELD-B REDEFINES FIELD-A PIC 9(10).
1636";
1637        let schema = parse(input).unwrap();
1638
1639        assert_eq!(schema.fields.len(), 2);
1640        let field_b = &schema.fields[1];
1641        assert_eq!(field_b.redefines_of, Some("FIELD-A".to_string()));
1642    }
1643
1644    #[test]
1645    fn test_edited_pic_acceptance() {
1646        // Phase E1: Edited PIC is now supported and should parse successfully
1647        // Note: Complex decimal patterns not yet supported; using simple pattern
1648        let input = "01 AMOUNT PIC ZZZZ.";
1649        let result = parse(input);
1650
1651        assert!(result.is_ok(), "Edited PIC should parse successfully");
1652        let schema = result.unwrap();
1653        assert_eq!(schema.fields.len(), 1);
1654        assert!(matches!(
1655            schema.fields[0].kind,
1656            FieldKind::EditedNumeric { .. }
1657        ));
1658    }
1659
1660    #[test]
1661    fn test_sign_clause_without_separate_rejected() {
1662        // SIGN LEADING without SEPARATE is invalid — overpunching is handled by S in PIC.
1663        // Skip when sign_separate feature is disabled by env (e.g. COPYBOOK_FF_SIGN_SEPARATE=0).
1664        let flags = FeatureFlags::from_env();
1665        if !flags.is_enabled(Feature::SignSeparate) {
1666            return;
1667        }
1668        let input = "01 AMOUNT PIC S9(5) SIGN LEADING.";
1669        let result = parse(input);
1670
1671        assert!(
1672            result.is_err(),
1673            "SIGN clause without SEPARATE should be rejected"
1674        );
1675        let error = result.unwrap_err();
1676        // SIGN SEPARATE is enabled by default; rejection is a syntax error, not feature-flag error
1677        assert_eq!(error.code, ErrorCode::CBKP001_SYNTAX);
1678    }
1679
1680    #[test]
1681    fn test_sign_leading_separate_accepted() {
1682        // SIGN IS LEADING SEPARATE is always accepted (promoted to stable)
1683        // Skip when sign_separate feature is disabled by env (e.g. COPYBOOK_FF_SIGN_SEPARATE=0).
1684        let flags = FeatureFlags::from_env();
1685        if !flags.is_enabled(Feature::SignSeparate) {
1686            return;
1687        }
1688        let input = "01 AMOUNT PIC S9(5) SIGN IS LEADING SEPARATE.";
1689        let result = parse(input);
1690        assert!(
1691            result.is_ok(),
1692            "SIGN IS LEADING SEPARATE should be accepted"
1693        );
1694    }
1695
1696    #[test]
1697    fn test_sign_trailing_separate_accepted() {
1698        // SIGN TRAILING SEPARATE is always accepted (promoted to stable)
1699        // Skip when sign_separate feature is disabled by env (e.g. COPYBOOK_FF_SIGN_SEPARATE=0).
1700        let flags = FeatureFlags::from_env();
1701        if !flags.is_enabled(Feature::SignSeparate) {
1702            return;
1703        }
1704        let input = "01 AMOUNT PIC S9(5) SIGN TRAILING SEPARATE.";
1705        let result = parse(input);
1706        assert!(result.is_ok(), "SIGN TRAILING SEPARATE should be accepted");
1707    }
1708
1709    #[test]
1710    fn test_schema_fingerprint() {
1711        let input = "01 CUSTOMER-ID PIC X(10).";
1712        let schema = parse(input).unwrap();
1713
1714        // Should have a non-empty fingerprint
1715        assert!(!schema.fingerprint.is_empty());
1716        assert_ne!(schema.fingerprint, "placeholder");
1717
1718        // Same input should produce same fingerprint
1719        let schema2 = parse(input).unwrap();
1720        assert_eq!(schema.fingerprint, schema2.fingerprint);
1721    }
1722
1723    #[test]
1724    fn test_duplicate_name_handling() {
1725        let input = r"
172601 RECORD-A.
1727   05 FIELD-NAME PIC X(10).
1728   05 FIELD-NAME PIC 9(5).
1729";
1730        let schema = parse(input).unwrap();
1731
1732        // Should have one root field with hierarchical structure
1733        assert_eq!(schema.fields.len(), 1);
1734        let root = &schema.fields[0];
1735        assert_eq!(root.name, "RECORD-A");
1736        assert!(matches!(root.kind, FieldKind::Group));
1737
1738        // Root should have 2 children with disambiguated names
1739        assert_eq!(root.children.len(), 2);
1740        assert_eq!(root.children[0].name, "FIELD-NAME");
1741        assert_eq!(root.children[1].name, "FIELD-NAME__dup2");
1742    }
1743
1744    #[test]
1745    fn test_parent_child_same_name_does_not_get_dup_suffix() {
1746        let input = "01 U-J.\n   05 U-J PIC X(5).";
1747        let schema = parse(input).unwrap();
1748        let group = &schema.fields[0];
1749        let child = &group.children[0];
1750        assert_eq!(child.name, "U-J");
1751        assert_eq!(child.path, "U-J.U-J");
1752    }
1753
1754    #[test]
1755    fn test_odo_validation() {
1756        let input = r"
175701 COUNTER PIC 9(3).
175801 ARRAY-FIELD PIC X(10) OCCURS 5 TIMES DEPENDING ON COUNTER.
1759";
1760        let result = parse(input);
1761
1762        // Should succeed with valid ODO structure
1763        assert!(result.is_ok());
1764        let schema = result.unwrap();
1765        assert_eq!(schema.fields.len(), 2);
1766
1767        // Check ODO field
1768        let odo_field = &schema.fields[1];
1769        if let Some(Occurs::ODO {
1770            max, counter_path, ..
1771        }) = &odo_field.occurs
1772        {
1773            assert_eq!(*max, 5);
1774            assert_eq!(counter_path, "COUNTER");
1775        } else {
1776            panic!("Expected ODO occurs, got {:?}", odo_field.occurs);
1777        }
1778    }
1779
1780    #[test]
1781    fn test_redefines_validation() {
1782        let input = r"
178301 FIELD-A PIC X(10).
178401 FIELD-B REDEFINES FIELD-A PIC 9(10).
1785";
1786        let result = parse(input);
1787
1788        // Should succeed with valid REDEFINES
1789        assert!(result.is_ok());
1790        let schema = result.unwrap();
1791        assert_eq!(schema.fields.len(), 2);
1792
1793        let field_b = &schema.fields[1];
1794        assert_eq!(field_b.redefines_of, Some("FIELD-A".to_string()));
1795    }
1796
1797    #[test]
1798    fn test_hierarchical_structure() {
1799        let input = r"
180001 CUSTOMER-RECORD.
1801   05 CUSTOMER-ID PIC X(10).
1802   05 CUSTOMER-NAME.
1803      10 FIRST-NAME PIC X(20).
1804      10 LAST-NAME PIC X(30).
1805   05 CUSTOMER-ADDRESS.
1806      10 STREET PIC X(38).
1807      10 CITY PIC X(30).
1808";
1809        let schema = parse(input).unwrap();
1810
1811        // Should have one root field
1812        assert_eq!(schema.fields.len(), 1);
1813        let root = &schema.fields[0];
1814        assert_eq!(root.name, "CUSTOMER-RECORD");
1815        assert!(matches!(root.kind, FieldKind::Group));
1816
1817        // Root should have 3 children
1818        assert_eq!(root.children.len(), 3);
1819
1820        // Check first child
1821        let customer_id = &root.children[0];
1822        assert_eq!(customer_id.name, "CUSTOMER-ID");
1823        assert_eq!(customer_id.path, "CUSTOMER-RECORD.CUSTOMER-ID");
1824        assert!(matches!(customer_id.kind, FieldKind::Alphanum { len: 10 }));
1825
1826        // Check second child (group)
1827        let customer_name = &root.children[1];
1828        assert_eq!(customer_name.name, "CUSTOMER-NAME");
1829        assert_eq!(customer_name.path, "CUSTOMER-RECORD.CUSTOMER-NAME");
1830        assert!(matches!(customer_name.kind, FieldKind::Group));
1831        assert_eq!(customer_name.children.len(), 2);
1832
1833        // Check nested children
1834        let first_name = &customer_name.children[0];
1835        assert_eq!(first_name.name, "FIRST-NAME");
1836        assert_eq!(first_name.path, "CUSTOMER-RECORD.CUSTOMER-NAME.FIRST-NAME");
1837    }
1838
1839    #[test]
1840    fn test_sha256_fingerprint() {
1841        let input = "01 CUSTOMER-ID PIC X(10).";
1842        let schema = parse(input).unwrap();
1843
1844        // Should have a SHA-256 fingerprint (64 hex characters)
1845        assert_eq!(schema.fingerprint.len(), 64);
1846        assert!(schema.fingerprint.chars().all(|c| c.is_ascii_hexdigit()));
1847
1848        // Same input should produce same fingerprint
1849        let schema2 = parse(input).unwrap();
1850        assert_eq!(schema.fingerprint, schema2.fingerprint);
1851
1852        // Different input should produce different fingerprint
1853        let input2 = "01 CUSTOMER-ID PIC X(20).";
1854        let schema3 = parse(input2).unwrap();
1855        assert_ne!(schema.fingerprint, schema3.fingerprint);
1856    }
1857
1858    #[test]
1859    fn test_invalid_redefines_target() {
1860        let input = r"
186101 FIELD-A PIC X(10).
186201 FIELD-B REDEFINES NONEXISTENT PIC 9(10).
1863";
1864        let result = parse(input);
1865
1866        // Should fail with invalid REDEFINES target
1867        assert!(result.is_err());
1868        assert!(matches!(
1869            result.unwrap_err().code,
1870            ErrorCode::CBKP001_SYNTAX
1871        ));
1872    }
1873
1874    #[test]
1875    fn test_blank_when_zero() {
1876        let input = "01 AMOUNT PIC 9(5) BLANK WHEN ZERO.";
1877        let schema = parse(input).unwrap();
1878
1879        assert_eq!(schema.fields.len(), 1);
1880        let field = &schema.fields[0];
1881        assert!(field.blank_when_zero);
1882    }
1883
1884    #[test]
1885    fn test_synchronized_field() {
1886        let input = "01 BINARY-FIELD PIC 9(5) USAGE COMP SYNCHRONIZED.";
1887        let schema = parse(input).unwrap();
1888
1889        assert_eq!(schema.fields.len(), 1);
1890        let field = &schema.fields[0];
1891        assert!(field.synchronized);
1892        assert!(matches!(field.kind, FieldKind::BinaryInt { .. }));
1893    }
1894
1895    #[test]
1896    fn test_edited_pic_ambiguous_field_name() {
1897        // "Z-Z" is tokenized as EditedPic but is a valid COBOL data name
1898        let input = "01 Z-Z PIC X(10).";
1899        let schema = parse(input).expect("should parse Z-Z as field name");
1900        assert_eq!(schema.fields.len(), 1);
1901        assert_eq!(schema.fields[0].name, "Z-Z");
1902    }
1903
1904    #[test]
1905    fn test_pic_clause_ambiguous_field_name() {
1906        // "ZZ" is tokenized as PicClause/EditedPic but is a valid COBOL data name
1907        let input = "01 ZZ PIC X(5).";
1908        let schema = parse(input).expect("should parse ZZ as field name");
1909        assert_eq!(schema.fields.len(), 1);
1910        assert_eq!(schema.fields[0].name, "ZZ");
1911    }
1912
1913    #[test]
1914    fn test_redefines_target_ambiguous_name() {
1915        let input = "01 Z-Z PIC X(10).\n01 ALT REDEFINES Z-Z PIC X(10).";
1916        let schema = parse(input).expect("should parse REDEFINES Z-Z");
1917        assert_eq!(schema.fields[1].redefines_of.as_deref(), Some("Z-Z"));
1918    }
1919
1920    #[test]
1921    fn test_depending_on_ambiguous_name() {
1922        let input = "01 Z-Z PIC 9(3).\n01 ITEMS PIC X(5) OCCURS 5 TIMES DEPENDING ON Z-Z.";
1923        let schema = parse(input).expect("should parse DEPENDING ON Z-Z");
1924        let items = &schema.fields[1];
1925        match &items.occurs {
1926            Some(Occurs::ODO { counter_path, .. }) => {
1927                assert_eq!(counter_path, "Z-Z");
1928            }
1929            other => panic!("Expected ODO occurs, got {:?}", other),
1930        }
1931    }
1932}