Skip to main content

logicaffeine_language/analysis/
discovery.rs

1//! Discovery pass for type and policy extraction.
2//!
3//! Runs before main parsing to scan tokens for type and policy definitions.
4//! Populates [`TypeRegistry`] and [`PolicyRegistry`] for use during parsing.
5//!
6//! # Discovery Targets
7//!
8//! | Block | Pattern | Result |
9//! |-------|---------|--------|
10//! | `## Definition` | "A Stack is a generic collection." | `TypeDef::Generic` |
11//! | `## Definition` | "A User is a structure." | `TypeDef::Struct` |
12//! | `## Definition` | "A Shape is an enum." | `TypeDef::Enum` |
13//! | `## Policy` | "A user can publish if they are admin." | `CapabilityDef` |
14//!
15//! # Key Function
16//!
17//! [`DiscoveryPass::run`] - Execute the discovery pass and return registries.
18
19use crate::token::{Token, TokenType, BlockType};
20use logicaffeine_base::{Interner, Symbol};
21use super::registry::{TypeRegistry, TypeDef, FieldDef, FieldType, VariantDef};
22use super::policy::{PolicyRegistry, PredicateDef, CapabilityDef, PolicyCondition};
23use super::dependencies::scan_dependencies;
24
25/// Result of running the discovery pass
26pub struct DiscoveryResult {
27    pub types: TypeRegistry,
28    pub policies: PolicyRegistry,
29}
30
31/// Discovery pass that scans tokens before main parsing to build a TypeRegistry.
32///
33/// This pass looks for type definitions in `## Definition` blocks:
34/// - "A Stack is a generic collection." → Generic type
35/// - "A User is a structure." → Struct type
36/// - "A Shape is an enum." → Enum type
37///
38/// Phase 50: Also scans `## Policy` blocks for security predicates and capabilities.
39pub struct DiscoveryPass<'a> {
40    tokens: &'a [Token],
41    pos: usize,
42    interner: &'a mut Interner,
43}
44
45impl<'a> DiscoveryPass<'a> {
46    pub fn new(tokens: &'a [Token], interner: &'a mut Interner) -> Self {
47        Self { tokens, pos: 0, interner }
48    }
49
50    /// Run discovery pass, returning populated TypeRegistry
51    /// (Backward compatible - returns only TypeRegistry)
52    pub fn run(&mut self) -> TypeRegistry {
53        self.run_full().types
54    }
55
56    /// Phase 50: Run discovery pass, returning both TypeRegistry and PolicyRegistry
57    pub fn run_full(&mut self) -> DiscoveryResult {
58        let mut type_registry = TypeRegistry::with_primitives(self.interner);
59        let mut policy_registry = PolicyRegistry::new();
60
61        while self.pos < self.tokens.len() {
62            // Look for Definition blocks
63            if self.check_block_header(BlockType::Definition) {
64                self.advance(); // consume ## Definition
65                self.scan_definition_block(&mut type_registry);
66            } else if self.check_block_header(BlockType::TypeDef) {
67                // Inline type definition: ## A Point has: or ## A Color is one of:
68                // The article is part of the block header, so don't skip it
69                self.advance(); // consume ## A/An
70                self.parse_type_definition_inline(&mut type_registry);
71            } else if self.check_block_header(BlockType::Policy) {
72                // Phase 50: Security policy definitions
73                self.advance(); // consume ## Policy
74                self.scan_policy_block(&mut policy_registry);
75            } else if self.check_block_header(BlockType::Requires) {
76                // Requires blocks contain dependency metadata, not type definitions.
77                // Skip to next block header.
78                self.advance(); // consume ## Requires
79                while self.pos < self.tokens.len() {
80                    if matches!(self.tokens.get(self.pos), Some(Token { kind: TokenType::BlockHeader { .. }, .. })) {
81                        break;
82                    }
83                    self.advance();
84                }
85            } else {
86                self.advance();
87            }
88        }
89
90        DiscoveryResult {
91            types: type_registry,
92            policies: policy_registry,
93        }
94    }
95
96    fn check_block_header(&self, expected: BlockType) -> bool {
97        matches!(
98            self.tokens.get(self.pos),
99            Some(Token { kind: TokenType::BlockHeader { block_type }, .. })
100            if *block_type == expected
101        )
102    }
103
104    fn scan_definition_block(&mut self, registry: &mut TypeRegistry) {
105        // Scan until next block header or EOF
106        while self.pos < self.tokens.len() {
107            if matches!(self.peek(), Some(Token { kind: TokenType::BlockHeader { .. }, .. })) {
108                break;
109            }
110
111            // Look for "A [Name] is a..." pattern
112            if self.check_article() {
113                self.try_parse_type_definition(registry);
114            } else {
115                self.advance();
116            }
117        }
118    }
119
120    /// Phase 50: Scan policy block for predicate and capability definitions
121    /// Patterns:
122    /// - "A User is admin if the user's role equals \"admin\"."
123    /// - "A User can publish the Document if the user is admin OR the user equals the document's owner."
124    fn scan_policy_block(&mut self, registry: &mut PolicyRegistry) {
125        while self.pos < self.tokens.len() {
126            if matches!(self.peek(), Some(Token { kind: TokenType::BlockHeader { .. }, .. })) {
127                break;
128            }
129
130            // Skip newlines and indentation
131            if self.check_newline() || self.check_indent() || self.check_dedent() {
132                self.advance();
133                continue;
134            }
135
136            // Look for "A [Type] is [predicate] if..." or "A [Type] can [action] ..."
137            if self.check_article() {
138                self.try_parse_policy_definition(registry);
139            } else {
140                self.advance();
141            }
142        }
143    }
144
145    /// Phase 50: Parse a policy definition
146    fn try_parse_policy_definition(&mut self, registry: &mut PolicyRegistry) {
147        self.advance(); // consume article
148
149        // Get subject type name (e.g., "User")
150        let subject_type = match self.consume_noun_or_proper() {
151            Some(sym) => sym,
152            None => return,
153        };
154
155        // Determine if predicate ("is admin") or capability ("can publish")
156        if self.check_copula() {
157            // "A User is admin if..."
158            self.advance(); // consume "is"
159
160            // Get predicate name (e.g., "admin")
161            let predicate_name = match self.consume_noun_or_proper() {
162                Some(sym) => sym,
163                None => return,
164            };
165
166            // Expect "if"
167            if !self.check_word("if") {
168                self.skip_to_period();
169                return;
170            }
171            self.advance(); // consume "if"
172
173            // Handle multi-line condition (colon followed by indented lines)
174            if self.check_colon() {
175                self.advance();
176            }
177            if self.check_newline() {
178                self.advance();
179            }
180            if self.check_indent() {
181                self.advance();
182            }
183
184            // Parse condition
185            let condition = self.parse_policy_condition(subject_type, None);
186
187            registry.register_predicate(PredicateDef {
188                subject_type,
189                predicate_name,
190                condition,
191            });
192
193            self.skip_to_period();
194        } else if self.check_word("can") {
195            // "A User can publish the Document if..."
196            self.advance(); // consume "can"
197
198            // Get action name (e.g., "publish", "edit"). For a verb action take its
199            // LEMMA — that is exactly what the `Check ... can <verb>` site resolves
200            // (parser), so the registered and looked-up symbols agree. Using
201            // `consume_noun_or_proper` first would capture the verb LEXEME instead and
202            // every capability check would miss ("No capability 'Edit' defined").
203            let action = match self.peek() {
204                Some(Token { kind: TokenType::Verb { lemma, .. }, .. }) => {
205                    let sym = *lemma;
206                    self.advance();
207                    sym
208                }
209                _ => match self.consume_noun_or_proper() {
210                    Some(sym) => sym,
211                    None => return,
212                },
213            };
214
215            // Skip "the" article if present
216            if self.check_article() {
217                self.advance();
218            }
219
220            // Get object type (e.g., "Document")
221            let object_type = match self.consume_noun_or_proper() {
222                Some(sym) => sym,
223                None => return,
224            };
225
226            // Expect "if"
227            if !self.check_word("if") {
228                self.skip_to_period();
229                return;
230            }
231            self.advance(); // consume "if"
232
233            // Parse condition (may include colon for multi-line)
234            if self.check_colon() {
235                self.advance();
236            }
237            if self.check_newline() {
238                self.advance();
239            }
240            if self.check_indent() {
241                self.advance();
242            }
243
244            let condition = self.parse_policy_condition(subject_type, Some(object_type));
245
246            registry.register_capability(CapabilityDef {
247                subject_type,
248                action,
249                object_type,
250                condition,
251            });
252
253            // Skip to end of definition (may span multiple lines)
254            self.skip_policy_definition();
255        } else {
256            self.skip_to_period();
257        }
258    }
259
260    /// Phase 50: Parse a policy condition
261    /// Handles: field comparisons, predicate references, and OR/AND combinators
262    fn parse_policy_condition(&mut self, subject_type: Symbol, object_type: Option<Symbol>) -> PolicyCondition {
263        // Left-fold the FULL n-ary chain. Returning after the first connective
264        // silently dropped every conjunct/disjunct past the second atom, making
265        // an AND-policy over-permissive and an OR-policy over-restrictive.
266        let mut acc = self.parse_atomic_condition(subject_type, object_type);
267
268        // Check for OR/AND combinators
269        loop {
270            // Skip newlines between conditions
271            while self.check_newline() {
272                self.advance();
273            }
274
275            // Handle ", AND" or ", OR" patterns
276            if self.check_comma() {
277                self.advance(); // consume comma
278                // Skip whitespace after comma
279                while self.check_newline() {
280                    self.advance();
281                }
282            }
283
284            if self.check_word("AND") {
285                self.advance();
286                // Skip newlines after AND
287                while self.check_newline() {
288                    self.advance();
289                }
290                let right = self.parse_atomic_condition(subject_type, object_type);
291                acc = PolicyCondition::And(Box::new(acc), Box::new(right));
292            } else if self.check_word("OR") {
293                self.advance();
294                // Skip newlines after OR
295                while self.check_newline() {
296                    self.advance();
297                }
298                let right = self.parse_atomic_condition(subject_type, object_type);
299                acc = PolicyCondition::Or(Box::new(acc), Box::new(right));
300            } else {
301                break;
302            }
303        }
304
305        acc
306    }
307
308    /// Phase 50: Parse an atomic condition
309    fn parse_atomic_condition(&mut self, subject_type: Symbol, object_type: Option<Symbol>) -> PolicyCondition {
310        // Skip "The" article if present
311        if self.check_article() {
312            self.advance();
313        }
314
315        // Get the subject reference (e.g., "user" or "user's role")
316        let subject_ref = match self.consume_noun_or_proper() {
317            Some(sym) => sym,
318            None => return PolicyCondition::FieldEquals {
319                field: self.interner.intern("unknown"),
320                value: self.interner.intern("unknown"),
321                is_string_literal: false,
322            },
323        };
324
325        // Check if it's a field access ("'s role") or a predicate ("is admin")
326        if self.check_possessive() {
327            self.advance(); // consume "'s"
328
329            // Get field name
330            let field = match self.consume_noun_or_proper() {
331                Some(sym) => sym,
332                None => return PolicyCondition::FieldEquals {
333                    field: self.interner.intern("unknown"),
334                    value: self.interner.intern("unknown"),
335                    is_string_literal: false,
336                },
337            };
338
339            // Expect "equals"
340            if self.check_word("equals") {
341                self.advance();
342
343                // First try an object-field RHS: `the document's owner`, giving a
344                // cross-field comparison `self.<field> == object.<obj_field>`. Only a
345                // literal/identifier RHS falls through to `FieldEquals`.
346                let checkpoint = self.pos;
347                if self.check_article() {
348                    self.advance();
349                }
350                if let Some(obj_ref) = self.consume_noun_or_proper() {
351                    if self.check_possessive() {
352                        self.advance(); // consume "'s"
353                        if let Some(obj_field) = self.consume_noun_or_proper() {
354                            return PolicyCondition::SubjectFieldEqualsObjectField {
355                                subject_field: field,
356                                object: obj_ref,
357                                object_field: obj_field,
358                            };
359                        }
360                    }
361                }
362                // Not an object-field reference — rewind and take a plain value.
363                self.pos = checkpoint;
364                let (value, is_string_literal) = self.consume_value();
365
366                return PolicyCondition::FieldEquals { field, value, is_string_literal };
367            }
368        } else if self.check_copula() {
369            // "user is admin"
370            self.advance(); // consume "is"
371
372            // Get predicate name
373            let predicate = match self.consume_noun_or_proper() {
374                Some(sym) => sym,
375                None => return PolicyCondition::FieldEquals {
376                    field: self.interner.intern("unknown"),
377                    value: self.interner.intern("unknown"),
378                    is_string_literal: false,
379                },
380            };
381
382            return PolicyCondition::Predicate {
383                subject: subject_ref,
384                predicate,
385            };
386        } else if self.check_word("equals") {
387            // "user equals the document's owner"
388            self.advance(); // consume "equals"
389
390            // Skip "the" if present
391            if self.check_article() {
392                self.advance();
393            }
394
395            // Check for object field reference: "document's owner"
396            if let Some(obj_ref) = self.consume_noun_or_proper() {
397                if self.check_possessive() {
398                    self.advance(); // consume "'s"
399                    if let Some(field) = self.consume_noun_or_proper() {
400                        return PolicyCondition::ObjectFieldEquals {
401                            subject: subject_ref,
402                            object: obj_ref,
403                            field,
404                        };
405                    }
406                }
407            }
408        }
409
410        // Fallback: unknown condition
411        PolicyCondition::FieldEquals {
412            field: self.interner.intern("unknown"),
413            value: self.interner.intern("unknown"),
414            is_string_literal: false,
415        }
416    }
417
418    /// Consume a value (string literal or identifier), returning the symbol and whether it was a string literal
419    fn consume_value(&mut self) -> (Symbol, bool) {
420        if let Some(Token { kind: TokenType::StringLiteral(sym), .. }) = self.peek() {
421            let s = *sym;
422            self.advance();
423            (s, true)
424        } else if let Some(sym) = self.consume_noun_or_proper() {
425            (sym, false)
426        } else {
427            (self.interner.intern("unknown"), false)
428        }
429    }
430
431    /// Check for possessive marker ('s)
432    fn check_possessive(&self) -> bool {
433        matches!(self.peek(), Some(Token { kind: TokenType::Possessive, .. }))
434    }
435
436    /// Skip to end of a multi-line policy definition
437    fn skip_policy_definition(&mut self) {
438        let mut depth = 0;
439        while self.pos < self.tokens.len() {
440            if self.check_indent() {
441                depth += 1;
442            } else if self.check_dedent() {
443                if depth == 0 {
444                    break;
445                }
446                depth -= 1;
447            }
448            if self.check_period() && depth == 0 {
449                self.advance();
450                break;
451            }
452            if matches!(self.peek(), Some(Token { kind: TokenType::BlockHeader { .. }, .. })) {
453                break;
454            }
455            self.advance();
456        }
457    }
458
459    /// Parse inline type definition where article was part of block header (## A Point has:)
460    fn parse_type_definition_inline(&mut self, registry: &mut TypeRegistry) {
461        // Don't skip article - it was part of the block header
462        self.parse_type_definition_body(registry);
463    }
464
465    fn try_parse_type_definition(&mut self, registry: &mut TypeRegistry) {
466        self.advance(); // skip article
467        self.parse_type_definition_body(registry);
468    }
469
470    fn parse_type_definition_body(&mut self, registry: &mut TypeRegistry) {
471        // Phase 47/49: Check for pre-type modifiers: "A portable Config has:" or "A shared Config has:"
472        let mut is_portable = false;
473        let mut is_shared = false;
474        loop {
475            if self.check_portable() {
476                is_portable = true;
477                self.advance();
478            } else if self.check_shared() {
479                is_shared = true;
480                self.advance();
481            } else {
482                break;
483            }
484        }
485
486        if let Some(name_sym) = self.consume_noun_or_proper() {
487            // Phase 34: Check for "of [T]" which indicates user-defined generic
488            let type_params = if self.check_preposition("of") {
489                self.advance(); // consume "of"
490                self.parse_type_params()
491            } else {
492                vec![]
493            };
494            if self.check_copula() {
495                let copula_pos = self.pos;
496                self.advance(); // consume is/are
497
498                // Check for modifiers in any order (e.g., "is Shared and Portable and")
499                loop {
500                    if self.check_portable() {
501                        self.advance(); // consume "Portable"
502                        is_portable = true;
503                        if self.check_word("and") {
504                            self.advance(); // consume "and"
505                        }
506                    } else if self.check_shared() {
507                        self.advance(); // consume "Shared"
508                        is_shared = true;
509                        if self.check_word("and") {
510                            self.advance(); // consume "and"
511                        }
512                    } else {
513                        break;
514                    }
515                }
516
517                // If no modifiers were found, restore position
518                if !is_portable && !is_shared {
519                    self.pos = copula_pos;
520                }
521            }
522
523            // Phase 31/34: Check for "has:" which indicates struct with fields
524            // Pattern: "A Point has:" or "A Box of [T] has:" or "A Message is Portable and has:"
525            if self.check_word("has") {
526                self.advance(); // consume "has"
527                if self.check_colon() {
528                    self.advance(); // consume ":"
529                    // Skip newline if present
530                    if self.check_newline() {
531                        self.advance();
532                    }
533                    if self.check_indent() {
534                        self.advance(); // consume INDENT
535                        let fields = self.parse_struct_fields_with_params(&type_params);
536                        registry.register(name_sym, TypeDef::Struct { fields, generics: type_params, is_portable, is_shared });
537                        return;
538                    }
539                }
540            }
541
542            // Check for "is either:" or "is one of:" pattern (Phase 33/34: Sum types with variants)
543            if self.check_copula() {
544                self.advance(); // consume is/are
545
546                // Phase 33: Check for "either:" or "one of:" pattern
547                let is_enum_pattern = if self.check_either() {
548                    self.advance(); // consume "either"
549                    true
550                } else if self.check_word("one") {
551                    self.advance(); // consume "one"
552                    if self.check_word("of") {
553                        self.advance(); // consume "of"
554                        true
555                    } else {
556                        false
557                    }
558                } else {
559                    false
560                };
561
562                if is_enum_pattern {
563                    if self.check_colon() {
564                        self.advance(); // consume ":"
565                        // Skip newline if present
566                        if self.check_newline() {
567                            self.advance();
568                        }
569                        if self.check_indent() {
570                            self.advance(); // consume INDENT
571                            let variants = self.parse_enum_variants_with_params(&type_params);
572                            registry.register(name_sym, TypeDef::Enum { variants, generics: type_params, is_portable, is_shared });
573                            return;
574                        }
575                    }
576                }
577
578                if self.check_article() {
579                    self.advance(); // consume a/an
580
581                    // Look for type indicators
582                    if self.check_word("generic") {
583                        registry.register(name_sym, TypeDef::Generic { param_count: 1 });
584                        self.skip_to_period();
585                    } else if self.check_word("record") || self.check_word("struct") || self.check_word("structure") {
586                        registry.register(name_sym, TypeDef::Struct { fields: vec![], generics: vec![], is_portable: false, is_shared: false });
587                        self.skip_to_period();
588                    } else if self.check_word("sum") || self.check_word("enum") || self.check_word("choice") {
589                        registry.register(name_sym, TypeDef::Enum { variants: vec![], generics: vec![], is_portable: false, is_shared: false });
590                        self.skip_to_period();
591                    }
592                }
593            } else if !type_params.is_empty() {
594                // "A Stack of [Things] is..." - old generic syntax, still supported
595                registry.register(name_sym, TypeDef::Generic { param_count: type_params.len() });
596                self.skip_to_period();
597            }
598        }
599    }
600
601    /// Phase 33/34: Parse enum variants in "is either:" block
602    /// Each variant: "A VariantName." or "A VariantName with a field, which is Type."
603    /// or concise: "A VariantName (field: Type)."
604    fn parse_enum_variants_with_params(&mut self, type_params: &[Symbol]) -> Vec<VariantDef> {
605        let mut variants = Vec::new();
606
607        while self.pos < self.tokens.len() {
608            // Exit on dedent or next block
609            if self.check_dedent() {
610                self.advance();
611                break;
612            }
613            if matches!(self.peek(), Some(Token { kind: TokenType::BlockHeader { .. }, .. })) {
614                break;
615            }
616
617            // Skip newlines between variants
618            if self.check_newline() {
619                self.advance();
620                continue;
621            }
622
623            // Parse variant: "A VariantName [with fields | (field: Type)]." or bare "VariantName."
624            // Optionally consume article (a/an) if present
625            if self.check_article() {
626                self.advance(); // consume "A"/"An"
627            }
628
629            // Try to parse variant name (noun or proper name)
630            if let Some(variant_name) = self.consume_noun_or_proper() {
631                // Check for payload fields
632                let fields = if self.check_word("with") {
633                    // Natural syntax: "A Circle with a radius, which is Int."
634                    self.parse_variant_fields_natural_with_params(type_params)
635                } else if self.check_lparen() {
636                    // Concise syntax: "A Circle (radius: Int)."
637                    self.parse_variant_fields_concise_with_params(type_params)
638                } else {
639                    // Unit variant: "A Point." or "Point."
640                    vec![]
641                };
642
643                variants.push(VariantDef {
644                    name: variant_name,
645                    fields,
646                });
647
648                // Consume period
649                if self.check_period() {
650                    self.advance();
651                }
652            } else {
653                self.advance(); // skip malformed token
654            }
655        }
656
657        variants
658    }
659
660    /// Phase 33: Parse enum variants (backward compat wrapper)
661    fn parse_enum_variants(&mut self) -> Vec<VariantDef> {
662        self.parse_enum_variants_with_params(&[])
663    }
664
665    /// Parse variant fields in natural syntax.
666    /// Supports multiple syntaxes:
667    /// - "with a radius, which is Int." (verbose natural)
668    /// - "with radius Int" (concise natural - no article/comma)
669    fn parse_variant_fields_natural_with_params(&mut self, type_params: &[Symbol]) -> Vec<FieldDef> {
670        let mut fields = Vec::new();
671
672        // "with" has already been detected, consume it
673        self.advance();
674
675        loop {
676            // Skip article (optional)
677            if self.check_article() {
678                self.advance();
679            }
680
681            // Get field name
682            if let Some(field_name) = self.consume_noun_or_proper() {
683                // Support multiple type annotation patterns:
684                // 1. ", which is Type" (verbose)
685                // 2. " Type" (concise - just a type name after field name)
686                let ty = if self.check_comma() {
687                    self.advance(); // consume ","
688                    // Consume "which"
689                    if self.check_word("which") {
690                        self.advance();
691                    }
692                    // Consume "is"
693                    if self.check_copula() {
694                        self.advance();
695                    }
696                    self.consume_field_type_with_params(type_params)
697                } else if self.check_colon() {
698                    self.advance(); // consume ":"
699                    self.consume_field_type_with_params(type_params)
700                } else {
701                    // Concise syntax: "radius Int" - type immediately follows field name
702                    self.consume_field_type_with_params(type_params)
703                };
704
705                fields.push(FieldDef {
706                    name: field_name,
707                    ty,
708                    is_public: true, // Variant fields are always public
709                });
710
711                // Check for "and" to continue: "and height Int"
712                // May have comma before "and"
713                if self.check_comma() {
714                    self.advance(); // consume comma before "and"
715                }
716                if self.check_word("and") {
717                    self.advance();
718                    continue;
719                }
720            }
721            break;
722        }
723
724        fields
725    }
726
727    /// Backward compat wrapper
728    fn parse_variant_fields_natural(&mut self) -> Vec<FieldDef> {
729        self.parse_variant_fields_natural_with_params(&[])
730    }
731
732    /// Parse variant fields in concise syntax: "(radius: Int)" or "(width: Int, height: Int)"
733    fn parse_variant_fields_concise_with_params(&mut self, type_params: &[Symbol]) -> Vec<FieldDef> {
734        let mut fields = Vec::new();
735
736        // Consume "("
737        self.advance();
738
739        loop {
740            // Get field name
741            if let Some(field_name) = self.consume_noun_or_proper() {
742                // Expect ": Type" pattern
743                let ty = if self.check_colon() {
744                    self.advance(); // consume ":"
745                    self.consume_field_type_with_params(type_params)
746                } else {
747                    FieldType::Primitive(self.interner.intern("Unknown"))
748                };
749
750                fields.push(FieldDef {
751                    name: field_name,
752                    ty,
753                    is_public: true, // Variant fields are always public
754                });
755
756                // Check for "," to continue
757                if self.check_comma() {
758                    self.advance();
759                    continue;
760                }
761            }
762            break;
763        }
764
765        // Consume ")"
766        if self.check_rparen() {
767            self.advance();
768        }
769
770        fields
771    }
772
773    /// Backward compat wrapper
774    fn parse_variant_fields_concise(&mut self) -> Vec<FieldDef> {
775        self.parse_variant_fields_concise_with_params(&[])
776    }
777
778    /// Parse struct fields in "has:" block
779    /// Each field: "a [public] name, which is Type."
780    fn parse_struct_fields_with_params(&mut self, type_params: &[Symbol]) -> Vec<FieldDef> {
781        let mut fields = Vec::new();
782
783        while self.pos < self.tokens.len() {
784            // Exit on dedent or next block
785            if self.check_dedent() {
786                self.advance();
787                break;
788            }
789            if matches!(self.peek(), Some(Token { kind: TokenType::BlockHeader { .. }, .. })) {
790                break;
791            }
792
793            // Skip newlines between fields
794            if self.check_newline() {
795                self.advance();
796                continue;
797            }
798
799            // Parse field: "a [public] name, which is Type." or "name: Type." (no article)
800            // Check for article (optional for concise syntax)
801            let has_article = self.check_article();
802            if has_article {
803                self.advance(); // consume "a"/"an"
804            }
805
806            // Check for "public" modifier
807            let has_public_keyword = if self.check_word("public") {
808                self.advance();
809                true
810            } else {
811                false
812            };
813            // Visibility determined later based on syntax used
814            let mut is_public = has_public_keyword;
815
816            // Get field name - try to parse if we had article OR if next token looks like identifier
817            if let Some(field_name) = self.consume_noun_or_proper() {
818                // Support both syntaxes:
819                // 1. "name: Type." (concise) - public by default
820                // 2. "name, which is Type." (natural) - public by default
821                let ty = if self.check_colon() {
822                    // Concise syntax: "x: Int" - public by default
823                    is_public = true;
824                    self.advance(); // consume ":"
825                    self.consume_field_type_with_params(type_params)
826                } else if self.check_comma() {
827                    // Natural syntax: "name, which is Type" - also public by default
828                    is_public = true;
829                    self.advance(); // consume ","
830                    // Consume "which"
831                    if self.check_word("which") {
832                        self.advance();
833                    }
834                    // Consume "is"
835                    if self.check_copula() {
836                        self.advance();
837                    }
838                    self.consume_field_type_with_params(type_params)
839                } else if !has_article {
840                    // No colon and no article - this wasn't a field, skip
841                    continue;
842                } else {
843                    // Fallback: unknown type
844                    FieldType::Primitive(self.interner.intern("Unknown"))
845                };
846
847                fields.push(FieldDef {
848                    name: field_name,
849                    ty,
850                    is_public,
851                });
852
853                // Consume period
854                if self.check_period() {
855                    self.advance();
856                }
857            } else if !has_article {
858                // Didn't have article and couldn't get field name - skip this token
859                self.advance();
860            }
861        }
862
863        fields
864    }
865
866    /// Backward compat wrapper
867    fn parse_struct_fields(&mut self) -> Vec<FieldDef> {
868        self.parse_struct_fields_with_params(&[])
869    }
870
871    /// Parse a field type reference
872    fn consume_field_type(&mut self) -> FieldType {
873        // Bug fix: Handle parenthesized type expressions: "Seq of (Seq of Int)"
874        if self.check_lparen() {
875            self.advance(); // consume "("
876            let inner_type = self.consume_field_type();
877            if self.check_rparen() {
878                self.advance(); // consume ")"
879            }
880            return inner_type;
881        }
882
883        // Skip article if present (e.g., "a Tally" -> "Tally")
884        if self.check_article() {
885            self.advance();
886        }
887
888        if let Some(name) = self.consume_noun_or_proper() {
889            let name_str = self.interner.resolve(name);
890
891            // Phase 49c: Check for bias/algorithm modifier on SharedSet: "SharedSet (AddWins) of T"
892            let modified_name = if name_str == "SharedSet" || name_str == "ORSet" {
893                if self.check_lparen() {
894                    self.advance(); // consume "("
895                    let modifier = if self.check_removewins() {
896                        self.advance(); // consume "RemoveWins"
897                        Some("SharedSet_RemoveWins")
898                    } else if self.check_addwins() {
899                        self.advance(); // consume "AddWins"
900                        Some("SharedSet_AddWins")
901                    } else {
902                        None
903                    };
904                    if self.check_rparen() {
905                        self.advance(); // consume ")"
906                    }
907                    modifier.map(|m| self.interner.intern(m))
908                } else {
909                    None
910                }
911            } else if name_str == "SharedSequence" {
912                // Phase 49c: Check for algorithm modifier on SharedSequence: "SharedSequence (YATA) of T"
913                if self.check_lparen() {
914                    self.advance(); // consume "("
915                    let modifier = if self.check_yata() {
916                        self.advance(); // consume "YATA"
917                        Some("SharedSequence_YATA")
918                    } else {
919                        None
920                    };
921                    if self.check_rparen() {
922                        self.advance(); // consume ")"
923                    }
924                    modifier.map(|m| self.interner.intern(m))
925                } else {
926                    None
927                }
928            } else {
929                None
930            };
931
932            // Use modified name if we found a modifier, otherwise use original
933            let final_name = modified_name.unwrap_or(name);
934            let final_name_str = self.interner.resolve(final_name);
935
936            // Phase 49c: Handle "SharedMap from K to V" / "ORMap from K to V" syntax
937            if (final_name_str == "SharedMap" || final_name_str == "ORMap") && self.check_from() {
938                self.advance(); // consume "from"
939                let key_type = self.consume_field_type();
940                // Expect "to" (can be TokenType::To or preposition)
941                if self.check_to() {
942                    self.advance(); // consume "to"
943                }
944                let value_type = self.consume_field_type();
945                return FieldType::Generic { base: final_name, params: vec![key_type, value_type] };
946            }
947
948            // Check for generic: "List of Int", "Seq of Text", "Map of K to V"
949            if self.check_preposition("of") {
950                // Check if this is a Map type that needs two params (before we start mutating)
951                let is_map_type = final_name_str == "Map" || final_name_str == "HashMap";
952
953                self.advance();
954                let first_param = self.consume_field_type();
955
956                // For Map/HashMap, check for "to" separator to parse second type parameter
957                if is_map_type && self.check_to() {
958                    self.advance(); // consume "to"
959                    let second_param = self.consume_field_type();
960                    return FieldType::Generic { base: final_name, params: vec![first_param, second_param] };
961                }
962
963                return FieldType::Generic { base: final_name, params: vec![first_param] };
964            }
965
966            // Phase 49b: "Divergent T" syntax (no "of" required)
967            if final_name_str == "Divergent" {
968                // Next token should be the inner type
969                let param = self.consume_field_type();
970                return FieldType::Generic { base: final_name, params: vec![param] };
971            }
972
973            // Check if primitive
974            match final_name_str {
975                "Int" | "Nat" | "Text" | "Bool" | "Real" | "Unit" | "Word8" | "Word16" | "Word32"
976                | "Word64" | "Lanes8Word32" | "Lanes4Word32" | "Lanes16Word8" | "Lanes4Word64" | "Lanes16Word16" => {
977                    FieldType::Primitive(final_name)
978                }
979                _ => FieldType::Named(final_name),
980            }
981        } else {
982            FieldType::Primitive(self.interner.intern("Unknown"))
983        }
984    }
985
986    // Helper methods
987    fn peek(&self) -> Option<&Token> {
988        self.tokens.get(self.pos)
989    }
990
991    fn advance(&mut self) {
992        if self.pos < self.tokens.len() {
993            self.pos += 1;
994        }
995    }
996
997    fn check_article(&self) -> bool {
998        match self.peek() {
999            Some(Token { kind: TokenType::Article(_), .. }) => true,
1000            // Also accept ProperName("A") / ProperName("An") which can occur at line starts
1001            Some(Token { kind: TokenType::ProperName(sym), .. }) => {
1002                let text = self.interner.resolve(*sym);
1003                text.eq_ignore_ascii_case("a") || text.eq_ignore_ascii_case("an")
1004            }
1005            _ => false,
1006        }
1007    }
1008
1009    fn check_copula(&self) -> bool {
1010        match self.peek() {
1011            Some(Token { kind: TokenType::Is | TokenType::Are, .. }) => true,
1012            // Also match "is" when tokenized as a verb (common in declarative mode)
1013            Some(Token { kind: TokenType::Verb { lemma, .. }, .. }) => {
1014                let word = self.interner.resolve(*lemma).to_lowercase();
1015                word == "is" || word == "are"
1016            }
1017            _ => false,
1018        }
1019    }
1020
1021    fn check_preposition(&self, word: &str) -> bool {
1022        if let Some(Token { kind: TokenType::Preposition(sym), .. }) = self.peek() {
1023            self.interner.resolve(*sym) == word
1024        } else {
1025            false
1026        }
1027    }
1028
1029    fn consume_noun_or_proper(&mut self) -> Option<Symbol> {
1030        let t = self.peek()?;
1031        match &t.kind {
1032            TokenType::Noun(s) | TokenType::ProperName(s) => {
1033                let sym = *s;
1034                self.advance();
1035                Some(sym)
1036            }
1037            // Phase 31: Also accept Adjective as identifier (for field names like "x")
1038            TokenType::Adjective(s) => {
1039                let sym = *s;
1040                self.advance();
1041                Some(sym)
1042            }
1043            // Accept Adverb as identifier (for field names like "next")
1044            TokenType::Adverb(_) => {
1045                let sym = t.lexeme;
1046                self.advance();
1047                Some(sym)
1048            }
1049            // Phase 47: Accept Performative as type name (for agent messages like "Command")
1050            TokenType::Performative(s) => {
1051                let sym = *s;
1052                self.advance();
1053                Some(sym)
1054            }
1055            // Phase 34: Accept special tokens as identifiers using their lexeme
1056            TokenType::Items | TokenType::Some => {
1057                let sym = t.lexeme;
1058                self.advance();
1059                Some(sym)
1060            }
1061            // Phase 49/50: Accept Verb tokens as identifiers
1062            // - Uppercase verbs like "Setting" are type names
1063            // - Lowercase verbs like "trusted", "privileged" are predicate names
1064            // Use lexeme to preserve the original word (not lemma which strips suffixes)
1065            TokenType::Verb { .. } => {
1066                let sym = t.lexeme;
1067                self.advance();
1068                Some(sym)
1069            }
1070            // Phase 49b: Accept CRDT type tokens as type names
1071            TokenType::Tally => {
1072                self.advance();
1073                Some(self.interner.intern("Tally"))
1074            }
1075            TokenType::SharedSet => {
1076                self.advance();
1077                Some(self.interner.intern("SharedSet"))
1078            }
1079            TokenType::SharedSequence => {
1080                self.advance();
1081                Some(self.interner.intern("SharedSequence"))
1082            }
1083            TokenType::CollaborativeSequence => {
1084                self.advance();
1085                Some(self.interner.intern("CollaborativeSequence"))
1086            }
1087            TokenType::SharedMap => {
1088                self.advance();
1089                Some(self.interner.intern("SharedMap"))
1090            }
1091            TokenType::Divergent => {
1092                self.advance();
1093                Some(self.interner.intern("Divergent"))
1094            }
1095            // Phase 49: Accept Ambiguous tokens (e.g., "name" could be verb or noun)
1096            // Use lexeme to get the original word
1097            TokenType::Ambiguous { .. } => {
1098                let sym = t.lexeme;
1099                self.advance();
1100                Some(sym)
1101            }
1102            // Escape hatch keyword can be a type/identifier name
1103            TokenType::Escape => {
1104                let sym = t.lexeme;
1105                self.advance();
1106                Some(sym)
1107            }
1108            // Phrasal verb particles can be identifiers (out, up, down, etc.)
1109            TokenType::Particle(_) => {
1110                let sym = t.lexeme;
1111                self.advance();
1112                Some(sym)
1113            }
1114            // Prepositions can be identifiers in code context (from, into, etc.)
1115            TokenType::Preposition(_) => {
1116                let sym = t.lexeme;
1117                self.advance();
1118                Some(sym)
1119            }
1120            // Phase 103: Accept Focus tokens as identifiers (e.g., "Just" for Maybe variants)
1121            TokenType::Focus(_) => {
1122                let sym = t.lexeme;
1123                self.advance();
1124                Some(sym)
1125            }
1126            // Phase 103: Accept Nothing token as identifier (for Maybe/Option variants)
1127            TokenType::Nothing => {
1128                let sym = t.lexeme;
1129                self.advance();
1130                Some(sym)
1131            }
1132            // Phase 103: Accept Article tokens as type parameter names (L, R, A, etc.)
1133            TokenType::Article(_) => {
1134                let sym = t.lexeme;
1135                self.advance();
1136                Some(sym)
1137            }
1138            // Phase 103: Accept Either token as type name (for Either type definition)
1139            TokenType::Either => {
1140                let sym = t.lexeme;
1141                self.advance();
1142                Some(sym)
1143            }
1144            // Calendar unit tokens can be type/variant/field names (Day, Week, Month, Year)
1145            TokenType::CalendarUnit(_) => {
1146                let sym = t.lexeme;
1147                self.advance();
1148                Some(sym)
1149            }
1150            _ => None
1151        }
1152    }
1153
1154    fn check_word(&self, word: &str) -> bool {
1155        if let Some(token) = self.peek() {
1156            // Check against the lexeme of the token
1157            self.interner.resolve(token.lexeme).eq_ignore_ascii_case(word)
1158        } else {
1159            false
1160        }
1161    }
1162
1163    fn skip_to_period(&mut self) {
1164        while self.pos < self.tokens.len() {
1165            if matches!(self.peek(), Some(Token { kind: TokenType::Period, .. })) {
1166                self.advance();
1167                break;
1168            }
1169            self.advance();
1170        }
1171    }
1172
1173    fn check_colon(&self) -> bool {
1174        matches!(self.peek(), Some(Token { kind: TokenType::Colon, .. }))
1175    }
1176
1177    fn check_newline(&self) -> bool {
1178        matches!(self.peek(), Some(Token { kind: TokenType::Newline, .. }))
1179    }
1180
1181    fn check_indent(&self) -> bool {
1182        matches!(self.peek(), Some(Token { kind: TokenType::Indent, .. }))
1183    }
1184
1185    fn check_dedent(&self) -> bool {
1186        matches!(self.peek(), Some(Token { kind: TokenType::Dedent, .. }))
1187    }
1188
1189    fn check_comma(&self) -> bool {
1190        matches!(self.peek(), Some(Token { kind: TokenType::Comma, .. }))
1191    }
1192
1193    fn check_period(&self) -> bool {
1194        matches!(self.peek(), Some(Token { kind: TokenType::Period, .. }))
1195    }
1196
1197    fn check_either(&self) -> bool {
1198        matches!(self.peek(), Some(Token { kind: TokenType::Either, .. }))
1199    }
1200
1201    fn check_lparen(&self) -> bool {
1202        matches!(self.peek(), Some(Token { kind: TokenType::LParen, .. }))
1203    }
1204
1205    fn check_rparen(&self) -> bool {
1206        matches!(self.peek(), Some(Token { kind: TokenType::RParen, .. }))
1207    }
1208
1209    /// Phase 49c: Check for AddWins token
1210    fn check_addwins(&self) -> bool {
1211        matches!(self.peek(), Some(Token { kind: TokenType::AddWins, .. }))
1212    }
1213
1214    /// Phase 49c: Check for RemoveWins token
1215    fn check_removewins(&self) -> bool {
1216        matches!(self.peek(), Some(Token { kind: TokenType::RemoveWins, .. }))
1217    }
1218
1219    /// Phase 49c: Check for YATA token
1220    fn check_yata(&self) -> bool {
1221        matches!(self.peek(), Some(Token { kind: TokenType::YATA, .. }))
1222    }
1223
1224    /// Phase 49c: Check for "to" (either TokenType::To or preposition "to")
1225    fn check_to(&self) -> bool {
1226        match self.peek() {
1227            Some(Token { kind: TokenType::To, .. }) => true,
1228            Some(Token { kind: TokenType::Preposition(sym), .. }) => {
1229                self.interner.resolve(*sym) == "to"
1230            }
1231            _ => false,
1232        }
1233    }
1234
1235    /// Phase 49c: Check for "from" (either TokenType::From or preposition "from")
1236    fn check_from(&self) -> bool {
1237        match self.peek() {
1238            Some(Token { kind: TokenType::From, .. }) => true,
1239            Some(Token { kind: TokenType::Preposition(sym), .. }) => {
1240                self.interner.resolve(*sym) == "from"
1241            }
1242            _ => false,
1243        }
1244    }
1245
1246    /// Phase 47: Check for Portable token
1247    fn check_portable(&self) -> bool {
1248        matches!(self.peek(), Some(Token { kind: TokenType::Portable, .. }))
1249    }
1250
1251    /// Phase 49: Check for Shared token
1252    fn check_shared(&self) -> bool {
1253        matches!(self.peek(), Some(Token { kind: TokenType::Shared, .. }))
1254    }
1255
1256    // Phase 34: Bracket checks for type parameters
1257    fn check_lbracket(&self) -> bool {
1258        matches!(self.peek(), Some(Token { kind: TokenType::LBracket, .. }))
1259    }
1260
1261    fn check_rbracket(&self) -> bool {
1262        matches!(self.peek(), Some(Token { kind: TokenType::RBracket, .. }))
1263    }
1264
1265    /// Phase 34: Parse type parameters in brackets: "[T]" or "[A] and [B]"
1266    fn parse_type_params(&mut self) -> Vec<Symbol> {
1267        let mut params = Vec::new();
1268
1269        loop {
1270            if self.check_lbracket() {
1271                self.advance(); // consume [
1272                if let Some(param) = self.consume_noun_or_proper() {
1273                    params.push(param);
1274                }
1275                if self.check_rbracket() {
1276                    self.advance(); // consume ]
1277                }
1278            }
1279
1280            // Check for "and" separator for multi-param generics
1281            if self.check_word("and") {
1282                self.advance();
1283                continue;
1284            }
1285            break;
1286        }
1287        params
1288    }
1289
1290    /// Phase 34: Parse a field type reference, recognizing type parameters
1291    fn consume_field_type_with_params(&mut self, type_params: &[Symbol]) -> FieldType {
1292        // Bug fix: Handle parenthesized type expressions: "Seq of (Seq of Int)"
1293        if self.check_lparen() {
1294            self.advance(); // consume "("
1295            let inner_type = self.consume_field_type_with_params(type_params);
1296            if self.check_rparen() {
1297                self.advance(); // consume ")"
1298            }
1299            return inner_type;
1300        }
1301
1302        // Phase 34: Single-letter type params like "A" may be tokenized as Article
1303        // Check for Article that matches a type param first
1304        if let Some(Token { kind: TokenType::Article(_), lexeme, .. }) = self.peek() {
1305            let text = self.interner.resolve(*lexeme);
1306            // Find matching type param by name (case-insensitive for single letters)
1307            for &param_sym in type_params {
1308                let param_name = self.interner.resolve(param_sym);
1309                if text.eq_ignore_ascii_case(param_name) {
1310                    self.advance(); // consume the article token
1311                    return FieldType::TypeParam(param_sym);
1312                }
1313            }
1314            // Article didn't match a type param, skip it (e.g., "a Tally" -> "Tally")
1315            self.advance();
1316        }
1317
1318        if let Some(name) = self.consume_noun_or_proper() {
1319            // Check if this is a type parameter reference
1320            if type_params.contains(&name) {
1321                return FieldType::TypeParam(name);
1322            }
1323
1324            let name_str = self.interner.resolve(name);
1325
1326            // Phase 49c: Check for bias/algorithm modifier on SharedSet: "SharedSet (AddWins) of T"
1327            let modified_name = if name_str == "SharedSet" || name_str == "ORSet" {
1328                if self.check_lparen() {
1329                    self.advance(); // consume "("
1330                    let modifier = if self.check_removewins() {
1331                        self.advance(); // consume "RemoveWins"
1332                        Some("SharedSet_RemoveWins")
1333                    } else if self.check_addwins() {
1334                        self.advance(); // consume "AddWins"
1335                        Some("SharedSet_AddWins")
1336                    } else {
1337                        None
1338                    };
1339                    if self.check_rparen() {
1340                        self.advance(); // consume ")"
1341                    }
1342                    modifier.map(|m| self.interner.intern(m))
1343                } else {
1344                    None
1345                }
1346            } else if name_str == "SharedSequence" {
1347                // Phase 49c: Check for algorithm modifier on SharedSequence: "SharedSequence (YATA) of T"
1348                if self.check_lparen() {
1349                    self.advance(); // consume "("
1350                    let modifier = if self.check_yata() {
1351                        self.advance(); // consume "YATA"
1352                        Some("SharedSequence_YATA")
1353                    } else {
1354                        None
1355                    };
1356                    if self.check_rparen() {
1357                        self.advance(); // consume ")"
1358                    }
1359                    modifier.map(|m| self.interner.intern(m))
1360                } else {
1361                    None
1362                }
1363            } else {
1364                None
1365            };
1366
1367            // Use modified name if we found a modifier, otherwise use original
1368            let final_name = modified_name.unwrap_or(name);
1369            let final_name_str = self.interner.resolve(final_name);
1370
1371            // Phase 49c: Handle "SharedMap from K to V" / "ORMap from K to V" syntax
1372            if (final_name_str == "SharedMap" || final_name_str == "ORMap") && self.check_from() {
1373                self.advance(); // consume "from"
1374                let key_type = self.consume_field_type_with_params(type_params);
1375                // Expect "to" (can be TokenType::To or preposition)
1376                if self.check_to() {
1377                    self.advance(); // consume "to"
1378                }
1379                let value_type = self.consume_field_type_with_params(type_params);
1380                return FieldType::Generic { base: final_name, params: vec![key_type, value_type] };
1381            }
1382
1383            // Check for generic: "List of Int", "Seq of Text", "List of T", "Map of K to V"
1384            if self.check_preposition("of") {
1385                // Check if this is a Map type that needs two params (before we start mutating)
1386                let is_map_type = final_name_str == "Map" || final_name_str == "HashMap";
1387
1388                self.advance();
1389                let first_param = self.consume_field_type_with_params(type_params);
1390
1391                // For Map/HashMap, check for "to" separator to parse second type parameter
1392                if is_map_type && self.check_to() {
1393                    self.advance(); // consume "to"
1394                    let second_param = self.consume_field_type_with_params(type_params);
1395                    return FieldType::Generic { base: final_name, params: vec![first_param, second_param] };
1396                }
1397
1398                return FieldType::Generic { base: final_name, params: vec![first_param] };
1399            }
1400
1401            // Phase 49b: "Divergent T" syntax (no "of" required)
1402            if final_name_str == "Divergent" {
1403                // Next token should be the inner type
1404                let param = self.consume_field_type_with_params(type_params);
1405                return FieldType::Generic { base: final_name, params: vec![param] };
1406            }
1407
1408            // Check if primitive
1409            match final_name_str {
1410                "Int" | "Nat" | "Text" | "Bool" | "Real" | "Unit" | "Word8" | "Word16" | "Word32"
1411                | "Word64" | "Lanes8Word32" | "Lanes4Word32" | "Lanes16Word8" | "Lanes4Word64" | "Lanes16Word16" => {
1412                    FieldType::Primitive(final_name)
1413                }
1414                _ => FieldType::Named(final_name),
1415            }
1416        } else {
1417            FieldType::Primitive(self.interner.intern("Unknown"))
1418        }
1419    }
1420}
1421
1422// Note: discover_with_imports is defined in the main crate since it needs
1423// access to the project::Loader which is part of the compile system.
1424
1425#[cfg(test)]
1426mod tests {
1427    use super::*;
1428    use crate::Lexer;
1429    use crate::mwe;
1430
1431    fn make_tokens(source: &str, interner: &mut Interner) -> Vec<Token> {
1432        let mut lexer = Lexer::new(source, interner);
1433        let tokens = lexer.tokenize();
1434        let mwe_trie = mwe::build_mwe_trie();
1435        mwe::apply_mwe_pipeline(tokens, &mwe_trie, interner)
1436    }
1437
1438    #[test]
1439    fn discovery_finds_generic_in_definition_block() {
1440        let source = "## Definition\nA Stack is a generic collection.";
1441        let mut interner = Interner::new();
1442        let tokens = make_tokens(source, &mut interner);
1443
1444        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1445        let registry = discovery.run();
1446
1447        let stack = interner.intern("Stack");
1448        assert!(registry.is_generic(stack), "Stack should be discovered as generic");
1449    }
1450
1451    #[test]
1452    fn discovery_parses_struct_with_fields() {
1453        let source = r#"## Definition
1454A Point has:
1455    an x, which is Int.
1456    a y, which is Int.
1457"#;
1458        let mut interner = Interner::new();
1459        let tokens = make_tokens(source, &mut interner);
1460
1461        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1462        let registry = discovery.run();
1463
1464        let point = interner.intern("Point");
1465        assert!(registry.is_type(point), "Point should be registered");
1466
1467        if let Some(TypeDef::Struct { fields, generics, .. }) = registry.get(point) {
1468            assert_eq!(fields.len(), 2, "Point should have 2 fields, got {:?}", fields);
1469            assert_eq!(interner.resolve(fields[0].name), "x");
1470            assert_eq!(interner.resolve(fields[1].name), "y");
1471            assert!(generics.is_empty(), "Point should have no generics");
1472        } else {
1473            panic!("Point should be a struct with fields");
1474        }
1475    }
1476
1477    #[test]
1478    fn discovery_works_with_markdown_header() {
1479        // Phase 36: LOGOS files have `# Header` before `## Definition`
1480        let source = r#"# Geometry
1481
1482## Definition
1483A Point has:
1484    an x, which is Int.
1485"#;
1486        let mut interner = Interner::new();
1487        let tokens = make_tokens(source, &mut interner);
1488
1489        // Debug: print tokens to see what we're getting
1490        for (i, tok) in tokens.iter().enumerate() {
1491            eprintln!("Token {}: {:?}", i, tok.kind);
1492        }
1493
1494        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1495        let registry = discovery.run();
1496        let point = interner.intern("Point");
1497        assert!(registry.is_type(point), "Point should be discovered even with # header");
1498    }
1499
1500    #[test]
1501    fn discovery_parses_portable_enum() {
1502        let source = r#"## Definition
1503A Command is Portable and is either:
1504    a Start.
1505    a Stop.
1506    a Pause.
1507"#;
1508        let mut interner = Interner::new();
1509        let tokens = make_tokens(source, &mut interner);
1510
1511        // Debug: print tokens to see what we're getting
1512        eprintln!("Tokens for portable enum:");
1513        for (i, tok) in tokens.iter().enumerate() {
1514            eprintln!("Token {}: {:?} ({})", i, tok.kind, interner.resolve(tok.lexeme));
1515        }
1516
1517        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1518        let registry = discovery.run();
1519
1520        let command = interner.intern("Command");
1521        assert!(registry.is_type(command), "Command should be registered as type");
1522
1523        if let Some(TypeDef::Enum { variants, is_portable, .. }) = registry.get(command) {
1524            eprintln!("Command is_portable: {}", is_portable);
1525            eprintln!("Variants: {:?}", variants.iter().map(|v| interner.resolve(v.name)).collect::<Vec<_>>());
1526            assert!(*is_portable, "Command should be portable");
1527            assert_eq!(variants.len(), 3, "Command should have 3 variants");
1528        } else {
1529            panic!("Command should be an enum, got: {:?}", registry.get(command));
1530        }
1531    }
1532
1533    #[test]
1534    fn discovery_parses_lww_int_field() {
1535        let source = r#"## Definition
1536A Setting is Shared and has:
1537    a volume, which is LastWriteWins of Int.
1538"#;
1539        let mut interner = Interner::new();
1540        let tokens = make_tokens(source, &mut interner);
1541
1542        // Debug: print tokens
1543        eprintln!("Tokens for LWW of Int:");
1544        for (i, tok) in tokens.iter().enumerate() {
1545            eprintln!("{:3}: {:?} ({})", i, tok.kind, interner.resolve(tok.lexeme));
1546        }
1547
1548        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1549        let registry = discovery.run();
1550
1551        let setting = interner.intern("Setting");
1552        assert!(registry.is_type(setting), "Setting should be registered");
1553
1554        if let Some(TypeDef::Struct { fields, is_shared, .. }) = registry.get(setting) {
1555            eprintln!("is_shared: {}", is_shared);
1556            eprintln!("Fields: {:?}", fields.len());
1557            for f in fields {
1558                eprintln!("  field: {} = {:?}", interner.resolve(f.name), f.ty);
1559            }
1560            assert!(*is_shared, "Setting should be shared");
1561            assert_eq!(fields.len(), 1, "Setting should have 1 field");
1562        } else {
1563            panic!("Setting should be a struct, got: {:?}", registry.get(setting));
1564        }
1565    }
1566}